From 9186792cacd4077febfdfedec2143c7b7494b5ab Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:02:49 +0800 Subject: [PATCH 1/6] =?UTF-8?q?feat(config):=20=E6=96=B0=E5=A2=9E=20xagent?= =?UTF-8?q?=5Feffort=5Fcaps=EF=BC=8C=E6=8C=89=E5=A4=96=E9=83=A8=20agent=20?= =?UTF-8?q?=E8=AE=BE=E6=8E=A8=E7=90=86=E5=BC=BA=E5=BA=A6=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 真实跨厂商 E2E 里,共享强度 xhigh 传给 agy 后,gemini-3.8-flash 报 "has no xhigh effort (available: low, medium, high)",每个 gemini 席位 都 DROPOUT。契约规定不支持的强度不悄悄降级,所以需要一个显式上限。 新增共享字段 xagent_effort_caps(如 {"gemini": "high"}):主会话给外部 agent 传 --effort 时不超过它的上限,降过档就在报告里写明;只限制强度, 不换模型。只能写在共享层,null 删除。成员名单和上限共用一个"仅共享层" 校验入口。antigravity-runtime 补上漏写的 auxiliary_panel_members 与新字段。 Co-Authored-By: Claude Opus 5.5 --- ADAPTATION.md | 2 +- README.md | 2 +- README.zh-CN.md | 2 +- bin/config.mjs | 45 ++++++++++++++++++-------- references/antigravity-runtime.md | 2 +- references/runtime.md | 4 +-- scripts/config.test.mjs | 53 +++++++++++++++++++++++++++++++ skills/poteto-mode/SKILL.md | 2 +- skills/setup-pstack/SKILL.md | 4 ++- 9 files changed, 95 insertions(+), 21 deletions(-) diff --git a/ADAPTATION.md b/ADAPTATION.md index fdcc04b..da8b7af 100644 --- a/ADAPTATION.md +++ b/ADAPTATION.md @@ -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. diff --git a/README.md b/README.md index a33be8d..1105f74 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README.zh-CN.md b/README.zh-CN.md index 6102218..39e4319 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -192,7 +192,7 @@ pstack 本身不安装定时器;长任务流程也不保证会话退出或主 共享配置中的 `required_panel_members`(例如 `["xagent:grok", "xagent:gemini"]`)会把这些外部 Agent 追加到两个主控的每个评审组和默认评审组,单个评审组或主控覆盖都无法遗漏它们。评审组里的 `xagent:` 不会被替换成继承模型,也不会以 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。 | 配置值 | 本机执行入口 | |---|---| diff --git a/bin/config.mjs b/bin/config.mjs index 2f99053..5e3fa42 100755 --- a/bin/config.mjs +++ b/bin/config.mjs @@ -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"); @@ -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:[/]`); - } - 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:[/]`); + } + 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(", ")}`); } } @@ -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) { @@ -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`); } @@ -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`); } @@ -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]); @@ -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] }, reasoning_effort: hostPolicy.reasoning_effort ?? config.reasoning_effort ?? null, defaultModel: "inherit-parent", defaultPanel: withRequired(DEFAULT_PANEL), diff --git a/references/antigravity-runtime.md b/references/antigravity-runtime.md index 28b5d54..452eba4 100644 --- a/references/antigravity-runtime.md +++ b/references/antigravity-runtime.md @@ -10,7 +10,7 @@ Resolve `` from this skill's installed path and run: node /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. diff --git a/references/runtime.md b/references/runtime.md index 4c0e41f..0ab3d91 100644 --- a/references/runtime.md +++ b/references/runtime.md @@ -27,9 +27,9 @@ node /bin/config.mjs show --host codex Resolve `` from this skill's installed path. The canonical configuration is `~/.config/pstack/config.json`, outside host plugin caches. If the file is absent, the helper returns inherited defaults without writing anything. Only setup or an explicit configuration request writes preferences. Do not ask every worker to run setup. -Shared `roles`, `panels`, `reasoning_effort`, `required_panel_members`, and `auxiliary_panel_members` define policy. Codex and Claude Code apply their respective host overrides from `hosts.codex` and `hosts.claude`; Antigravity's `show` view ignores both and returns only common policy. A T3 session applies the optional `hosts.t3` section instead, and ignores `hosts.codex` and `hosts.claude` whichever host runs it. Shared model choices are `inherit-parent`, `auto`, or an `xagent:[/]` entry (below); other concrete model IDs belong only in `hosts.codex` or `hosts.claude`, and T3 targets `/` only in `hosts.t3`. Missing roles inherit the main conversation; a missing panel uses the helper's `defaultPanel`, two independent inherited-model attempts plus any required panel members. Model availability is validated against the running host, not against upstream names or this JSON schema. A requested unavailable model is an unresolved choice, not permission to silently substitute. Never splice reasoning effort into a model ID. On Codex and Claude Code, a native agent type's own definition owns its effort when it sets one, and its model when the role is `inherit-parent`; the effective `reasoning_effort` reaches every other worker, including `xagent:` workers through `--effort`. The host adapter names the definition fields. In Antigravity, `reasoning_effort` is a displayed shared preference only; use it only if the current host exposes compatible support. +Shared `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, and `xagent_effort_caps` define policy. Codex and Claude Code apply their respective host overrides from `hosts.codex` and `hosts.claude`; Antigravity's `show` view ignores both and returns only common policy. A T3 session applies the optional `hosts.t3` section instead, and ignores `hosts.codex` and `hosts.claude` whichever host runs it. Shared model choices are `inherit-parent`, `auto`, or an `xagent:[/]` entry (below); other concrete model IDs belong only in `hosts.codex` or `hosts.claude`, and T3 targets `/` only in `hosts.t3`. Missing roles inherit the main conversation; a missing panel uses the helper's `defaultPanel`, two independent inherited-model attempts plus any required panel members. Model availability is validated against the running host, not against upstream names or this JSON schema. A requested unavailable model is an unresolved choice, not permission to silently substitute. Never splice reasoning effort into a model ID. On Codex and Claude Code, a native agent type's own definition owns its effort when it sets one, and its model when the role is `inherit-parent`; the effective `reasoning_effort` reaches every other worker, including `xagent:` workers through `--effort`. The host adapter names the definition fields. In Antigravity, `reasoning_effort` is a displayed shared preference only; use it only if the current host exposes compatible support. -A value `xagent:` (for example `xagent:grok`) names an external agent CLI, not a host model, so it is valid in shared policy. `xagent:/` (for example `xagent:claude/claude-opus-5-5`) also pins that CLI's own model; the model belongs to the agent, not the host, so it is shared policy too. Never pass either as a subagent `model`. Run that worker with `/bin/xagent [/] [--effort ]`, passing everything after `xagent:` as the first argument and the worker's effort as `--effort` (omitted when there is no effective value), only when the selected host exposes the required process execution capability and the task authorizes delegation, as specified in the bundled [xagent skill](../skills/xagent/SKILL.md): `ro` for reviewers, judges, and explorers; `rw` for writers. The parent gives every external worker, including readers, an exclusively owned worktree at the intended revision and every attempt a new output directory. Give it the same standalone brief a native worker would get. Its evidence is process completion, `meta.json` completed/status, its recorded `model`, `effort` and `resolved_models`, and `result.md`, held to the same standard as a native worker's result; a `PASS` still needs the parent's own check. Reject read-only violations and unavailable snapshots without resetting a shared checkout. +A value `xagent:` (for example `xagent:grok`) names an external agent CLI, not a host model, so it is valid in shared policy. `xagent:/` (for example `xagent:claude/claude-opus-5-5`) also pins that CLI's own model; the model belongs to the agent, not the host, so it is shared policy too. Never pass either as a subagent `model`. Run that worker with `/bin/xagent [/] [--effort ]`, passing everything after `xagent:` as the first argument and the worker's effort as `--effort` (omitted when there is no effective value; never above that agent's entry in shared `xagent_effort_caps`, the highest level its CLI's model accepts, and say so when the cap lowered it), only when the selected host exposes the required process execution capability and the task authorizes delegation, as specified in the bundled [xagent skill](../skills/xagent/SKILL.md): `ro` for reviewers, judges, and explorers; `rw` for writers. The parent gives every external worker, including readers, an exclusively owned worktree at the intended revision and every attempt a new output directory. Give it the same standalone brief a native worker would get. Its evidence is process completion, `meta.json` completed/status, its recorded `model`, `effort` and `resolved_models`, and `result.md`, held to the same standard as a native worker's result; a `PASS` still needs the parent's own check. Reject read-only violations and unavailable snapshots without resetting a shared checkout. Shared `required_panel_members` lists `xagent:` entries that Codex and Claude Code `show` append to every effective panel and to `defaultPanel`, so no per-panel or host override can leave them out. Antigravity's read-only view also displays these common entries, but does not start them; apply the delegation rules above only when the host's actual tools and the task authorization support it. On hosts that execute a panel, run every entry of the effective panel; a pool such as `arena cross-judge pool` still selects one. An `xagent:` entry is never replaced by an inherited model or another agent and never dropped to finish with N-1. Unless the user set a time limit, give an external worker at least xagent's 900-second default. A pinned model that the CLI reports it did not run is `DROPOUT` with reason `model_mismatch`, never a quiet substitute. On `DROPOUT`, inspect partial changes and rerun once into a new output directory; without a user-set limit, double the timeout when the first attempt timed out. A second `DROPOUT`, `BLOCKED`, a missing CLI, or an agent xagent rejects is an unresolved required outcome: report it by agent name, keep it out of coverage and consensus claims, and do not call the panel or task complete unless the user waives it. The one exception is a panel entry also listed in shared `auxiliary_panel_members`: it gets the same retry, but its unresolved outcome is a reported gap that does not block a panel whose other members completed. Name it as a gap, keep it out of coverage and consensus claims, and never replace it with another model or agent. The listing never makes a role or any other panel member optional. diff --git a/scripts/config.test.mjs b/scripts/config.test.mjs index 9796a8e..073be56 100644 --- a/scripts/config.test.mjs +++ b/scripts/config.test.mjs @@ -486,6 +486,59 @@ test("Given malformed or host-scoped auxiliary panel members, When update or sho assert.match(shown.stderr, /auxiliary_panel_members/); }); +test("Given xagent effort caps, When every host view runs show, Then each lists them and a null patch removes them", (t) => { + const dir = tempDir(); + t.after(() => rmSync(dir, { recursive: true, force: true })); + const configPath = path.join(dir, "config.json"); + const inputPath = path.join(dir, "patch.json"); + const config = validConfig(); + config.reasoning_effort = "xhigh"; + writeJson(configPath, config); + const shown = (host) => JSON.parse(runCli(["show", "--host", host, "--config", configPath]).stdout).effective; + assert.deepEqual(shown("codex").xagent_effort_caps, {}); + + writeJson(inputPath, { xagent_effort_caps: { gemini: "high", pi: "max" } }); + const update = runCli(["update", "--input", inputPath, "--config", configPath]); + assert.equal(update.status, 0, update.stderr); + for (const host of ["codex", "claude", "t3", "antigravity"]) { + assert.deepEqual(shown(host).xagent_effort_caps, { gemini: "high", pi: "max" }, host); + assert.equal(shown(host).reasoning_effort, "xhigh", host); + } + + writeJson(inputPath, { xagent_effort_caps: null }); + assert.equal(runCli(["update", "--input", inputPath, "--config", configPath]).status, 0); + assert.deepEqual(shown("claude").xagent_effort_caps, {}); + assert.equal(Object.hasOwn(JSON.parse(readFileSync(configPath, "utf8")), "xagent_effort_caps"), false); +}); + +test("Given malformed or host-scoped xagent effort caps, When update or show runs, Then they are rejected without writing", (t) => { + const dir = tempDir(); + t.after(() => rmSync(dir, { recursive: true, force: true })); + const configPath = path.join(dir, "config.json"); + const inputPath = path.join(dir, "patch.json"); + writeJson(configPath, validConfig()); + const original = readFileSync(configPath, "utf8"); + + for (const value of [{}, [], "high", { gemini: "ultra" }, { gemini: null }, { "Gemini Pro": "high" }, { "xagent:gemini": "high" }]) { + writeJson(inputPath, { xagent_effort_caps: value }); + const result = runCli(["update", "--input", inputPath, "--config", configPath]); + assert.notEqual(result.status, 0, JSON.stringify(value)); + assert.equal(readFileSync(configPath, "utf8"), original); + } + writeJson(inputPath, { xagent_effort_caps: { gemini: "high" } }); + for (const host of ["codex", "claude", "t3"]) { + assert.notEqual(runCli(["update", "--host", host, "--input", inputPath, "--config", configPath]).status, 0, host); + assert.equal(readFileSync(configPath, "utf8"), original); + } + + const handWritten = validConfig(); + handWritten.hosts.claude.xagent_effort_caps = { gemini: "high" }; + writeJson(configPath, handWritten); + const shown = runCli(["show", "--host", "claude", "--config", configPath]); + assert.notEqual(shown.status, 0); + assert.match(shown.stderr, /xagent_effort_caps/); +}); + test("Given a malformed xagent entry in common policy, When update runs, Then it is rejected without writing", (t) => { const dir = tempDir(); t.after(() => rmSync(dir, { recursive: true, force: true })); diff --git a/skills/poteto-mode/SKILL.md b/skills/poteto-mode/SKILL.md index 645e85a..6af369a 100644 --- a/skills/poteto-mode/SKILL.md +++ b/skills/poteto-mode/SKILL.md @@ -97,7 +97,7 @@ Classify the current delegate's work, not the whole project's difficulty. After On T3, a role may declare `candidates`. Resolve exactly one through the T3 helper: prefer the parent provider when listed, otherwise the first configured candidate. To request another configured provider for a different model perspective, pass `--provider` and record the reason. A `verification workers` candidate role selects, with `--provider`, a candidate whose resolved model is a different family from the checked work's. A provider name alone does not identify the family; when every candidate resolves to that family, label the review same-family. Do not spawn every candidate or retry a failed selection on another candidate. Explicit `@latest` family selectors use the task's live catalog and resolve to exact model IDs before dispatch; reuse those resolved IDs throughout this task. Native host policy and exact pinned targets keep their existing meaning. -**Match effort to the job.** Before each spawn, start from the role's resolved policy and the effective `reasoning_effort`, then pick the effort the job needs. The policy order is `low`, `medium`, `high`, `xhigh`, `max`; one step above `high` is `xhigh`. Raise it one step, up to `max`, for the hardest or subtlest work. Lower it one step for judgment and two or more steps, down to `low`, for routine work. Verification workers keep the resolved effort. Change only the effort, never the role's model, T3 target, or `xagent:` entry, so a diverse panel stays diverse. Pass it only through the separate effort control the host adapter names, never inside a model ID. With no effective value or no such control, the worker inherits and the reply says so. An `xagent:` worker gets the adjusted value through `bin/xagent --effort`, and with no effective value runs at its CLI's own setting. If the host rejects the adjusted value, do not try another one. Use the resolved value and say so. Fast mode and priority service tiers cost more, so use them only when the user asked. +**Match effort to the job.** Before each spawn, start from the role's resolved policy and the effective `reasoning_effort`, then pick the effort the job needs. The policy order is `low`, `medium`, `high`, `xhigh`, `max`; one step above `high` is `xhigh`. Raise it one step, up to `max`, for the hardest or subtlest work. Lower it one step for judgment and two or more steps, down to `low`, for routine work. Verification workers keep the resolved effort. Change only the effort, never the role's model, T3 target, or `xagent:` entry, so a diverse panel stays diverse. Pass it only through the separate effort control the host adapter names, never inside a model ID. With no effective value or no such control, the worker inherits and the reply says so. An `xagent:` worker gets the adjusted value through `bin/xagent --effort`, capped by its `xagent_effort_caps` entry, and with no effective value runs at its CLI's own setting. If the host rejects the adjusted value, do not try another one. Use the resolved value and say so. Fast mode and priority service tiers cost more, so use them only when the user asked. You own the result. Inspect the changed files, diffs, test evidence, and terminal report. On a failed resume, consolidate the scope before replacing the worker. Report actual same-family or cross-family review accurately. diff --git a/skills/setup-pstack/SKILL.md b/skills/setup-pstack/SKILL.md index 3137b6d..cff0599 100644 --- a/skills/setup-pstack/SKILL.md +++ b/skills/setup-pstack/SKILL.md @@ -20,7 +20,7 @@ In Antigravity, run `node /bin/config.mjs show --host antigravity` 1. Identify the current host from its actual tools and read the effective configuration with `node /bin/config.mjs show --host `. Missing configuration means all roles inherit and panels use two independent attempts; setup is optional. 2. For a setup request, run `node /bin/config.mjs init`. It creates `~/.config/pstack/config.json` only when absent and imports any legacy `~/.codex/pstack/models.json` or `~/.claude/pstack/models.json` into that host's overrides. Existing shared configuration is authoritative. Legacy files remain untouched and are no longer runtime sources. 3. Honor preferences already given. Shared roles and panel entries can use only `inherit-parent`, `auto`, or `xagent:` (an external agent CLI dispatched through xagent, for example `xagent:grok`), optionally with that CLI's own model as `xagent:/`, for example `xagent:claude/claude-opus-5-5`; check the model with that CLI, such as `grok models` or `pi --list-models`, before saving it. Any other concrete model goes into the current host's override after checking actual availability and role constraints. Keep reasoning separate. Do not infer IDs from upstream brand names or silently substitute for an unavailable explicit choice. Preserve the other host's choices. -4. Write a small JSON patch containing only the requested `roles`, `panels`, `reasoning_effort`, `required_panel_members`, and/or `auxiliary_panel_members`. Apply shared preferences with `node /bin/config.mjs update --input `; add `--host codex`, `--host claude`, or `--host t3` for host-specific overrides. The helper merges per-role changes atomically. A null role/panel or null effort removes that override and restores inheritance from the next layer. Delete the scratch patch after readback, not the evidence. Concurrent edits fail visibly rather than overwriting each other. +4. Write a small JSON patch containing only the requested `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, and/or `xagent_effort_caps`. Apply shared preferences with `node /bin/config.mjs update --input `; add `--host codex`, `--host claude`, or `--host t3` for host-specific overrides. The helper merges per-role changes atomically. A null role/panel or null effort removes that override and restores inheritance from the next layer. Delete the scratch patch after readback, not the evidence. Concurrent edits fail visibly rather than overwriting each other. 5. Read back both hosts with `show --host codex` and `show --host claude`, plus `show --host t3` when a T3 section exists. Report the shared path and effective choices. Validation of JSON/model names is not proof of model execution. Workers inherit the resolved policy from the parent; do not configure each worker separately. 6. Inspect the current project's existing verify skill or harness. If neither exists, mention `create-verification-skill` once; generate and exercise one only when requested. @@ -40,6 +40,8 @@ Shared `required_panel_members`, for example `["xagent:grok", "xagent:gemini"]`, Shared `auxiliary_panel_members`, for example `["xagent:gemini"]`, marks external panel members whose failure after the retry is a reported gap instead of a blocker. It adds no member to any panel, so list the agent in the panels it should join. It is rejected in a host override; `null` removes it. +Shared `xagent_effort_caps`, for example `{"gemini": "high"}`, sets the highest effort each external agent receives, for a CLI whose model accepts fewer levels than `reasoning_effort` or a raised job asks for. Read the levels from that CLI's model list, such as `agy models`, before saving. It changes only the effort, never the model. It is rejected in a host override; `null` removes it. + Multi-party steps are the panels above, `reflect judgment` with `reflect tooling`, and `verification workers`. Upstream gives each of them two model families by default; this fork's `inherit-parent` defaults stay one family until configured. When another vendor's CLI is installed and signed in, propose a cross-vendor layout for them: the host's own model plus the other vendor through `xagent:` in each panel, and only the other vendor in `arena cross-judge pool`, since arena picks a single judge from it. Mark a supporting vendor auxiliary rather than required when it should not hold up a review. Confirm the proposal with the user before writing it. On Codex and Claude Code, `reasoning_effort` reaches `xagent:` workers and native agent types whose definition sets no effort; a definition that sets its own effort keeps it. Without a value, an external CLI runs at its own default, which may be the user's interactive setting. From 37439cabcc8b5055e1a6ca92debe60a80d4c3510 Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:06:28 +0800 Subject: [PATCH 2/6] =?UTF-8?q?fix(agents):=20comment-sicko=20=E7=9A=84=20?= =?UTF-8?q?tools=20=E6=94=B9=E7=94=A8=20YAML=20=E5=88=97=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?Antigravity=20=E4=B8=8D=E5=86=8D=E8=B7=B3=E8=BF=87=E5=AE=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit agy plugin validate --strict 报 comment-sicko 的 frontmatter 无效:tools 是逗号字符串,Antigravity 要求列表,于是加载时跳过这个 agent。Claude Code 文档写明 tools 接受逗号字符串或 YAML 列表;用临时插件副本在 headless Claude 里实测,两种写法下 comment-sicko 都只拿到 Read、Grep、Glob,去掉 tools 的 对照组则拿到 425 个工具(含 Bash、Edit、Write)。改后 agy 加载 2 个 agent。 xagent-e2e.sh 的厂商列表和 claude 未登录时的兜底评审改为可用 XAGENT_E2E_AGENTS / XAGENT_E2E_REVIEWER 覆盖,默认值不变;grok 暂停订阅时 可以不带它跑。 Co-Authored-By: Claude Opus 5.5 --- agents/comment-sicko.md | 2 +- scripts/xagent-e2e.sh | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/agents/comment-sicko.md b/agents/comment-sicko.md index e08b476..6e7125e 100644 --- a/agents/comment-sicko.md +++ b/agents/comment-sicko.md @@ -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 diff --git a/scripts/xagent-e2e.sh b/scripts/xagent-e2e.sh index 1ee0544..e13008a 100755 --- a/scripts/xagent-e2e.sh +++ b/scripts/xagent-e2e.sh @@ -3,6 +3,8 @@ # usage: scripts/xagent-e2e.sh [run-dir] evidence stays in run-dir. # claude joins once `claude auth login` is done; until then it is only checked to come back as DROPOUT. # XAGENT_E2E_MODELS="claude/opus codex/gpt-6-astra" pins those agents' models in the selection check. +# XAGENT_E2E_AGENTS="codex gemini pi" limits the vendors exercised (default: codex grok gemini pi), and +# XAGENT_E2E_REVIEWER picks the codex host's reviewer while claude is logged out (default: grok). set -uo pipefail export XAGENT_EPHEMERAL=1 root=$(cd "$(dirname "$0")/.." && pwd) @@ -10,7 +12,7 @@ xagent="$root/bin/xagent" verify="$root/scripts/xagent-e2e-verify.py" run=${1:-$(mktemp -d "${TMPDIR:-/tmp}/xagent-e2e.XXXXXX")} mkdir -p "$run" && run=$(cd "$run" && pwd) -agents="codex grok gemini pi" reviewer=grok +agents=${XAGENT_E2E_AGENTS:-codex grok gemini pi} reviewer=${XAGENT_E2E_REVIEWER:-grok} claude auth status >/dev/null 2>&1 && agents="claude $agents" reviewer=claude claude_ready=1 || claude_ready=0 fail=0 ok() { echo "ok $1"; } From 7b8365008da46483a53262c105fab73daac7cd1c Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:06:36 +0800 Subject: [PATCH 3/6] =?UTF-8?q?chore(release):=20=E6=8F=92=E4=BB=B6?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8D=87=E5=88=B0=200.15.25-shared.8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 发布 xagent_effort_caps 与 comment-sicko 的 frontmatter 修复,两个 manifest 按 docs/guide/01-setup.md 的要求一起升级。 Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 23d2abc..20f8a03 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -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" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index ad41835..17dd3fe 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -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" From ff8b1d7784894f2a9b19ed363b0ef98a54712111 Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:21:44 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(t3):=20T3=20=E8=A7=A3=E6=9E=90=E5=99=A8?= =?UTF-8?q?=E4=B9=9F=E6=8C=89=20xagent=5Feffort=5Fcaps=20=E7=BB=99?= =?UTF-8?q?=E5=A4=96=E9=83=A8=20agent=20=E9=99=8D=E5=BC=BA=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit T3 按 config.mjs resolve 的输出派发外部 worker,而解析器对 xagent 条目 直接返回请求的强度,T3 上 gemini 仍会拿到 xhigh 并 DROPOUT。现在 xagent worker 的 effort 已按上限降档,并带上与 T3 worker 一致的 warnings 数组, 降过档时写明原因;t3-runtime 同步说明,新测试覆盖降档、不受限的 agent 和低于上限的单次强度。 xagent-e2e.sh 的 XAGENT_E2E_AGENTS 说明收窄为只选择写修复、评审和选择 矩阵里的厂商:探针、嵌套派发、超时和 codex-host 用例始终调用 codex、 gemini(agy)和 pi。 Co-Authored-By: Claude Opus 5.5 --- bin/t3-targets.mjs | 14 +++++++++----- references/t3-runtime.md | 2 +- scripts/t3-targets.test.mjs | 19 ++++++++++++++++++- scripts/xagent-e2e.sh | 5 +++-- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/bin/t3-targets.mjs b/bin/t3-targets.mjs index cb73693..ef949c1 100644 --- a/bin/t3-targets.mjs +++ b/bin/t3-targets.mjs @@ -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) : []; @@ -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"; @@ -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 ?? {})), }; } diff --git a/references/t3-runtime.md b/references/t3-runtime.md index a0bb2c6..bc0ae5c 100644 --- a/references/t3-runtime.md +++ b/references/t3-runtime.md @@ -34,7 +34,7 @@ For candidate roles, `selection.strategy` and `selection.entry` explain the chos | --- | --- | | `inherit-parent`, `auto` | Omit `target` (`null` in the preview) to preserve the parent's complete model selection and options. A requested effort is reported as unsent; use an explicit model target to set effort independently. `resolved` names the parent used for validation, not a replacement target. T3 has no automatic model router | | `/` | Use that worker's exact previewed `target`, including its options | -| `xagent:[/]` | The worker has `transport: xagent` and separate `agent`, `model` and `effort`. Run its entry through `bin/xagent` as the shared runtime specifies; never pass it to T3 as a model | +| `xagent:[/]` | The worker has `transport: xagent` and separate `agent`, `model`, `effort` and `warnings`; `effort` is already lowered to the agent's `xagent_effort_caps` entry, with a warning when it was. Run its entry through `bin/xagent` as the shared runtime specifies; never pass it to T3 as a model | For explicit targets, the helper maps effort to the model's listed select control (`effort`, `reasoningEffort`, or `thinking`), without guessing from its provider name. An unsupported value is an error; if it was a per-job adjustment, the shared rule allows retrying the original policy value, with disclosure. A model with no effort control produces a warning and `effort.sent: null`; it runs with its runtime default, so do not claim that the requested effort was applied. Inherited targets preserve all parent options instead: T3 replaces a supplied options object, and the catalog does not expose the parent's complete selected options. Report warnings. The preview adds no speed or service-tier option; do not add one unless the user asked. A preview proves selection only, not authentication or execution. diff --git a/scripts/t3-targets.test.mjs b/scripts/t3-targets.test.mjs index c6b94b1..928ed03 100644 --- a/scripts/t3-targets.test.mjs +++ b/scripts/t3-targets.test.mjs @@ -110,13 +110,30 @@ test("Given mixed and repeated panel seats, When previewed, Then order, independ f.config.hosts.t3.panels.review = ["code/ordinary", "code/ordinary", "xagent:claude/opus[1m]"]; const workers = f.resolved(["--panel", "review"]).workers; assert.deepEqual(workers.map(w => w.entry), ["code/ordinary", "code/ordinary", "xagent:claude/opus[1m]", "xagent:gemini"]); - assert.deepEqual(workers[2], { entry: "xagent:claude/opus[1m]", transport: "xagent", agent: "claude", model: "opus[1m]", effort: "high" }); + assert.deepEqual(workers[2], { entry: "xagent:claude/opus[1m]", transport: "xagent", agent: "claude", model: "opus[1m]", effort: "high", warnings: [] }); assert.equal(workers[3].model, null); const defaultPlan = f.resolved(["--panel", "unconfigured"]); assert.equal(defaultPlan.selection.defaulted, true); assert.deepEqual(defaultPlan.workers.map(w => w.entry), ["inherit-parent", "inherit-parent", "xagent:gemini"]); }); +test("Given an xagent effort cap, When the effort is above it, Then only that agent is lowered and the plan says so", t => { + const f = fixture(t); + f.config.xagent_effort_caps = { gemini: "high" }; + f.config.hosts.t3.reasoning_effort = "xhigh"; + f.config.hosts.t3.panels.review = ["xagent:claude", "judge/strong"]; + const workers = f.resolved(["--panel", "review"]).workers; + assert.deepEqual(workers.map(w => w.entry), ["xagent:claude", "judge/strong", "xagent:gemini"]); + assert.equal(workers[0].effort, "xhigh"); + assert.deepEqual(workers[0].warnings, []); + assert.equal(workers[1].effort.sent, "xhigh"); + assert.equal(workers[2].effort, "high"); + assert.deepEqual(workers[2].warnings, ["xagent:gemini: requested effort xhigh is above its xagent_effort_caps entry, so high is sent"]); + const job = f.resolved(["--panel", "review", "--effort", "medium"]).workers; + assert.equal(job[2].effort, "medium"); + assert.deepEqual(job[2].warnings, []); +}); + test("Given an unavailable explicit target, When any panel member cannot run, Then the whole preview fails without a replacement", t => { const f = fixture(t); f.config.hosts.t3.panels.review = ["code/ordinary", "judge/strong"]; diff --git a/scripts/xagent-e2e.sh b/scripts/xagent-e2e.sh index e13008a..a201cfb 100755 --- a/scripts/xagent-e2e.sh +++ b/scripts/xagent-e2e.sh @@ -3,8 +3,9 @@ # usage: scripts/xagent-e2e.sh [run-dir] evidence stays in run-dir. # claude joins once `claude auth login` is done; until then it is only checked to come back as DROPOUT. # XAGENT_E2E_MODELS="claude/opus codex/gpt-6-astra" pins those agents' models in the selection check. -# XAGENT_E2E_AGENTS="codex gemini pi" limits the vendors exercised (default: codex grok gemini pi), and -# XAGENT_E2E_REVIEWER picks the codex host's reviewer while claude is logged out (default: grok). +# XAGENT_E2E_AGENTS="codex gemini pi" picks the vendors in the fix, review and selection matrix (default: +# codex grok gemini pi), and XAGENT_E2E_REVIEWER picks the codex host's reviewer while claude is logged out +# (default: grok). The probe, nested-dispatch, timeout and codex-hosted cases always run codex, gemini (agy) and pi. set -uo pipefail export XAGENT_EPHEMERAL=1 root=$(cd "$(dirname "$0")/.." && pwd) From 06c0e36202df48b95de4c2b85a5b4be8833e3e1c Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:38:45 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(e2e):=20Codex=20fork=20=E5=87=BA?= =?UTF-8?q?=E7=9A=84=E5=AD=90=20agent=20=E4=B8=8D=E5=86=8D=E8=A2=AB?= =?UTF-8?q?=E8=AF=AF=E5=88=A4=E4=B8=BA=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第二轮跨厂商 E2E 中,codex host 的原生评审实际已完成(首尾快照、结论 No findings、task_complete),校验器却记为 failed、模型为空。原因是那个 子 agent 由主会话 fork 而来:它的 rollout 开头复制了主会话的 session_meta 和 turn_context。解析器每遇到一条 session_meta 就覆盖 linked,于是被复制 的那条改成 False;两条 turn_context 的模型不同,resolved 也变成空。 现在跳过带主会话 turn_id 的复制记录和不属于该子线程的条目,linked 只认 id 为子线程本身的 session_meta。新测试覆盖 fork 后完成和未完成两种情况; 换回旧解析器时它得到 ('failed', None, False),与真实失败一致。 Co-Authored-By: Claude Opus 5.5 --- scripts/e2e-native-evidence.py | 9 ++++++-- scripts/pstack-host-e2e-native.test.py | 32 ++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/scripts/e2e-native-evidence.py b/scripts/e2e-native-evidence.py index 837c078..c338cb2 100644 --- a/scripts/e2e-native-evidence.py +++ b/scripts/e2e-native-evidence.py @@ -153,8 +153,10 @@ def payloads(path): if thread not in rollouts: return [], f"no Codex rollout for thread {thread} under {sessions}" - requested, efforts, returned, started, finished = {}, {}, set(), {}, set() + requested, efforts, returned, started, finished, parent_turns = {}, {}, set(), {}, set(), set() for _, payload in payloads(rollouts[thread]): + if isinstance(payload.get("turn_id"), str): + parent_turns.add(payload["turn_id"]) if payload.get("type") == "function_call" and payload.get("name") == "spawn_agent": try: arguments = json.loads(payload.get("arguments") or "{}") @@ -176,9 +178,12 @@ def payloads(path): child_rollout = rollouts.get(child) if isinstance(child, str) else None linked, models, complete, aborted, touched, edited, commands = False, [], False, False, set(), set(), [] for kind, payload in payloads(child_rollout) if child_rollout else (): + # A forked child's rollout begins with a copy of the parent's history, which keeps the parent's turn ids. + if payload.get("turn_id") in parent_turns or payload.get("thread_id") not in (None, child): + continue item = payload.get("item") if payload.get("type") == "item_completed" and isinstance(payload.get("item"), dict) else {} if kind == "session_meta": - linked = payload.get("parent_thread_id") == thread + linked = linked or (payload.get("id") == child and payload.get("parent_thread_id") == thread) elif kind == "turn_context": models.append(payload.get("model")) elif item.get("type") == "CommandExecution": diff --git a/scripts/pstack-host-e2e-native.test.py b/scripts/pstack-host-e2e-native.test.py index 3fad5d1..25278aa 100644 --- a/scripts/pstack-host-e2e-native.test.py +++ b/scripts/pstack-host-e2e-native.test.py @@ -86,15 +86,18 @@ def claude_stream(*calls): return [json.dumps(event) for event in events] -def write_codex_rollouts(home, thread, spawns, parent_rollout=True, effort=None): +def write_codex_rollouts(home, thread, spawns, parent_rollout=True, effort=None, forked=False): """Write rollouts shaped like `codex exec` sessions; return the matching `--json` host log. Each spawn is (call, requested model, child thread, model the child ran, finished, *actions), where an action - is a shell command the child ran, ("run", command, output), or edits(path). + is a shell command the child ran, ("run", command, output), or edits(path). A forked child's rollout starts + with a copy of the parent's history under the parent's turn id, as Codex writes it. """ day = home / "sessions" / "2026" / "10" / "16" day.mkdir(parents=True, exist_ok=True) - parent = [{"type": "session_meta", "payload": {"id": thread}}, {"type": "turn_context", "payload": {"model": "gpt-6-astra"}}] + parent_turn = f"{thread}-turn" + parent_context = {"type": "turn_context", "payload": {"model": "gpt-6-astra", "turn_id": parent_turn}} + parent = [{"type": "session_meta", "payload": {"id": thread}}, parent_context] for call, requested, child, actual, finished, *actions in spawns: arguments = {"task_name": call, "fork_turns": "none", "message": "brief"} if requested is not None: @@ -109,8 +112,13 @@ def write_codex_rollouts(home, thread, spawns, parent_rollout=True, effort=None) "type": "SubAgentActivity", "kind": "started", "id": call, "agent_thread_id": child, "agent_path": f"/root/{call}"}}}) if actual is None: continue - events = [{"type": "session_meta", "payload": {"id": child, "parent_thread_id": thread}}, - {"type": "turn_context", "payload": {"model": actual}}] + events = [{"type": "session_meta", "payload": {"id": child, "parent_thread_id": thread, **({"forked_from_id": thread} if forked else {})}}] + if forked: + events += [{"type": "session_meta", "payload": {"id": thread}}, parent_context, + {"type": "event_msg", "payload": {"type": "item_completed", "thread_id": thread, "turn_id": parent_turn, "item": { + "type": "CommandExecution", "command": ["/bin/zsh", "-lc", "echo parent"], "aggregated_output": "parent", "status": "completed"}}}, + {"type": "event_msg", "payload": {"type": "task_complete", "turn_id": parent_turn, "last_agent_message": "parent done"}}] + events.append({"type": "turn_context", "payload": {"model": actual, "turn_id": f"{child}-turn"}}) for index, action in enumerate(actions): if isinstance(action, tuple) and action[0] == "edit": events.append({"type": "event_msg", "payload": {"type": "item_completed", "item": { @@ -171,6 +179,20 @@ def test_given_host_notes_named_meta_json_beside_xagent_receipts_when_collected_ checks = self.valid_receipts(receipts, native_dispatches=native, **CODEX_MIXED) self.assertEqual(checks, ALL_CHECKS_PASS) + def test_given_a_forked_codex_child_when_checked_then_its_copy_of_the_parent_history_is_ignored(self): + home = self.root / "codex-home-forked" + native, gap = codex_native_dispatches(write_codex_rollouts(home, "thread-forked", [ + ("call-done", None, "thread-done", "gpt-6-luna", True, ("run", "echo child", "child")), + ("call-open", None, "thread-open", "gpt-6-luna", False, ("run", "echo still", "still")), + ], forked=True), home) + + self.assertIsNone(gap) + done, still_open = native + self.assertEqual((done.status, done.resolved, done.usable), ("completed", "gpt-6-luna", True)) + self.assertEqual([command.command for command in done.commands], ["echo child"]) + self.assertEqual((still_open.status, still_open.resolved), ("pending", "gpt-6-luna")) + self.assertEqual([command.command for command in still_open.commands], ["echo still"]) + def test_given_mixed_claude_policy_when_stream_shows_native_dispatches_and_xagent_receipts_then_all_checks_pass(self): receipts = self.xagent_reviews_for(CLAUDE_MIXED["reviewers"]) native, gap = claude_native_dispatches(claude_stream( From b1bce6e7f605baacd45e3da8bd2b6f512d268d7d Mon Sep 17 00:00:00 2001 From: Martin <60476606+mtmtian@users.noreply.github.com> Date: Sun, 18 Oct 2026 15:49:03 +0800 Subject: [PATCH 6/6] =?UTF-8?q?docs(xagent):=20=E5=8F=AA=E9=80=89=E9=83=A8?= =?UTF-8?q?=E5=88=86=E5=8E=82=E5=95=86=E7=9A=84=20xagent-e2e=20=E8=BF=90?= =?UTF-8?q?=E8=A1=8C=E8=AE=B0=E4=B8=BA=E9=83=A8=E5=88=86=E8=AF=81=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit xagent skill 说 xagent-e2e.sh 会"在两种模式下跑每个 agent、调用每家厂商"。 有了 XAGENT_E2E_AGENTS 之后,只选部分厂商的运行不再满足这一点,却可能被 当成完整的跨厂商证据。现在写明:默认才调用每家厂商;选了子集时,两种模式 只覆盖列出的厂商,固定用例仍会调用 codex、gemini 和 pi,记录时要标为部分 证据并写出被排除的厂商。 Co-Authored-By: Claude Opus 5.5 --- skills/xagent/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/xagent/SKILL.md b/skills/xagent/SKILL.md index 1996b83..1e3790f 100644 --- a/skills/xagent/SKILL.md +++ b/skills/xagent/SKILL.md @@ -52,4 +52,4 @@ A role or panel entry `xagent:` or `xagent:/` runs that wor ## Checks - `scripts/xagent-classify.test.sh`, `python3 scripts/xagent.test.py` and `node --test scripts/xagent-pi-guard.test.mjs`: offline, including deterministic read-only violations and unresponsive/cancelled processes. -- `scripts/xagent-e2e.sh [run-dir]`: every agent in both modes, each CLI accepting `--effort` and any model pinned in `XAGENT_E2E_MODELS`, allowlist denials, the agy mirror, nested refusal, timeout, and Codex as host. Calls every vendor. +- `scripts/xagent-e2e.sh [run-dir]`: every agent in both modes, each CLI accepting `--effort` and any model pinned in `XAGENT_E2E_MODELS`, allowlist denials, the agy mirror, nested refusal, timeout, and Codex as host. Calls every vendor by default. A run with `XAGENT_E2E_AGENTS` covers only the listed vendors in both modes, while its fixed cases still call codex, gemini and pi; record it as partial evidence that names the vendors left out.