Skip to content
Merged
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.6",
"version": "0.15.25-shared.7",
"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.6",
"version": "0.15.25-shared.7",
"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
6 changes: 5 additions & 1 deletion ADAPTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ The 0.15.21 sync (upstream 0.15.16–0.15.21, #484, #486, #536, #538, #539, #570

The 0.15.25 sync (upstream 0.15.22–0.15.25, #633 and #634: #595, #597, #602, #604, #606, #609, #613, #615, #616, #628, #633) takes the `in-your-own-words` and `sharpen` skills with the local frontmatter and runtime-contract line, the shorter visual `ketchup` reply (host-rendered charts instead of `/visualize`), ketchup catch-ups at the end of autopilot ticks and replies, the restated attack-the-premise, fix-root-causes, make-operations-idempotent, and test-behavior principles, the cut marginal sections in interrogate's lead-judgment reference, swarm, and the migrate-callers and type-system-discipline principles, the always-on signal after a deep dig, the full-swarm-once review cadence in autopilot-full and multi-phase-plan, the principle-first rewrites of autopilot-full, autopilot-stack, and shipping, and Graphite's removal from `orch frontier set`. The per-job effort rule ("match the model to the job") becomes "match effort to the job" in poteto-mode's Subagents section: workers start from the resolved role and `reasoning_effort`, rise one step for the hardest work and drop one step for judgment or two or more for routine work, and pass the value only through each host's separate effort control (Codex `reasoning_effort`, the Claude Agent `effort` field, T3 `target.options`). It never changes a role's model, T3 target, or `xagent:<agent>` entry, so panels stay diverse. xagent workers originally kept their CLI's own setting; they now get the adjusted value through `bin/xagent --effort`. Upstream's urgency-based fast mode is skipped: fast mode and priority tiers cost more and stay off unless the user asks, which also matches the T3 overlay. The autopilot and shipping rewrites keep this fork's isolated host workers instead of Cursor cloud agents, the project harness instead of `control-ui`/`control-cli`, the selected state directory instead of the root agent's system-prompt store, an authorized host scheduler instead of `/loop 1h`, the installed playbook instead of `origin/main`, merges only under explicit merge authorization, `--auto` only on request, and replacement of a stuck agent only after its ownership is resolved. `orch frontier set` takes upstream's base-chain discovery through `gh` or `--forge origin` in place of the optional Graphite path, but keeps this fork's authoritative `--prs` list, which reads each PR's state and head SHA with `gh pr view` and so works from a detached worker worktree; `--prs` with `--forge origin` fails because Origin's list output carries no head SHA. The discovery path also carries local fixes for defects in upstream's version, found in independent review: `gh` queries pin `--repo` to the GitHub origin and drop PRs whose head repository is another fork, so a same-named fork branch never gets the local SHA; every list passes `--limit 1000` and fails when the result may be truncated, since `gh pr list` returns 30 rows by default; trunk comes from `origin/HEAD`, else from whichever of `main` and `master` exists, remote refs first, and fails when both or neither exist instead of assuming `main`; and `ssh://[email protected]/` origins are accepted alongside HTTPS and scp-style ones, here and in `worktree-audit.sh`. Recovery hints offer `--prs` only where it works, on GitHub. Origin's list output has no head repository, so `--forge origin` cannot drop same-named fork PRs; prefer `gh` or `--prs` where forks exist. Shipping reads and fixes the bottom PR's base before every merge, the first after a restart included, because a PR still targeting its merged parent merges into that branch. The T3 overlay treats a queued `WAITING`/`merge-queue` as a wait in Shipping, which stops only at `COMPLETE` or a blocker. The multi-phase-plan skeleton makes its rebase items depend on the execution playbook, because Autopilot-stack owners never rebase. `worktree-audit.sh` keeps its GitHub-only PR query, because Origin's list output has no head repository and the audit must not match a same-named fork branch. `poteto-agent.md` resolves poteto-mode from the Claude install record or the Codex plugin cache instead of `~/.cursor/plugins/`. The T3 overlay's no-polling watch-pr rule now covers Shipping as well as Babysit. The Cursor `/` menu note in the README is skipped.

Model tiering by role follows upstream again. Poteto-mode's Subagents section restores upstream's "Code delegates tier by difficulty" routing that earlier adaptations dropped: each code playbook's delegate reads its role, the hardest changes read `hardest tasks`, prose and judgment read `judgment and prose`, and verification workers read `swarm workers`. The models come from `config.json`, where every role still inherits the parent by default, instead of upstream's Cursor slug defaults, and effort still moves per job within the role's model. `how`, `why` and `reflect` name their role keys, such as `how explorer` and `reflect judgment`, so a configured role is the one they read.
Model tiering by role follows upstream again. Poteto-mode's Subagents section restores upstream's "Code delegates tier by difficulty" routing that earlier adaptations dropped: each code playbook's delegate reads its role, the hardest changes read `hardest tasks`, prose and judgment read `judgment and prose`, and verification workers read `verification workers`. The models come from `config.json`, where every role still inherits the parent by default, instead of upstream's Cursor slug defaults, and effort still moves per job within the role's model. `how`, `why` and `reflect` name their role keys, such as `how explorer` and `reflect judgment`, so a configured role is the one they read.

`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.

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. 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. 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: 2 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,8 @@ 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。

| 配置值 | 本机执行入口 |
|---|---|
| `xagent:claude` | `claude` |
Expand Down
34 changes: 20 additions & 14 deletions bin/config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ const REASONING_EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
const DEFAULT_PANEL = ["inherit-parent", "inherit-parent"];
// External agents that must join every panel, so a per-panel or per-host list cannot leave them out.
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];

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

// null means unset, in a stored config as in a patch.
function validateRequiredMembers(policy, scope, label) {
if (!Object.hasOwn(policy, REQUIRED_MEMBERS)) return;
const field = `${label}.${REQUIRED_MEMBERS}`;
if (scope !== "common") throw new Error(`${field} is shared policy and cannot be set per host`);
const members = policy[REQUIRED_MEMBERS];
if (members === null) return;
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>]`);
function validateMemberLists(policy, scope, label) {
for (const key of MEMBER_LISTS) {
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`);
}
if (new Set(members).size !== members.length) throw new Error(`${field} entries must be unique`);
}

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

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

function validatePatch(patch, scope, label) {
requireObject(patch, label);
const allowed = new Set(["roles", "panels", "reasoning_effort", REQUIRED_MEMBERS]);
const allowed = new Set(["roles", "panels", "reasoning_effort", ...MEMBER_LISTS]);
for (const key of Object.keys(patch)) {
if (!allowed.has(key)) throw new Error(`${label}.${key} is not a supported update field`);
}
Expand All @@ -181,7 +186,7 @@ function validatePatch(patch, scope, label) {
}
}
}
validateRequiredMembers(patch, scope, label);
validateMemberLists(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 @@ -288,7 +293,7 @@ function applyPatch(policy, patch) {
});
}
}
for (const key of ["reasoning_effort", REQUIRED_MEMBERS]) {
for (const key of ["reasoning_effort", ...MEMBER_LISTS]) {
if (!Object.hasOwn(patch, key)) continue;
if (patch[key] === null) delete policy[key];
else policy[key] = cloneJson(patch[key]);
Expand Down Expand Up @@ -328,6 +333,7 @@ export function showConfig({ configPath = getDefaultConfigPath(), host } = {}) {
roles: { ...config.roles, ...hostPolicy.roles },
panels: Object.fromEntries(Object.entries(panels).map(([name, panel]) => [name, withRequired(panel)])),
[REQUIRED_MEMBERS]: [...required],
[AUXILIARY_MEMBERS]: [...(config[AUXILIARY_MEMBERS] ?? [])],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the Antigravity policy contract

When --host antigravity reads a config containing this new field, the output now includes auxiliary_panel_members, but references/antigravity-runtime.md:13,21 still explicitly enumerates the returned/displayed shared values without it. Antigravity workflows are required to read that adapter, so they can ignore the auxiliary designation and continue treating a failed auxiliary xagent as a blocking required outcome; document the field and its nonblocking panel semantics in that adapter alongside this output change.

Useful? React with 👍 / 👎.

reasoning_effort: hostPolicy.reasoning_effort ?? config.reasoning_effort ?? null,
defaultModel: "inherit-parent",
defaultPanel: withRequired(DEFAULT_PANEL),
Expand Down
Loading
Loading