Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack",
"version": "0.15.25-shared.7",
"version": "0.15.25-shared.8",
"description": "Shared pstack engineering workflows for Claude Code and Codex, with one configuration and host-specific runtime adapters.",
"author": {
"name": "Local adaptation; upstream Lauren Tan"
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack-codex",
"version": "0.15.25-shared.7",
"version": "0.15.25-shared.8",
"description": "Shared pstack engineering workflows for Codex and Claude Code, with one configuration and native host adapters.",
"author": {
"name": "Lauren Tan; local Codex adaptation"
Expand Down
2 changes: 1 addition & 1 deletion ADAPTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ Model tiering by role follows upstream again. Poteto-mode's Subagents section re

`verification workers` is a local role with no upstream line. Upstream runs orchestrate's verifier and show-me-your-work's handback review on a different model family than the work. On Codex or Claude Code another family needs an `xagent:` entry, so verification takes its own role instead of sharing `swarm workers` with fan-out workers, and each host can point it at the other vendor while swarms keep their own worker. A swarm's verify lanes, as in autopilot's swarm-verify, and the handback review read it too. On T3 a candidate role picks the candidate whose resolved model family differs from the checked work's, because a provider name alone does not identify the family.

Two local rules keep cross-vendor panels usable. Shared `auxiliary_panel_members` names external members whose unresolved outcome is a reported gap rather than a blocker, so a supporting vendor's outage does not stop a review its primary members finished; upstream reaches the same availability by falling back within a family, which xagent entries never do. Native agent definitions own their effort: on Codex and Claude Code a worker's effort goes to a native agent type only when its definition sets none, so `reasoning_effort` mainly reaches `xagent:` workers, whose CLIs otherwise run at the user's interactive default. setup-pstack proposes a cross-vendor layout for multi-party steps when another vendor's CLI is signed in, because this fork's `inherit-parent` defaults stay one family where upstream's defaults pair two.
Three local rules keep cross-vendor panels usable. Shared `auxiliary_panel_members` names external members whose unresolved outcome is a reported gap rather than a blocker, so a supporting vendor's outage does not stop a review its primary members finished; upstream reaches the same availability by falling back within a family, which xagent entries never do. Native agent definitions own their effort: on Codex and Claude Code a worker's effort goes to a native agent type only when its definition sets none, so `reasoning_effort` mainly reaches `xagent:` workers, whose CLIs otherwise run at the user's interactive default. Shared `xagent_effort_caps` keeps that effort within what each external CLI's model accepts: an unsupported level is a `DROPOUT`, never a quiet downgrade, so a shared `xhigh` would otherwise drop every seat of a CLI whose model stops at `high`, as agy's `gemini-3.8-flash` does. setup-pstack proposes a cross-vendor layout for multi-party steps when another vendor's CLI is signed in, because this fork's `inherit-parent` defaults stay one family where upstream's defaults pair two.

The orch store lock carries a local fix for three races that upstream still has at `e5136d82`: a writer killed between the exclusive open and its pid write left an empty lock that only `--force` could pass, two writers that saw the same dead holder could both take the lock, and a release could unlink the lock a `--force` writer had just put in its place. `acquireLock` now writes the pid to a private file and hard-links it into place, falling back to the exclusive open where hard links are unsupported. Every read-then-unlink of the lock, in a takeover or a release, runs behind a `.orch.lock.takeover` claim, and a takeover re-reads the holder before it unlinks. A writer that meets a live claim is refused with `is being released or replaced; retry`; a release that meets one retries every 10 ms, so `close` waits while a live claimant, even a stopped one, holds it. The fix and its multi-process race tests are adapted from michael-denyer/pstack-claude #219 (`eef8f7c6`) and #238 (`de804764`), MIT, Copyright (c) 2026 Michael Denyer, whose notice is added to `LICENSE`. That fork's TLA+ model and its retry when a lock vanishes before its holder is read are not taken. The lock lives in `skills/poteto-mode/scripts/orch/lock.ts`, with `UserError` and `errorCode` in `orch/errors.ts`, because this fork replaced upstream's lock wholesale and `store.ts` was already past 1,000 lines; its race tests live in `orch/lock.test.ts`, sharing `orch/store.test-helper.ts` with `orch.test.ts`. Each acquire also removes the private pid files and staged claims that a writer killed mid-claim leaves behind, judged by the pid in their names, which the kill-at-every-step test now checks. The frontier code stays in `store.ts` even though it is large, so upstream's edits there still merge file by file. Its GitHub remote parsing is `skills/poteto-mode/scripts/github-remote.ts`, moved out of upstream's `watch-pr/github.ts` so `orch` and `watch-pr` share one parser.

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Marketplace registration is needed only once per host. Host caches are generated
- Claude Code entry: `/pstack:poteto-mode`.
- Optional setup: `$pstack-codex:setup-pstack` or `/pstack:setup-pstack`.

Preferences are shared in `~/.config/pstack/config.json`. Roles inherit the main conversation unless configured; workers receive the resolved policy and do not need setup. Concrete model choices are host-scoped so one vendor's model ID never leaks into another host. The exception is an external agent's own model, such as `xagent:claude/claude-opus-5-5`: xagent hands it to that agent's CLI from either host, so it can be shared. Shared reasoning and panel-size preferences apply to both when the actual runtime supports them. Shared `required_panel_members` adds external agents such as `xagent:grok` to every panel on both hosts, and those entries are never silently skipped. Shared `auxiliary_panel_members` marks external members, such as `xagent:gemini`, whose failure after a retry is reported as a gap instead of blocking a panel the other members finished. On Codex and Claude Code, an agent type whose definition sets its own effort keeps it; shared reasoning reaches the rest, including external agents. See [the runtime contract](./references/runtime.md).
Preferences are shared in `~/.config/pstack/config.json`. Roles inherit the main conversation unless configured; workers receive the resolved policy and do not need setup. Concrete model choices are host-scoped so one vendor's model ID never leaks into another host. The exception is an external agent's own model, such as `xagent:claude/claude-opus-5-5`: xagent hands it to that agent's CLI from either host, so it can be shared. Shared reasoning and panel-size preferences apply to both when the actual runtime supports them. Shared `required_panel_members` adds external agents such as `xagent:grok` to every panel on both hosts, and those entries are never silently skipped. Shared `auxiliary_panel_members` marks external members, such as `xagent:gemini`, whose failure after a retry is reported as a gap instead of blocking a panel the other members finished. Shared `xagent_effort_caps`, such as `{"gemini": "high"}`, keeps an external agent's effort within the levels its CLI's model accepts. On Codex and Claude Code, an agent type whose definition sets its own effort keeps it; shared reasoning reaches the rest, including external agents. See [the runtime contract](./references/runtime.md).

Examples below use `$skill` as a short reference to the bundled skill; in Claude Code invoke `/pstack:skill` or read its file through the host's skill interface.

Expand Down
2 changes: 1 addition & 1 deletion README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ pstack 本身不安装定时器;长任务流程也不保证会话退出或主

共享配置中的 `required_panel_members`(例如 `["xagent:grok", "xagent:gemini"]`)会把这些外部 Agent 追加到两个主控的每个评审组和默认评审组,单个评审组或主控覆盖都无法遗漏它们。评审组里的 `xagent:<agent>` 不会被替换成继承模型,也不会以 N-1 静默跳过;重试和失败报告规则见[运行时契约](./references/runtime.md)。

共享配置中的 `auxiliary_panel_members`(例如 `["xagent:gemini"]`)把这些外部 Agent 标为辅助成员:重试一次后仍失败,只记为覆盖缺口,不阻塞其余成员已完成的评审组;它们仍不会被替换成别的模型。在 Codex 和 Claude Code 上,agent 定义里自己设了推理强度的子 Agent 保持定义里的值,共享的 `reasoning_effort` 作用于其余 worker,包括外部 Agent。
共享配置中的 `auxiliary_panel_members`(例如 `["xagent:gemini"]`)把这些外部 Agent 标为辅助成员:重试一次后仍失败,只记为覆盖缺口,不阻塞其余成员已完成的评审组;它们仍不会被替换成别的模型。共享配置中的 `xagent_effort_caps`(例如 `{"gemini": "high"}`)给外部 Agent 设推理强度上限,适用于模型支持的档位比 `reasoning_effort` 少的 CLI;它只限制强度,不会换模型。在 Codex 和 Claude Code 上,agent 定义里自己设了推理强度的子 Agent 保持定义里的值,共享的 `reasoning_effort` 作用于其余 worker,包括外部 Agent。

| 配置值 | 本机执行入口 |
|---|---|
Expand Down
2 changes: 1 addition & 1 deletion agents/comment-sicko.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name: comment-sicko
description: Read-only review of comments and workaround code in a caller-scoped diff. Propose changes with evidence; never edit application code.
model: inherit
tools: Read, Grep, Glob
tools: [Read, Grep, Glob]
---

# Comment Sicko
Expand Down
45 changes: 32 additions & 13 deletions bin/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ const REQUIRED_MEMBERS = "required_panel_members";
// External agents whose unresolved outcome is reported as a gap instead of blocking their panel.
const AUXILIARY_MEMBERS = "auxiliary_panel_members";
const MEMBER_LISTS = [REQUIRED_MEMBERS, AUXILIARY_MEMBERS];
// The highest effort each external agent's CLI accepts; the parent passes it the lower of the job's effort and this cap.
const EFFORT_CAPS = "xagent_effort_caps";
const XAGENT_NAME = /^[a-z][a-z0-9-]*$/;
// Fields that only common policy may set, because xagent runs the same way from every host.
const SHARED_FIELDS = [...MEMBER_LISTS, EFFORT_CAPS];

export function getDefaultConfigPath(homeDir = homedir()) {
return path.join(homeDir, ".config", "pstack", "config.json");
Expand Down Expand Up @@ -124,18 +129,31 @@ function validateEffort(policy, label) {
}

// null means unset, in a stored config as in a patch.
function validateMemberLists(policy, scope, label) {
for (const key of MEMBER_LISTS) {
function validateSharedFields(policy, scope, label) {
for (const key of SHARED_FIELDS) {
if (!Object.hasOwn(policy, key)) continue;
const field = `${label}.${key}`;
if (scope !== "common") throw new Error(`${field} is shared policy and cannot be set per host`);
const members = policy[key];
if (members === null) continue;
if (!Array.isArray(members) || members.length === 0) throw new Error(`${field} must be a nonempty array or null`);
for (const member of members) {
if (typeof member !== "string" || !XAGENT_VALUE.test(member)) throw new Error(`${field} entries must be xagent:<agent>[/<model>]`);
}
if (new Set(members).size !== members.length) throw new Error(`${field} entries must be unique`);
const value = policy[key];
if (value === null) continue;
if (key === EFFORT_CAPS) validateEffortCaps(value, field);
else validateMemberList(value, field);
}
}

function validateMemberList(members, field) {
if (!Array.isArray(members) || members.length === 0) throw new Error(`${field} must be a nonempty array or null`);
for (const member of members) {
if (typeof member !== "string" || !XAGENT_VALUE.test(member)) throw new Error(`${field} entries must be xagent:<agent>[/<model>]`);
}
if (new Set(members).size !== members.length) throw new Error(`${field} entries must be unique`);
}

function validateEffortCaps(caps, field) {
if (!isObject(caps) || Object.keys(caps).length === 0) throw new Error(`${field} must be a nonempty object or null`);
for (const [agent, effort] of Object.entries(caps)) {
if (!XAGENT_NAME.test(agent)) throw new Error(`${field} keys must be xagent agent names`);
if (!REASONING_EFFORTS.has(effort)) throw new Error(`${field}.${agent} must be one of ${[...REASONING_EFFORTS].join(", ")}`);
}
}

Expand All @@ -147,7 +165,7 @@ function validatePolicy(policy, scope, label) {
validateRoles(policy.roles, scope, label);
validatePanels(policy.panels, scope, label);
validateEffort(policy, label);
validateMemberLists(policy, scope, label);
validateSharedFields(policy, scope, label);
}

function validateConfig(config) {
Expand All @@ -162,7 +180,7 @@ function validateConfig(config) {

function validatePatch(patch, scope, label) {
requireObject(patch, label);
const allowed = new Set(["roles", "panels", "reasoning_effort", ...MEMBER_LISTS]);
const allowed = new Set(["roles", "panels", "reasoning_effort", ...SHARED_FIELDS]);
for (const key of Object.keys(patch)) {
if (!allowed.has(key)) throw new Error(`${label}.${key} is not a supported update field`);
}
Expand All @@ -186,7 +204,7 @@ function validatePatch(patch, scope, label) {
}
}
}
validateMemberLists(patch, scope, label);
validateSharedFields(patch, scope, label);
if (Object.hasOwn(patch, "reasoning_effort") && patch.reasoning_effort !== null && !REASONING_EFFORTS.has(patch.reasoning_effort)) {
throw new Error(`${label}.reasoning_effort must be one of ${[...REASONING_EFFORTS].join(", ")} or null`);
}
Expand Down Expand Up @@ -293,7 +311,7 @@ function applyPatch(policy, patch) {
});
}
}
for (const key of ["reasoning_effort", ...MEMBER_LISTS]) {
for (const key of ["reasoning_effort", ...SHARED_FIELDS]) {
if (!Object.hasOwn(patch, key)) continue;
if (patch[key] === null) delete policy[key];
else policy[key] = cloneJson(patch[key]);
Expand Down Expand Up @@ -334,6 +352,7 @@ export function showConfig({ configPath = getDefaultConfigPath(), host } = {}) {
panels: Object.fromEntries(Object.entries(panels).map(([name, panel]) => [name, withRequired(panel)])),
[REQUIRED_MEMBERS]: [...required],
[AUXILIARY_MEMBERS]: [...(config[AUXILIARY_MEMBERS] ?? [])],
[EFFORT_CAPS]: { ...config[EFFORT_CAPS] },
Comment thread
mtmtian marked this conversation as resolved.
reasoning_effort: hostPolicy.reasoning_effort ?? config.reasoning_effort ?? null,
defaultModel: "inherit-parent",
defaultPanel: withRequired(DEFAULT_PANEL),
Expand Down
14 changes: 9 additions & 5 deletions bin/t3-targets.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Turn a resolved pstack policy and one live T3 catalog into dispatch arguments.
// This module neither launches tasks nor reads or changes configuration.
const EFFORT_IDS = new Set(["effort", "reasoningEffort", "thinking"]);
const EFFORT_ORDER = ["low", "medium", "high", "xhigh", "max"];

function unique(rows, predicate, label) {
const matches = Array.isArray(rows) ? rows.filter(predicate) : [];
Expand Down Expand Up @@ -73,15 +74,18 @@ function effortFor(model, requested, label) {
return { effort: { requested, sent: requested, option: control.id }, warnings: [] };
}

function workerFor(entry, catalog, requestedEffort) {
function workerFor(entry, catalog, requestedEffort, effortCaps) {
if (entry.startsWith("xagent:")) {
const spec = entry.slice("xagent:".length);
const slash = spec.indexOf("/");
const agent = slash < 0 ? spec : spec.slice(0, slash);
const cap = effortCaps[agent];
const lowered = requestedEffort !== null && cap !== undefined && EFFORT_ORDER.indexOf(requestedEffort) > EFFORT_ORDER.indexOf(cap);
return {
entry, transport: "xagent",
agent: slash < 0 ? spec : spec.slice(0, slash),
entry, transport: "xagent", agent,
model: slash < 0 ? null : spec.slice(slash + 1),
effort: requestedEffort,
effort: lowered ? cap : requestedEffort,
warnings: lowered ? [`${entry}: requested effort ${requestedEffort} is above its xagent_effort_caps entry, so ${cap} is sent`] : [],
};
}
const inherited = entry === "inherit-parent" || entry === "auto";
Expand Down Expand Up @@ -146,6 +150,6 @@ export function resolveT3Targets(policy, catalog, { role, panel, effort, provide
const requestedEffort = effort ?? policy.reasoning_effort ?? null;
return {
selection,
workers: entries.map(entry => workerFor(entry, catalog, requestedEffort)),
workers: entries.map(entry => workerFor(entry, catalog, requestedEffort, policy.xagent_effort_caps ?? {})),
};
}
2 changes: 1 addition & 1 deletion references/antigravity-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Resolve `<plugin-root>` from this skill's installed path and run:
node <plugin-root>/bin/config.mjs show --host antigravity
```

The command reads `~/.config/pstack/config.json`. It returns the common `roles`, `panels`, `reasoning_effort`, and `required_panel_members` values. It ignores the Codex and Claude Code host overrides, because the stored schema defines no Antigravity-specific preferences. If the file is absent, it prints inherited defaults and creates neither the file nor its parent directory.
The command reads `~/.config/pstack/config.json`. It returns the common `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, and `xagent_effort_caps` values. It ignores the Codex and Claude Code host overrides, because the stored schema defines no Antigravity-specific preferences. If the file is absent, it prints inherited defaults and creates neither the file nor its parent directory.

`update --host antigravity` is unsupported and must fail without changing preferences. Use an existing supported setup or update path only for an explicitly requested configuration change.

Expand Down
Loading
Loading