diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 54dddf9..0e864fa 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "0.15.25-shared.9", + "version": "0.15.25-shared.10", "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 13bbedc..2ed5325 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack-codex", - "version": "0.15.25-shared.9", + "version": "0.15.25-shared.10", "description": "Shared pstack engineering workflows for Codex and Claude Code, with one configuration and native host adapters.", "author": { "name": "Lauren Tan; local Codex adaptation" diff --git a/ADAPTATION.md b/ADAPTATION.md index dd47472..bff08f0 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 the helper requires `--checked` for this role and picks the candidate whose model family differs from the checked work's, read from model IDs, because a provider name alone does not identify the family and the parent-provider default would choose the parent's own family. -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. +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. Shared `model_effort_caps` bounds cost instead: upstream raises the hardest work one step above the base effort, so a base of `xhigh` reaches `max` on exactly the models difficulty routing picks for that work, such as Fable and Astra. A cap keyed by model-name part stops only those models, native, T3 or external, while other models keep the raise and the model choice by difficulty stays; a single global ceiling would also have stopped subtle work on cheaper models. The T3 resolver enforces it and the native parents apply it from poteto-mode's effort rule. 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 1105f74..3acb5f5 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. 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). +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. Shared `model_effort_caps`, such as `{"fable": "xhigh", "astra": "xhigh"}`, is the highest effort a worker on that model receives on any host, so raising the effort for the hardest work stops there on expensive models while others can still reach `max`. 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 39e4319..da116f1 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 标为辅助成员:重试一次后仍失败,只记为覆盖缺口,不阻塞其余成员已完成的评审组;它们仍不会被替换成别的模型。共享配置中的 `xagent_effort_caps`(例如 `{"gemini": "high"}`)给外部 Agent 设推理强度上限,适用于模型支持的档位比 `reasoning_effort` 少的 CLI;它只限制强度,不会换模型。在 Codex 和 Claude Code 上,agent 定义里自己设了推理强度的子 Agent 保持定义里的值,共享的 `reasoning_effort` 作用于其余 worker,包括外部 Agent。 +共享配置中的 `auxiliary_panel_members`(例如 `["xagent:gemini"]`)把这些外部 Agent 标为辅助成员:重试一次后仍失败,只记为覆盖缺口,不阻塞其余成员已完成的评审组;它们仍不会被替换成别的模型。共享配置中的 `xagent_effort_caps`(例如 `{"gemini": "high"}`)给外部 Agent 设推理强度上限,适用于模型支持的档位比 `reasoning_effort` 少的 CLI;它只限制强度,不会换模型。共享配置中的 `model_effort_caps`(例如 `{"fable": "xhigh", "astra": "xhigh"}`)按模型名设推理强度上限:所有宿主上跑在这些模型上的 worker,最难工作的升档到此为止,其他模型照样可以升到 `max`;同样只限制强度,不换模型。在 Codex 和 Claude Code 上,agent 定义里自己设了推理强度的子 Agent 保持定义里的值,共享的 `reasoning_effort` 作用于其余 worker,包括外部 Agent。 | 配置值 | 本机执行入口 | |---|---| diff --git a/bin/config.mjs b/bin/config.mjs index 3c25658..3761c95 100755 --- a/bin/config.mjs +++ b/bin/config.mjs @@ -27,9 +27,13 @@ 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"; +// The highest effort a worker on an expensive model receives, keyed by a dash-separated part of the model name +// (fable, astra), so every host's ID or alias for that model shares one cost ceiling. +const MODEL_EFFORT_CAPS = "model_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]; +const MODEL_NAME_PART = /^[a-z][a-z0-9]*$/; +// Fields that only common policy may set, because xagent and each model's cost are the same from every host. +const SHARED_FIELDS = [...MEMBER_LISTS, EFFORT_CAPS, MODEL_EFFORT_CAPS]; export function getDefaultConfigPath(homeDir = homedir()) { return path.join(homeDir, ".config", "pstack", "config.json"); @@ -136,7 +140,8 @@ function validateSharedFields(policy, scope, label) { if (scope !== "common") throw new Error(`${field} is shared policy and cannot be set per host`); const value = policy[key]; if (value === null) continue; - if (key === EFFORT_CAPS) validateEffortCaps(value, field); + if (key === EFFORT_CAPS) validateEffortCaps(value, field, XAGENT_NAME, "xagent agent names"); + else if (key === MODEL_EFFORT_CAPS) validateEffortCaps(value, field, MODEL_NAME_PART, "single model-name parts such as fable"); else validateMemberList(value, field); } } @@ -149,11 +154,11 @@ function validateMemberList(members, field) { if (new Set(members).size !== members.length) throw new Error(`${field} entries must be unique`); } -function validateEffortCaps(caps, field) { +function validateEffortCaps(caps, field, keyPattern, keyLabel) { 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(", ")}`); + for (const [key, effort] of Object.entries(caps)) { + if (!keyPattern.test(key)) throw new Error(`${field} keys must be ${keyLabel}`); + if (!REASONING_EFFORTS.has(effort)) throw new Error(`${field}.${key} must be one of ${[...REASONING_EFFORTS].join(", ")}`); } } @@ -353,6 +358,7 @@ export function showConfig({ configPath = getDefaultConfigPath(), host } = {}) { [REQUIRED_MEMBERS]: [...required], [AUXILIARY_MEMBERS]: [...(config[AUXILIARY_MEMBERS] ?? [])], [EFFORT_CAPS]: { ...config[EFFORT_CAPS] }, + [MODEL_EFFORT_CAPS]: { ...config[MODEL_EFFORT_CAPS] }, reasoning_effort: hostPolicy.reasoning_effort ?? config.reasoning_effort ?? null, defaultModel: "inherit-parent", defaultPanel: withRequired(DEFAULT_PANEL), diff --git a/bin/t3-targets.mjs b/bin/t3-targets.mjs index edfd4d8..b694ab4 100644 --- a/bin/t3-targets.mjs +++ b/bin/t3-targets.mjs @@ -93,18 +93,37 @@ function effortFor(model, requested, label) { return { effort: { requested, sent: requested, option: control.id }, warnings: [] }; } -function workerFor(entry, catalog, requestedEffort, effortCaps) { +// The requested effort lowered to a cap, with the warning that says so; a missing effort or cap changes nothing. +function capped(requested, cap, label, field) { + if (requested == null || cap == null || EFFORT_ORDER.indexOf(requested) <= EFFORT_ORDER.indexOf(cap)) { + return { effort: requested, warning: null }; + } + return { effort: cap, warning: `${label}: requested effort ${requested} is above its ${field} entry, so ${cap} is sent` }; +} + +// The lowest model_effort_caps entry naming a dash-separated part of the model ID's last segment, +// so fable matches the fable alias and claude-fable-5-1, and astra matches gpt-6-astra. +function modelCapFor(modelId, modelCaps) { + if (typeof modelId !== "string") return undefined; + const parts = modelId.slice(modelId.lastIndexOf("/") + 1).toLowerCase().split("-"); + return Object.entries(modelCaps) + .filter(([name]) => parts.includes(name)) + .map(([, cap]) => cap) + .sort((a, b) => EFFORT_ORDER.indexOf(a) - EFFORT_ORDER.indexOf(b))[0]; +} + +function workerFor(entry, catalog, requestedEffort, caps) { 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); const model = modelOf(entry, catalog); + const byModel = capped(requestedEffort, modelCapFor(model, caps.model), entry, "model_effort_caps"); + const byAgent = capped(byModel.effort, caps.agent[agent], entry, "xagent_effort_caps"); return { entry, transport: "xagent", agent, model, family: familyOf(model), - effort: lowered ? cap : requestedEffort, - warnings: lowered ? [`${entry}: requested effort ${requestedEffort} is above its xagent_effort_caps entry, so ${cap} is sent`] : [], + effort: byAgent.effort, + warnings: [byModel.warning, byAgent.warning].filter(Boolean), }; } const inherited = entry === "inherit-parent" || entry === "auto"; @@ -123,12 +142,15 @@ function workerFor(entry, catalog, requestedEffort, effortCaps) { ], }; } - const { effort, warnings } = effortFor(model, requestedEffort, `${providerId}/${model.id}`); + const label = `${providerId}/${model.id}`; + const byModel = capped(requestedEffort, modelCapFor(model.id, caps.model), label, "model_effort_caps"); + const { effort, warnings } = effortFor(model, byModel.effort, label); const target = { providerInstanceId: providerId, model: model.id }; if (effort.sent !== null) target.options = { [effort.option]: effort.sent }; return { entry, transport: "t3", target, - resolved: { providerInstanceId: providerId, model: model.id }, family: familyOf(model.id), effort, warnings, + resolved: { providerInstanceId: providerId, model: model.id }, family: familyOf(model.id), effort, + warnings: [byModel.warning, ...warnings].filter(Boolean), }; } @@ -186,11 +208,12 @@ export function resolveT3Targets(policy, catalog, { role, panel, effort, provide entries = kind === "role" ? [value] : value; } const requestedEffort = effort ?? policy.reasoning_effort ?? null; + const caps = { agent: policy.xagent_effort_caps ?? {}, model: policy.model_effort_caps ?? {} }; const auxiliary = new Set(policy.auxiliary_panel_members ?? []); return { selection, workers: entries.map(entry => { - const worker = workerFor(entry, catalog, requestedEffort, policy.xagent_effort_caps ?? {}); + const worker = workerFor(entry, catalog, requestedEffort, caps); // The auxiliary listing only relaxes panel seats; a role never becomes optional. worker.auxiliary = kind === "panel" && auxiliary.has(entry); if (checked !== undefined && !crossFamily(worker.family)) { diff --git a/references/antigravity-runtime.md b/references/antigravity-runtime.md index 452eba4..84d86d3 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`, `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. +The command reads `~/.config/pstack/config.json`. It returns the common `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, `xagent_effort_caps`, and `model_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 0ab3d91..9c925b2 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`, `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. +Shared `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, `xagent_effort_caps`, and `model_effort_caps` define policy. `model_effort_caps`, set only in common policy, maps a dash-separated part of a model name, such as `fable` or `astra`, to the highest effort a worker on that model receives after the per-job adjustment on any host; it matches every ID or alias with that part, such as `fable` and `claude-fable-5-1`, lowers effort only, never changes a model, and leaves other models' raises alone. 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; 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. +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 its pinned model's `model_effort_caps` entry or 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/references/t3-runtime.md b/references/t3-runtime.md index 0b56143..f995d1e 100644 --- a/references/t3-runtime.md +++ b/references/t3-runtime.md @@ -27,7 +27,7 @@ node /bin/config.mjs resolve --host t3 --panel 'interrogate reviewe node /bin/config.mjs resolve --host t3 --role 'verification workers' --checked inherit-parent --catalog ``` -Use the role the playbook requests. For a per-job effort adjustment, add `--effort ` after applying poteto-mode's rule. The helper reads the same effective policy as `show`; it writes nothing and starts no task. `selection.defaulted` means that role or panel was absent and used the shared inherited default. It checks each T3 provider, child capability, cross-provider capability, exact model and effort choice. An invalid member fails the whole preview before dispatch; report the named target and constraints. Do not substitute another model, provider, or `xagent:` unless the user says so. +Use the role the playbook requests. For a per-job effort adjustment, add `--effort ` after applying poteto-mode's rule. The helper lowers a worker's effort to its model's shared `model_effort_caps` entry, matched on the resolved or pinned model ID, with a warning; workers on other models keep the requested effort. The helper reads the same effective policy as `show`; it writes nothing and starts no task. `selection.defaulted` means that role or panel was absent and used the shared inherited default. It checks each T3 provider, child capability, cross-provider capability, exact model and effort choice. An invalid member fails the whole preview before dispatch; report the named target and constraints. Do not substitute another model, provider, or `xagent:` unless the user says so. A step that checks other work resolves with `--checked `, naming what produced that work: `inherit-parent` when the parent wrote it, the writer's resolved `/`, or `xagent:/`. Every worker reports `family`, read only from the last segment of its model ID: canonical `claude-`, `gpt-`, `gemini-` and `grok-` IDs name a family, anything else is unknown. A provider name never decides it, because one provider can route other vendors' models. A candidate role then takes the first candidate, the parent provider's first, whose known family differs from the checked work's (`selection.strategy: cross-family`). When none does, or either family is unknown, the default selection stands with a warning: label that review same-family, or say it is not cross-family evidence. `verification workers` fails without `--checked` unless `--provider` names a candidate for a reason the brief states. For a pool such as `arena cross-judge pool`, resolve the panel with `--checked inherit-parent` and choose a seat without a family warning. An `xagent:` entry without a model has an unknown family; pin its model where family evidence matters. diff --git a/scripts/check-fork.mjs b/scripts/check-fork.mjs index 845162f..e8dbf30 100644 --- a/scripts/check-fork.mjs +++ b/scripts/check-fork.mjs @@ -188,6 +188,13 @@ export const REQUIRED = [ hint: "the handback review runs on the verification workers role", section: "upstream-behavior-retained", }, + { + id: "effort-cap-raise", + file: "skills/poteto-mode/SKILL.md", + pattern: /Raise it one step, up to `max` or the worker model's shared `model_effort_caps` entry, for the hardest or subtlest work\./, + hint: "the hardest-work effort raise stops at the worker model's model_effort_caps entry", + section: "upstream-behavior-retained", + }, { id: "t3-no-service-tier", file: "references/t3-runtime.md", diff --git a/scripts/check-fork.test.mjs b/scripts/check-fork.test.mjs index aa453ba..252e560 100644 --- a/scripts/check-fork.test.mjs +++ b/scripts/check-fork.test.mjs @@ -169,6 +169,7 @@ const DROPPED = [ ["verification-workers-role", "skills/poteto-mode/SKILL.md", "Verification workers, including a swarm's verify lanes and the **show-me-your-work** handback review, read `verification workers`.", ""], ["swarm-verify-role", "skills/swarm/SKILL.md", "Workers that verify another agent's work, as in a swarm-verify, read `verification workers` instead.", ""], ["handback-review-role", "skills/show-me-your-work/SKILL.md", "on the `verification workers` role from the effective policy, on a different available model family", "on a different model family"], + ["effort-cap-raise", "skills/poteto-mode/SKILL.md", "up to `max` or the worker model's shared `model_effort_caps` entry,", "up to `max`,"], ["t3-no-service-tier", "references/t3-runtime.md", "The preview adds no speed or service-tier option; do not add one unless the user asked.", ""], ["t3-catalog-preview", "references/t3-runtime.md", "config.mjs resolve --host t3", "removed-by-sync"], ["t3-worktree-isolation", "references/t3-runtime.md", 'isolation: "worktree"', "removed-by-sync"], diff --git a/scripts/config.test.mjs b/scripts/config.test.mjs index 073be56..5d79518 100644 --- a/scripts/config.test.mjs +++ b/scripts/config.test.mjs @@ -539,6 +539,38 @@ test("Given malformed or host-scoped xagent effort caps, When update or show run assert.match(shown.stderr, /xagent_effort_caps/); }); +test("Given model effort caps, When every host view runs show, Then each lists them, only common policy may set them, and null 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"); + writeJson(configPath, validConfig()); + const shown = (host) => JSON.parse(runCli(["show", "--host", host, "--config", configPath]).stdout).effective; + assert.deepEqual(shown("claude").model_effort_caps, {}); + + writeJson(inputPath, { model_effort_caps: { fable: "xhigh", astra: "xhigh" } }); + assert.equal(runCli(["update", "--input", inputPath, "--config", configPath]).status, 0); + for (const host of ["codex", "claude", "t3", "antigravity"]) { + assert.deepEqual(shown(host).model_effort_caps, { fable: "xhigh", astra: "xhigh" }, host); + } + + const original = readFileSync(configPath, "utf8"); + for (const host of ["codex", "claude", "t3"]) { + assert.notEqual(runCli(["update", "--host", host, "--input", inputPath, "--config", configPath]).status, 0, host); + } + // A key must be one dash-separated part of a model name, so claude-fable could never match. + for (const value of [{}, "xhigh", { fable: "ultra" }, { "claude-fable": "xhigh" }, { Fable: "xhigh" }]) { + writeJson(inputPath, { model_effort_caps: value }); + assert.notEqual(runCli(["update", "--input", inputPath, "--config", configPath]).status, 0, JSON.stringify(value)); + } + assert.equal(readFileSync(configPath, "utf8"), original); + + writeJson(inputPath, { model_effort_caps: null }); + assert.equal(runCli(["update", "--input", inputPath, "--config", configPath]).status, 0); + assert.deepEqual(shown("codex").model_effort_caps, {}); + assert.equal(Object.hasOwn(JSON.parse(readFileSync(configPath, "utf8")), "model_effort_caps"), false); +}); + 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/scripts/t3-targets.test.mjs b/scripts/t3-targets.test.mjs index d2ff0e3..5089bce 100644 --- a/scripts/t3-targets.test.mjs +++ b/scripts/t3-targets.test.mjs @@ -134,6 +134,32 @@ test("Given an xagent effort cap, When the effort is above it, Then only that ag assert.deepEqual(job[2].warnings, []); }); +test("Given model effort caps, When a job asks for more, Then only workers on a capped model are lowered and say so", t => { + const f = fixture(t); + const model = id => ({ id, options: [{ id: "effort", type: "select", options: ["high", "xhigh", "max"].map(id => ({ id })) }] }); + f.catalog.providers[1].models.push(...["claude-fable-5-1", "gpt-6-astra", "gpt-6.1-sol"].map(model)); + f.config.model_effort_caps = { fable: "xhigh", astra: "xhigh" }; + f.config.xagent_effort_caps = { gemini: "high", claude: "high" }; + f.config.hosts.t3.panels.review = ["judge/claude-fable-5-1", "judge/gpt-6-astra", "judge/gpt-6.1-sol", "xagent:claude/claude-fable-5-1", "xagent:codex/fable"]; + const workers = f.resolved(["--panel", "review", "--effort", "max"]).workers; + const effortOf = w => w.target?.options.effort ?? w.effort; + assert.deepEqual(workers.map(w => [w.entry, effortOf(w)]), [ + ["judge/claude-fable-5-1", "xhigh"], + ["judge/gpt-6-astra", "xhigh"], + // An uncapped model keeps the raise. + ["judge/gpt-6.1-sol", "max"], + // The model cap applies first; the agent's own lower cap still applies after it. + ["xagent:claude/claude-fable-5-1", "high"], + ["xagent:codex/fable", "xhigh"], + // Without a pinned model, an xagent worker has no model to cap. + ["xagent:gemini", "high"], + ]); + assert.match(workers[0].warnings.join(" "), /max is above its model_effort_caps entry, so xhigh/); + assert.deepEqual(workers[2].warnings, []); + assert.deepEqual(workers[3].warnings.map(w => /model_effort_caps|xagent_effort_caps/.exec(w)[0]), ["model_effort_caps", "xagent_effort_caps"]); + assert.deepEqual(f.resolved(["--panel", "review", "--effort", "high"]).workers.slice(0, 3).flatMap(w => w.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/skills/poteto-mode/SKILL.md b/skills/poteto-mode/SKILL.md index 0b2f346..d3f2b38 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. Resolve `verification workers` with `--checked`, naming what produced the checked work; the helper takes a candidate whose model is a different family and warns when none is. A provider name alone does not identify the family; label a warned 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`, 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. +**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` or the worker model's shared `model_effort_caps` entry, 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 e3fd8b6..dd28078 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`, `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. +4. Write a small JSON patch containing only the requested `roles`, `panels`, `reasoning_effort`, `required_panel_members`, `auxiliary_panel_members`, `xagent_effort_caps`, and/or `model_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. @@ -42,6 +42,8 @@ Shared `auxiliary_panel_members`, for example `["xagent:gemini"]`, marks externa 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. +Shared `model_effort_caps`, for example `{"fable": "xhigh", "astra": "xhigh"}`, sets the highest effort a worker on an expensive model receives on any host, so poteto-mode's one-step raise for the hardest work stops there on that model while other models still reach `max`. Each key is one dash-separated part of a model name and matches every ID or alias containing it, such as `fable` and `claude-fable-5-1`. Propose it when the user wants to bound the cost of particular models. 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. For the T3 section, build the same layout from T3 seats. Give each vendor whose models the live catalog lists on a provider that can run child tasks a seat or candidate: `/@latest` for the Claude and GPT families the helper follows, and an exact catalog ID for any other, such as a Gemini or Grok model. Use `xagent:` only for a vendor the catalog lacks, and pin its model as `xagent:/` when its family must count as cross-family evidence. A T3 parent can run on either provider, so give `verification workers` candidates of different families and list each family once in `arena cross-judge pool`; resolving with `--checked` picks the other family at run time. Candidates still allow one target per provider, so when one provider routes both families, a candidate role holds only one of them: set the exact target of the family other than your usual parent's, and `--checked` warns when it matches. Confirm this proposal too before writing it.