diff --git a/CHANGES.md b/CHANGES.md index ca4c99e..1815324 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,14 @@ This port applies the Cursor → Claude Code substitutions in skill bodies. Earlier drafts left them flagged; this revision resolves them. A later pass added a Codex build that shares the same skills; see [Codex port](#codex-port) below. +## Unreleased adds opt-in Sonnet, Astra, Luna, and Terra families + +The provider matrix now supports Sonnet, GPT-6 Astra, GPT-5.6 Luna, and GPT-5.6 Terra alongside Fable, Sol, Grok, and Opus. A First-run active column keeps the existing four-family panel and generated first-run sheet unchanged. Setup derives active membership from the normalized final role map instead of persisting a second setting. Operators can add or remove supported families before probing. Setup collects efforts, probes, and runs its behavioral smoke only for the resulting active set, and it requires that set to equal the families in the final role map. + +Sonnet joins Fable and Opus as a rolling Claude alias. Runtime verification accepts only numeric concrete Sonnet revisions from Claude, stale versioned Sonnet descriptors normalize in memory or fail at the runner boundary, and Claude Code ships all five native Sonnet effort agents. Codex runner coverage now includes explicit Astra, Luna, and Terra argv and receipt cases. The generic runner, receipts, timeout policy, parent-owned routes, and no-fallback behavior are unchanged. + +The static default-panel check filters the matrix to First-run active rows. The four panel skills remain unchanged, and adding supported opt-in families does not expand their default fan-out. This change does not bump the plugin version. + ## 1.3.0 syncs to Cursor pstack 0.14.7 Open Pstack now tracks Cursor pstack 0.14.7 at `efa2a531985e0a8084d36ff3cf87233be8a9f34b`. @@ -197,7 +205,7 @@ pstack diverges from superpowers in one respect, and it is deliberate. superpowe **Verified.** Codex discovers the skills and namespaces them under `pstack` (`pstack:poteto-mode` and so on) in a live session. Mapping resolution mid-task and `spawn_agent` fan-out follow the `superpowers` pattern and are worth confirming per session. -**Maintenance.** The open-pstack version string lives in `plugins/pstack/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `plugins/pstack/.codex-plugin/plugin.json`, and the current-version row in `UPSTREAM.md`. A version bump must update all four. `tests/skill-collision-repro.sh` checks that they match. `.agents/plugins/marketplace.json` carries no version field. The canonical default panel quad is the model matrix in `provider-dispatch.md` (`provider:model@default` in family-row order). It is copied into the four panel skills (`arena`, `architect`, `how`, `interrogate`) and the `setup-pstack` first-run sheet. Keep those copies grep-identical when models change. The static test derives the quad from the matrix. After a sync that touches `skills/poteto-mode/scripts/`, run `bun install --frozen-lockfile`, `bun run test`, and `bun run typecheck` from that directory. `hooks/session-start-context.md` restates skill one-liners. Re-verify it whenever skill names or descriptions change. The package must not contain a `commands/` layer. Claude Code and Codex load the native `skills/` tree directly, and a command layer duplicates that inventory. The 21 `principle-*` leaves carry `user-invocable: false` to request exclusion from the user picker while `poteto-mode` reads them by path. Claude honors the metadata; Codex 0.149.0 currently does not ([#8](https://github.com/ericlitman/open-pstack/issues/8)). They must not carry `disable-model-invocation`, which would make them unreachable to the model. Re-run the behavioral mode of `tests/skill-collision-repro.sh` after Claude Code upgrades to check both model-initiated and user-initiated native skill invocation. +**Maintenance.** The open-pstack version string lives in `plugins/pstack/.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `plugins/pstack/.codex-plugin/plugin.json`, and the current-version row in `UPSTREAM.md`. A version bump must update all four. `tests/skill-collision-repro.sh` checks that they match. `.agents/plugins/marketplace.json` carries no version field. The model matrix in `provider-dispatch.md` owns the supported registry. Its First-run active rows, in family-row order and rendered as `provider:model@default`, are the canonical default panel. That panel is copied into the four panel skills (`arena`, `architect`, `how`, `interrogate`) and the `setup-pstack` first-run sheet. Keep those copies grep-identical when active defaults change. The static test derives the panel by filtering the matrix. After a sync that touches `skills/poteto-mode/scripts/`, run `bun install --frozen-lockfile`, `bun run test`, and `bun run typecheck` from that directory. `hooks/session-start-context.md` restates skill one-liners. Re-verify it whenever skill names or descriptions change. The package must not contain a `commands/` layer. Claude Code and Codex load the native `skills/` tree directly, and a command layer duplicates that inventory. The 21 `principle-*` leaves carry `user-invocable: false` to request exclusion from the user picker while `poteto-mode` reads them by path. Claude honors the metadata; Codex 0.149.0 currently does not ([#8](https://github.com/ericlitman/open-pstack/issues/8)). They must not carry `disable-model-invocation`, which would make them unreachable to the model. Re-run the behavioral mode of `tests/skill-collision-repro.sh` after Claude Code upgrades to check both model-initiated and user-initiated native skill invocation. ## 0.9.2 sync (against upstream `e46364b`) diff --git a/NOTICE.md b/NOTICE.md index 49d832d..55ec68f 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -32,7 +32,7 @@ Summary of structural changes: - `plugins/pstack/skills/babysit/` is independently authored as the Claude Code analog of Cursor's `/babysit` built-in. It has no upstream pstack equivalent; its workflow is informed by Cursor's public `/babysit` behavior. No code or prose was copied from any source. - `plugins/pstack/skills/poteto-mode/scripts/` is vendored from upstream (`watch-pr`, `orch`, `bootstrap.ts`, `worktree-audit.sh`, `package.json`, `bun.lock`) with these port edits: `worktree-audit.sh` reads `~/.claude/projects/` instead of Cursor's transcript directory and warns when `jq` or `rg` is missing (their absence silently blanks the columns the prune decision reads), the private workspace package is named `@open-pstack/poteto-mode-tools`, `bootstrap.ts` rejects Node before it reads Bun-only APIs, and `package.json` includes the port-authored tests in `bun run test`. `check-plan.mjs` is the Cursor 0.14.3 checker adapted for the shared Claude Code and Codex skeleton. `bootstrap.test.ts` and `check-plan.test.ts` are authored for this port. - `plugins/pstack/agents/comment-sicko.md` is upstream's `Comment Sicko` agent, renamed to `comment-sicko` so the name works as a Claude Code `subagent_type`. The body is verbatim. -- Claude-native Fable and Opus lanes are port-authored agent definitions. They select the rolling family alias plus requested effort for every selectable Claude-native pair in the provider-dispatch model matrix. +- Claude-native Fable, Opus, and Sonnet lanes are port-authored agent definitions. They select the rolling family alias plus requested effort for every selectable Claude-native pair in the provider-dispatch model matrix. - A Codex build shares the same `skills/` tree. It adds `plugins/pstack/.codex-plugin/plugin.json`, a root `.agents/plugins/marketplace.json`, and `plugins/pstack/skills/poteto-mode/references/codex-tools.md` (the Claude-to-Codex tool, model, and built-in map), plus a one-line Platform note in the skills that name a Claude primitive. The skill content itself is unchanged. See [CHANGES.md](CHANGES.md#codex-port). ## Modifications @@ -49,7 +49,7 @@ Files authored for this port (not derived from upstream): - `plugins/pstack/skills/poteto-mode/scripts/bootstrap.test.ts` - `plugins/pstack/skills/poteto-mode/scripts/check-plan.test.ts` - `plugins/pstack/skills/babysit/SKILL.md` (independently authored; workflow informed by Cursor's public `/babysit` behavior) -- `plugins/pstack/agents/pstack-fable-*.md` and `plugins/pstack/agents/pstack-opus-*.md` (Claude-native frontier lanes at each selectable effort) +- `plugins/pstack/agents/pstack-fable-*.md`, `plugins/pstack/agents/pstack-opus-*.md`, and `plugins/pstack/agents/pstack-sonnet-*.md` (Claude-native frontier lanes at each selectable effort) - `plugins/pstack/hooks/hooks.json`, `plugins/pstack/hooks/session-start`, and `plugins/pstack/hooks/session-start-context.md` (the auto-fire hook and its mandate) - `NOTICE.md` (this file) - `README.md` diff --git a/README.md b/README.md index fa4a064..dfa86ed 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ pstack does not ask you to trust an agent on day one. It helps the agent leave e ## Install -You need a current Claude Code or Codex installation. For the full four-model review, install and sign in to the Claude Code, Codex, and Grok command-line tools. [Bun](https://bun.sh) runs the small local tool that starts models outside the app you are using. You can still use the core workflows with fewer models. +You need a current Claude Code or Codex installation. For the default four-model review, install and sign in to the Claude Code, Codex, and Grok command-line tools. [Bun](https://bun.sh) runs the small local tool that starts models outside the app you are using. You can still use the core workflows with fewer models. ### Claude Code @@ -80,9 +80,9 @@ In Codex, ask: Use pstack:setup-pstack to configure pstack. ``` -Setup checks the models you can actually run, shows how each one will start, and asks before saving the choices. The current default group uses Fable, GPT-5.6 Sol, Grok 4.6, and Opus. +Setup checks the models you can actually run, shows how each one will start, and asks before saving the choices. It supports Fable, GPT-5.6 Sol, Grok 4.6, Opus, Sonnet, GPT-6 Astra, GPT-5.6 Luna, and GPT-5.6 Terra. The first run still uses only Fable, Sol, Grok, and Opus. You can add or remove supported families before setup probes them. -An older model sheet starts using the rolling aliases in memory as soon as this release is installed. Run setup once after updating to persist that migration. It replaces versioned Fable and Opus entries while preserving every role assignment and effort selection. +A model sheet with versioned Claude-family entries starts using the rolling aliases in memory as soon as this release is installed. Run setup once after updating to persist that migration. It replaces versioned Fable, Opus, and Sonnet entries while preserving every role assignment and effort selection. ### 2. Use poteto-mode @@ -124,7 +124,7 @@ Plugin skills include `pstack:` in their name. In Claude Code, invoke a native s Some pstack workflows use one model. Skills such as `architect`, `arena`, and `interrogate` can run several models in parallel. Each model run uses the subscription and token allowance of its own command-line tool. -`setup-pstack` lets you choose the models, one requested effort per model family, and how many run in parallel. A model from the app you are using runs inside that app. Other models run through their own command-line tools. Open Pstack does not quietly replace a failed model with a weaker one. +`setup-pstack` lets you choose the active model families, one requested effort per active family, and how many run in parallel. It probes and smokes only the families used by the final role map. A model from the app you are using runs inside that app. Other models run through their own command-line tools. Open Pstack does not quietly replace a failed model with a weaker one. ## Claude Code and Codex @@ -133,7 +133,7 @@ Both apps read the same pstack skills. Only the way they start those skills and | | Claude Code | Codex | | --- | --- | --- | | Start poteto-mode | Claude loads a small startup instruction that can route non-trivial work into it. You can also run `/pstack:poteto-mode` yourself. | Ask for `pstack:poteto-mode` by name. Codex does not load the Claude startup instruction. | -| Runs inside the app | Claude models stay inside Claude Code. | The Sol model stays inside Codex. | +| Runs inside the app | Claude models stay inside Claude Code. | Codex models stay inside Codex. | | Other models | Codex and Grok run through their signed-in command-line tools. | Claude and Grok run through their signed-in command-line tools. | | Skills and workflows | Shared with Codex. | Shared with Claude Code. | diff --git a/docs/reference.md b/docs/reference.md index 5e69907..1d8a578 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -63,7 +63,7 @@ The marketplace install is the normal user path. Direct links are only for testi │ │ ├── poteto-mode/references/{codex-tools,provider-dispatch}.md # tool + provider routing │ │ └── poteto-mode/scripts/ # bun/bash/node tooling: watch-pr, orch, runner, check-plan.mjs, worktree-audit.sh │ ├── hooks/ # SessionStart auto-fire: injects the poteto-mode mandate (Claude Code only) -│ └── agents/ # Claude subagents, including native Fable and Opus lanes at each selectable effort +│ └── agents/ # Claude subagents, including native Fable, Opus, and Sonnet lanes at each selectable effort ├── tests/skill-collision-repro.sh # native-skill package invariants and Claude invocation checks ├── LICENSE # pstack upstream MIT ├── LICENSE-cursor-team-kit # cursor-team-kit upstream MIT @@ -83,10 +83,10 @@ The Codex build shares one `skills/` tree with the Claude Code build. Nothing is - **Skill invocation.** Codex loads `SKILL.md` natively. There is no `Skill` tool. You invoke a skill by name (ask for it, or pick `pstack:poteto-mode` from the list). - **Package surface.** The native `skills/` tree is the only workflow source. The plugin ships no `commands/` layer and does not link prompts into `~/.codex/prompts/`. Codex would migrate such files into duplicate source-command skills while loading the native skill tree. The 21 `principle-*` leaves declare `user-invocable: false`. Claude keeps them out of its user picker; Codex 0.149.0 currently shows them despite that metadata ([#8](https://github.com/ericlitman/open-pstack/issues/8)). -- **Tool and built-in mapping.** Claude tool names and built-in skills resolve through [`codex-tools.md`](../plugins/pstack/skills/poteto-mode/references/codex-tools.md). Model execution resolves separately through [`provider-dispatch.md`](../plugins/pstack/skills/poteto-mode/references/provider-dispatch.md), so Codex can keep Sol native while invoking Claude and Grok externally. +- **Tool and built-in mapping.** Claude tool names and built-in skills resolve through [`codex-tools.md`](../plugins/pstack/skills/poteto-mode/references/codex-tools.md). Model execution resolves separately through [`provider-dispatch.md`](../plugins/pstack/skills/poteto-mode/references/provider-dispatch.md), so Codex can keep Codex families native while invoking Claude and Grok externally. - **Subagents.** The `Agent` tool maps to Codex `spawn_agent` / `wait_agent`, enabled by `multi_agent = true`. Parallel fan-out is multiple `spawn_agent` calls in one turn. If the native Codex lane is unavailable, record that lane as a dropout; external Claude and Grok lanes still run, and no provider is silently substituted. There is no `poteto-agent` subagent type on Codex; route ad-hoc subagents by dispatching a `spawn_agent` told to read `poteto-mode` first. - **Auto-fire.** The `hooks/` SessionStart injection is Claude Code-only; Codex has no plugin hook runtime. Enter `pstack:poteto-mode` by name, or add a standing instruction to `~/.codex/AGENTS.md` if you want the same always-on routing. -- **Models.** `/setup-pstack` writes provider-qualified descriptors and asks one requested effort per frontier family (`low`, `medium`, `high`, `xhigh`, `max`). The first-run panel is Fable max, GPT-5.6 Sol max, Grok 4.6 xhigh, and Opus xhigh. Fable and Opus use Claude's rolling aliases. Runtime dispatch normalizes older versioned descriptors in memory, so an installed sheet stops pinning immediately. A setup rerun persists that migration while keeping each role's family and effort. In Codex, Sol uses native `spawn_agent`; Claude and Grok use the deterministic external runner. In Claude Code, Fable and Opus use native agents; Sol and Grok use the runner. Children never detect the parent or reroute themselves. The `bug-fix`, `perf-issue`, and `hillclimb` roles stay on GPT-5.6 Sol max instead of upstream's Fable default because Sol costs less for these frequent delegated code roles. +- **Models.** `/setup-pstack` supports Fable, GPT-5.6 Sol, Grok 4.6, Opus, Sonnet, GPT-6 Astra, GPT-5.6 Luna, and GPT-5.6 Terra. It writes provider-qualified descriptors and asks one requested effort per active family (`low`, `medium`, `high`, `xhigh`, `max`). The role map is the only persisted source of active membership. Setup derives its target set from that map, probes and smokes only that set, and requires every selected family to appear in at least one role. The first-run panel remains Fable max, Sol max, Grok xhigh, and Opus xhigh. Fable, Opus, and Sonnet use Claude's rolling aliases. Runtime dispatch normalizes older versioned descriptors in memory, so an installed sheet stops pinning immediately. A setup rerun persists that migration. If neither normalization nor an operator choice changes the render, setup preserves the sheet and integration bytes exactly. In Codex, Sol, Astra, Luna, and Terra use native `spawn_agent`; Claude and Grok use the deterministic external runner. In Claude Code, Fable, Opus, and Sonnet use native agents; Codex and Grok use the runner. Children never detect the parent or reroute themselves. The `bug-fix`, `perf-issue`, and `hillclimb` roles stay on GPT-5.6 Sol max instead of upstream's Fable default because Sol costs less for these frequent delegated code roles. Verified in fresh installed Claude Code and Codex sessions: the user-facing skills are discovered and namespaced under `pstack`; both parents fan out the frontier quad through the documented native/external route table, retain long-running handles without a default timeout, and cross-judge only after every candidate is terminal. The `principle-*` leaves remain available for `poteto-mode` to read by path. Claude honors their `user-invocable: false` metadata; Codex 0.149.0 does not ([#8](https://github.com/ericlitman/open-pstack/issues/8)). @@ -158,7 +158,7 @@ The table uses the short upstream names. Claude Code exposes each native skill w `comment-sicko` is the read-only comment reviewer the `no-comments` skill spawns. Upstream names it `Comment Sicko`; the port renames it to `comment-sicko` so the name is a valid `subagent_type`. Invoke it through `/no-comments`, not directly. -Fable and Opus each ship at `low`, `medium`, `high`, `xhigh`, and `max`. Names are `pstack--`. `pstack-fable-max` and `pstack-opus-xhigh` remain. Each file selects the rolling family alias and requested effort, runs in the background, and denies nested Agent/Task dispatch. pstack dispatches them from provider-qualified descriptors; they are not user-facing workflows. +Fable, Opus, and Sonnet each ship at `low`, `medium`, `high`, `xhigh`, and `max`. Names are `pstack--`. `pstack-fable-max` and `pstack-opus-xhigh` remain. Each file selects the rolling family alias and requested effort, runs in the background, and denies nested Agent/Task dispatch. pstack dispatches them from provider-qualified descriptors; they are not user-facing workflows. ## Differences from upstream @@ -195,6 +195,8 @@ The port is editorial, not mechanical. Anywhere upstream pstack assumed Cursor-s | Model rule `~/.cursor/rules/pstack-models.mdc` | Override sheet `~/.claude/pstack-models.md`, included from `CLAUDE.md` | | Multi-model panels (arena, architect, interrogate, how-critics) | Provider dispatch restores the upstream frontier quad: `claude:fable@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:opus@xhigh`. Same-provider lanes stay native; external lanes use the bundled runner. | +The model registry also accepts `claude:sonnet`, `codex:gpt-6-astra`, `codex:gpt-5.6-luna`, and `codex:gpt-5.6-terra` at every selectable effort. They are opt-in families and do not change the default panel above. + ### Cross-vendor dispatch The earlier port collapsed panels to Claude-only models. The bundled runner restores upstream's cross-provider judgment signal without adding a daemon or model-router service. Claude Code shells out to Codex and Grok; Codex shells out to Claude and Grok. The top-level parent chooses every route and each external process receives a complete task directly, so there is no supervising model invocation and no child-side harness detection. diff --git a/plugins/pstack/agents/pstack-sonnet-high.md b/plugins/pstack/agents/pstack-sonnet-high.md new file mode 100644 index 0000000..86b918f --- /dev/null +++ b/plugins/pstack/agents/pstack-sonnet-high.md @@ -0,0 +1,12 @@ +--- +name: pstack-sonnet-high +description: Native Claude lane for pstack roles configured as claude:sonnet@high. +model: sonnet +effort: high +background: true +disallowedTools: Agent, Task +--- + +# pstack Sonnet lane + +Execute only the task and path scope the parent assigns. Read the grounding artifacts by path. Do not choose another model, spawn another agent, or start a pstack workflow. If the assignment is read-only, do not modify files. Return the requested artifact or verdict plus a concise rationale. diff --git a/plugins/pstack/agents/pstack-sonnet-low.md b/plugins/pstack/agents/pstack-sonnet-low.md new file mode 100644 index 0000000..5c202b4 --- /dev/null +++ b/plugins/pstack/agents/pstack-sonnet-low.md @@ -0,0 +1,12 @@ +--- +name: pstack-sonnet-low +description: Native Claude lane for pstack roles configured as claude:sonnet@low. +model: sonnet +effort: low +background: true +disallowedTools: Agent, Task +--- + +# pstack Sonnet lane + +Execute only the task and path scope the parent assigns. Read the grounding artifacts by path. Do not choose another model, spawn another agent, or start a pstack workflow. If the assignment is read-only, do not modify files. Return the requested artifact or verdict plus a concise rationale. diff --git a/plugins/pstack/agents/pstack-sonnet-max.md b/plugins/pstack/agents/pstack-sonnet-max.md new file mode 100644 index 0000000..321a7ae --- /dev/null +++ b/plugins/pstack/agents/pstack-sonnet-max.md @@ -0,0 +1,12 @@ +--- +name: pstack-sonnet-max +description: Native Claude lane for pstack roles configured as claude:sonnet@max. +model: sonnet +effort: max +background: true +disallowedTools: Agent, Task +--- + +# pstack Sonnet lane + +Execute only the task and path scope the parent assigns. Read the grounding artifacts by path. Do not choose another model, spawn another agent, or start a pstack workflow. If the assignment is read-only, do not modify files. Return the requested artifact or verdict plus a concise rationale. diff --git a/plugins/pstack/agents/pstack-sonnet-medium.md b/plugins/pstack/agents/pstack-sonnet-medium.md new file mode 100644 index 0000000..f22f051 --- /dev/null +++ b/plugins/pstack/agents/pstack-sonnet-medium.md @@ -0,0 +1,12 @@ +--- +name: pstack-sonnet-medium +description: Native Claude lane for pstack roles configured as claude:sonnet@medium. +model: sonnet +effort: medium +background: true +disallowedTools: Agent, Task +--- + +# pstack Sonnet lane + +Execute only the task and path scope the parent assigns. Read the grounding artifacts by path. Do not choose another model, spawn another agent, or start a pstack workflow. If the assignment is read-only, do not modify files. Return the requested artifact or verdict plus a concise rationale. diff --git a/plugins/pstack/agents/pstack-sonnet-xhigh.md b/plugins/pstack/agents/pstack-sonnet-xhigh.md new file mode 100644 index 0000000..cada3fe --- /dev/null +++ b/plugins/pstack/agents/pstack-sonnet-xhigh.md @@ -0,0 +1,12 @@ +--- +name: pstack-sonnet-xhigh +description: Native Claude lane for pstack roles configured as claude:sonnet@xhigh. +model: sonnet +effort: xhigh +background: true +disallowedTools: Agent, Task +--- + +# pstack Sonnet lane + +Execute only the task and path scope the parent assigns. Read the grounding artifacts by path. Do not choose another model, spawn another agent, or start a pstack workflow. If the assignment is read-only, do not modify files. Return the requested artifact or verdict plus a concise rationale. diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 74ab90d..e3fe7ef 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -8,22 +8,26 @@ pstack model choices are provider-qualified descriptors: ## Model matrix -| Family | Upstream pstack choice | Provider | Model | Default effort | Selectable efforts | Claude-native agent stem | -|---|---|---|---|---|---|---| -| fable | fable | claude | fable | max | low medium high xhigh max | fable | -| sol | gpt-5.6-sol-max | codex | gpt-5.6-sol | max | low medium high xhigh max | - | -| grok | grok-4.6-fast-xhigh | grok | grok-4.6 | xhigh | low medium high xhigh max | - | -| opus | opus | claude | opus | xhigh | low medium high xhigh max | opus | +| Family | Upstream pstack choice | Provider | Model | Default effort | Selectable efforts | Claude-native agent stem | First-run active | +|---|---|---|---|---|---|---|---| +| fable | fable | claude | fable | max | low medium high xhigh max | fable | yes | +| sol | gpt-5.6-sol-max | codex | gpt-5.6-sol | max | low medium high xhigh max | - | yes | +| grok | grok-4.6-fast-xhigh | grok | grok-4.6 | xhigh | low medium high xhigh max | - | yes | +| opus | opus | claude | opus | xhigh | low medium high xhigh max | opus | yes | +| sonnet | - | claude | sonnet | high | low medium high xhigh max | sonnet | no | +| astra | - | codex | gpt-6-astra | high | low medium high xhigh max | - | no | +| luna | - | codex | gpt-5.6-luna | high | low medium high xhigh max | - | no | +| terra | - | codex | gpt-5.6-terra | high | low medium high xhigh max | - | no | -The allowed effort universe is exactly `low`, `medium`, `high`, `xhigh`, `max`. First-run requested efforts are the Default effort cell of each row. A Claude-native agent stem of `-` means the family has no Claude-native agent. Otherwise the shipped agent name is `pstack--`. +The allowed effort universe is exactly `low`, `medium`, `high`, `xhigh`, `max`. A `-` in Upstream pstack choice means the portable build added that family. First run activates only rows whose First-run active cell is `yes`, in matrix order, and uses each active row's Default effort. Later runs derive the active family set from the non-alias descriptors in the normalized final role map. No separate active-family setting exists. A Claude-native agent stem of `-` means the family has no Claude-native agent. Otherwise the shipped agent name is `pstack--`. -`fable` and `opus` are Claude Code's rolling aliases. Claude resolves each alias to the latest available family revision. A runner receipt keeps the requested alias in `model` and the concrete provider-reported revision in `reportedModel`; verification accepts only a numeric `claude-fable-*` or `claude-opus-*` revision from the matching family. +`fable`, `opus`, and `sonnet` are Claude Code's rolling aliases. Claude resolves each alias to the latest available family revision. A runner receipt keeps the requested alias in `model` and the concrete provider-reported revision in `reportedModel`; verification accepts only a numeric `claude-fable-*`, `claude-opus-*`, or `claude-sonnet-*` revision from the matching family. ## Read-time normalization -Normalize configured descriptors before matching them to the matrix or choosing a route. If a provider-qualified Claude model starts with `claude-fable-` or `claude-opus-` and its remaining revision contains only digits and hyphens, replace that model component in memory with `fable` or `opus`. Preserve provider, effort, role, and lane order. Use only the normalized descriptor for native dispatch or runner argv. Never pass the versioned predecessor to Claude. +Normalize configured descriptors before matching them to the matrix or choosing a route. If a provider-qualified Claude model starts with `claude-fable-`, `claude-opus-`, or `claude-sonnet-` and its remaining revision contains only digits and hyphens, replace that model component in memory with `fable`, `opus`, or `sonnet`. Preserve provider, effort, role, and lane order. Use only the normalized descriptor for native dispatch or runner argv. Never pass the versioned predecessor to Claude. -This read-time rule makes an older installed sheet use the latest family revision immediately without writing user files. Once per parent run, report that the persisted sheet is stale and that `/setup-pstack` will rewrite it after its normal probes and confirmation. Unknown versioned Claude models remain invalid. The external runner rejects a missed Fable or Opus version pin instead of silently executing it. +This read-time rule makes an installed sheet with a versioned rolling-family entry use the latest family revision immediately without writing user files. Once per parent run, report that the persisted sheet is stale and that `/setup-pstack` will rewrite it after its normal probes and confirmation. Unknown versioned Claude models remain invalid. The external runner rejects a missed Fable, Opus, or Sonnet version pin instead of silently executing it. `fast` is part of Cursor's Grok selector, not a Grok Build CLI model or effort flag. The portable Grok route pins the current CLI model `grok-4.6`. The first-run Grok effort is `xhigh`. @@ -42,7 +46,7 @@ The top-level harness resolves the route once. A child receives an assigned prov Native dispatch avoids a second CLI startup and its base context. -- Claude Code: match the descriptor's `(provider, model)` to one model-matrix row, then dispatch it through `pstack--` using that row's Claude-native agent stem and the descriptor's effort. Those definitions select the rolling model alias, requested effort, and `background: true`. `pstack-fable-max` and `pstack-opus-xhigh` remain in that set. Pass the complete task, grounding paths, access mode, and unique output location in the `Agent` prompt. Retain the task handle and drain it only after fan-out. +- Claude Code: match the descriptor's `(provider, model)` to one model-matrix row, then dispatch it through `pstack--` using that row's Claude-native agent stem and the descriptor's effort. Those definitions select the rolling model alias, requested effort, and `background: true`. `pstack-fable-max` and `pstack-opus-xhigh` remain in that set, and Sonnet has the same five effort-specific definitions. Pass the complete task, grounding paths, access mode, and unique output location in the `Agent` prompt. Retain the task handle and drain it only after fan-out. - Codex: call `spawn_agent` with the descriptor's model and `reasoning_effort`, the complete task, grounding paths, access mode, and unique output location. Use an isolated worktree for a writer. Codex subagents already run concurrently. Do not send a same-provider descriptor to the external runner. It rejects that call because the native route is cheaper and already available. @@ -90,7 +94,7 @@ Success requires all of these: 1. Exit status `0`. 2. Receipt status `complete`. -3. Either `modelVerified: true` with `modelEvidence: "provider-report"`, or a Codex receipt with `reportedModel: null`, `modelVerified: false`, and `modelEvidence: "pinned-argv"`. For Claude's `fable` and `opus` aliases, the concrete provider report must belong to the requested family. Codex 0.149.0 accepts the exact `--model` argument but does not report the served model in its JSONL stream. +3. Either `modelVerified: true` with `modelEvidence: "provider-report"`, or a Codex receipt with `reportedModel: null`, `modelVerified: false`, and `modelEvidence: "pinned-argv"`. For Claude's `fable`, `opus`, and `sonnet` aliases, the concrete provider report must belong to the requested family. Codex 0.149.0 accepts the exact `--model` argument but does not report the served model in its JSONL stream. 4. A non-empty output file. The receipt also carries elapsed time, token usage when the CLI exposes it, and cost when available. Keep it with the arena or review artifacts so parent-harness comparisons are evidence-based. diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index ea697ff..de46773 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -177,4 +177,42 @@ describe("invocationCommand", () => { } } }); + + it("pins every additional supported family in external argv", () => { + const cases = [ + { + provider: "claude" as const, + model: "sonnet", + flag: ["--model", "sonnet"], + }, + { + provider: "codex" as const, + model: "gpt-6-astra", + flag: ["--model", "gpt-6-astra"], + }, + { + provider: "codex" as const, + model: "gpt-5.6-luna", + flag: ["--model", "gpt-5.6-luna"], + }, + { + provider: "codex" as const, + model: "gpt-5.6-terra", + flag: ["--model", "gpt-5.6-terra"], + }, + ]; + for (const { provider, model, flag } of cases) { + const spec = invocationCommand( + options({ provider, model, effort: "high" }) + ); + const modelIndex = spec.args.indexOf("--model"); + expect(spec.args.slice(modelIndex, modelIndex + 2)).toEqual(flag); + const effortFlag = + provider === "claude" + ? ["--effort", "high"] + : ["--config", 'model_reasoning_effort="high"']; + const effortIndex = spec.args.indexOf(effortFlag[0]); + expect(spec.args.slice(effortIndex, effortIndex + 2)).toEqual(effortFlag); + } + }); }); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts index 9762c70..4a29ef3 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts @@ -1,9 +1,10 @@ -export type RollingClaudeAlias = "fable" | "opus"; +export const ROLLING_CLAUDE_ALIASES = ["fable", "opus", "sonnet"] as const; +export type RollingClaudeAlias = (typeof ROLLING_CLAUDE_ALIASES)[number]; export function isRollingClaudeAlias( model: string ): model is RollingClaudeAlias { - return model === "fable" || model === "opus"; + return ROLLING_CLAUDE_ALIASES.includes(model as RollingClaudeAlias); } export function versionedClaudeAlias( @@ -11,6 +12,7 @@ export function versionedClaudeAlias( ): RollingClaudeAlias | null { if (/^claude-fable-[0-9]+(?:-[0-9]+)*$/.test(model)) return "fable"; if (/^claude-opus-[0-9]+(?:-[0-9]+)*$/.test(model)) return "opus"; + if (/^claude-sonnet-[0-9]+(?:-[0-9]+)*$/.test(model)) return "sonnet"; return null; } diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/model-matrix.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/model-matrix.test.ts index 9f82d0d..51ca502 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/model-matrix.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/model-matrix.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "bun:test"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; +import { ROLLING_CLAUDE_ALIASES } from "./model-aliases.ts"; import { EFFORTS, type Effort } from "./types.ts"; const PLUGIN_ROOT = join(import.meta.dir, "../../../.."); @@ -19,9 +20,19 @@ const MATRIX_HEADER = [ "Default effort", "Selectable efforts", "Claude-native agent stem", + "First-run active", ] as const; -const FAMILY_ORDER = ["fable", "sol", "grok", "opus"] as const; +const FAMILY_ORDER = [ + "fable", + "sol", + "grok", + "opus", + "sonnet", + "astra", + "luna", + "terra", +] as const; const PROVIDERS = ["claude", "codex", "grok"] as const; const DESCRIPTOR_RE = /(claude|codex|grok):[a-z0-9.-]+@(low|medium|high|xhigh|max)/g; @@ -52,11 +63,12 @@ const SHEET_ROLES = [ ] as const; const SETUP_SECTION_ORDER = [ "### 2. Load current state", - "### 3. Parse per-family efforts", - "### 4. Collect one requested effort per family", - "### 5. Probe the four requested pairs", - "### 6. Render, preserving role families", - "### 7. Confirm and commit", + "### 3. Parse the role map and active families", + "### 4. Choose the target active set and role assignments", + "### 5. Collect one requested effort per active family", + "### 6. Probe the target active set", + "### 7. Render the final role map", + "### 8. Confirm and commit", ] as const; interface MatrixRow { @@ -67,6 +79,7 @@ interface MatrixRow { defaultEffort: Effort; selectableEfforts: Effort[]; claudeNativeAgentStem: string | null; + firstRunActive: boolean; } function splitRow(line: string): string[] { @@ -108,9 +121,9 @@ function parseModelMatrix(markdown: string): MatrixRow[] { .slice(start + 1, end) .map((line) => line.trim()) .filter((line) => line.startsWith("|")); - if (table.length !== 6) { + if (table.length !== 10) { throw new Error( - `model matrix must be header, separator, and 4 data rows, got ${table.length}` + `model matrix must be header, separator, and 8 data rows, got ${table.length}` ); } const header = splitRow(table[0]); @@ -133,6 +146,7 @@ function parseModelMatrix(markdown: string): MatrixRow[] { defaultEffortRaw, selectableRaw, stemRaw, + firstRunActiveRaw, ] = cells; if (!(PROVIDERS as readonly string[]).includes(provider)) { throw new Error(`invalid provider: ${provider}`); @@ -149,6 +163,9 @@ function parseModelMatrix(markdown: string): MatrixRow[] { if (!selectableEfforts.includes(defaultEffort)) { throw new Error(`${family} default effort is not selectable`); } + if (firstRunActiveRaw !== "yes" && firstRunActiveRaw !== "no") { + throw new Error(`${family} First-run active must be yes or no`); + } return { family, upstreamChoice, @@ -157,12 +174,13 @@ function parseModelMatrix(markdown: string): MatrixRow[] { defaultEffort, selectableEfforts, claudeNativeAgentStem, + firstRunActive: firstRunActiveRaw === "yes", }; }); } function defaultDescriptors(rows: MatrixRow[]): string[] { - return rows.map( + return rows.filter((row) => row.firstRunActive).map( (row) => `${row.provider}:${row.model}@${row.defaultEffort}` ); } @@ -202,7 +220,7 @@ function firstRunSheet(setup: string): string { describe("model matrix", () => { const rows = parseModelMatrix(readFileSync(DISPATCH_PATH, "utf8")); const setup = readFileSync(SETUP_PATH, "utf8"); - const quad = defaultDescriptors(rows); + const defaultPanel = defaultDescriptors(rows); it("owns the effort universe and first-run defaults", () => { expect([...EFFORTS]).toEqual(["low", "medium", "high", "xhigh", "max"]); @@ -211,26 +229,33 @@ describe("model matrix", () => { expect(row.upstreamChoice.length).toBeGreaterThan(0); expect(row.model.length).toBeGreaterThan(0); expect(row.selectableEfforts.length).toBeGreaterThan(0); - expect(row.selectableEfforts).toEqual( - EFFORTS.filter((effort) => row.selectableEfforts.includes(effort)) - ); + expect(row.selectableEfforts).toEqual([...EFFORTS]); } expect( - rows.map((row) => [row.family, row.defaultEffort]) + rows.map((row) => [ + row.family, + row.provider, + row.model, + row.defaultEffort, + row.claudeNativeAgentStem, + row.firstRunActive, + ]) ).toEqual([ - ["fable", "max"], - ["sol", "max"], - ["grok", "xhigh"], - ["opus", "xhigh"], + ["fable", "claude", "fable", "max", "fable", true], + ["sol", "codex", "gpt-5.6-sol", "max", null, true], + ["grok", "grok", "grok-4.6", "xhigh", null, true], + ["opus", "claude", "opus", "xhigh", "opus", true], + ["sonnet", "claude", "sonnet", "high", "sonnet", false], + ["astra", "codex", "gpt-6-astra", "high", null, false], + ["luna", "codex", "gpt-5.6-luna", "high", null, false], + ["terra", "codex", "gpt-5.6-terra", "high", null, false], ]); expect( - rows - .filter((row) => row.family === "fable" || row.family === "opus") - .map((row) => [row.family, row.model]) - ).toEqual([ - ["fable", "fable"], - ["opus", "opus"], - ]); + rows.filter((row) => row.firstRunActive).map((row) => row.family) + ).toEqual(["fable", "sol", "grok", "opus"]); + expect( + rows.filter((row) => row.provider === "claude").map((row) => row.model) + ).toEqual([...ROLLING_CLAUDE_ALIASES]); }); it("ships exactly the declared Claude-native frontier agents", () => { @@ -297,7 +322,7 @@ describe("model matrix", () => { } expect(effort).toBe(row.defaultEffort); } - const expectedPanel = quad.join(", "); + const expectedPanel = defaultPanel.join(", "); for (const role of PANEL_ROLES) { const line = sheet .split("\n") @@ -319,9 +344,11 @@ describe("model matrix", () => { expect(setup).toContain("Do not invent a precedence rule."); expect(setup).toContain("Do not probe or write while any inconsistency is unresolved."); expect(setup).toContain("A failed probe writes nothing:"); - expect(setup).toContain("Run one probe per family"); + expect(setup).toContain("Run one probe per active family"); expect(setup).toContain("normalized complete role map from step 2"); - expect(setup).toContain("starts with `claude-fable-` or `claude-opus-`"); + expect(setup).toContain( + "starts with `claude-fable-`, `claude-opus-`, or `claude-sonnet-`" + ); expect(setup).toContain("preserving the provider, effort, role, and lane order"); expect(setup).toContain("Show any rolling-alias migrations"); expect(setup).toContain("Every documented role remains present."); @@ -354,6 +381,8 @@ describe("model matrix", () => { expect(normalization).toContain("Never pass the versioned predecessor to Claude."); expect(normalization).toContain("without writing user files"); expect(normalization).toContain("`/setup-pstack` will rewrite it"); - expect(normalization).toContain("runner rejects a missed Fable or Opus version pin"); + expect(normalization).toContain( + "runner rejects a missed Fable, Opus, or Sonnet version pin" + ); }); }); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts index b4ebc04..db792e8 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts @@ -113,7 +113,10 @@ describe("parseProviderOutput", () => { it("matches only concrete Claude revisions from the requested rolling family", () => { expect(reportedModelMatches("claude", "fable", "claude-fable-9-9")).toBe(true); expect(reportedModelMatches("claude", "opus", "claude-opus-9")).toBe(true); + expect(reportedModelMatches("claude", "sonnet", "claude-sonnet-9-9")).toBe(true); expect(reportedModelMatches("claude", "fable", "claude-opus-9")).toBe(false); + expect(reportedModelMatches("claude", "sonnet", "claude-opus-9")).toBe(false); + expect(reportedModelMatches("claude", "sonnet", "claude-sonnet-beta")).toBe(false); expect(reportedModelMatches("claude", "fable", "claude-fable-beta")).toBe(false); expect(reportedModelMatches("claude", "fable", "fable")).toBe(false); expect(reportedModelMatches("claude", "fable", "fable-preview")).toBe(false); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts index 20743b5..27b421b 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts @@ -93,7 +93,9 @@ const reportedModel = model === "fable" ? "claude-fable-9-9" : model === "opus" ? "claude-opus-9" - : model; + : model === "sonnet" + ? "claude-sonnet-9-9" + : model; if (process.env.FAKE_INVALID_MODEL === "1") { console.error("The requested model is not supported with this account."); process.exit(1); @@ -307,6 +309,50 @@ describe("runLane", () => { }); }); + it("receipts every additional supported external family", async () => { + const cases = [ + { + provider: "claude" as const, + model: "sonnet", + reportedModel: "claude-sonnet-9-9", + }, + { + provider: "codex" as const, + model: "gpt-6-astra", + reportedModel: null, + }, + { + provider: "codex" as const, + model: "gpt-5.6-luna", + reportedModel: null, + }, + { + provider: "codex" as const, + model: "gpt-5.6-terra", + reportedModel: null, + }, + ]; + for (const [index, { provider, model, reportedModel }] of cases.entries()) { + const input = { + ...options(provider, `supported-family-${index}`), + model, + effort: "high" as const, + }; + expect((await runLane(input)).exitCode).toBe(0); + expect(receipt(input.receiptPath)).toMatchObject({ + status: "complete", + provider, + model, + effort: "high", + reportedModel, + modelVerified: provider === "claude", + modelEvidence: provider === "claude" + ? "provider-report" + : "pinned-argv", + }); + } + }); + it("classifies an unavailable model without falling back", async () => { process.env.FAKE_INVALID_MODEL = "1"; const input = options("codex"); @@ -896,13 +942,19 @@ describe("runLane", () => { await expect(runLane(input)).rejects.toThrow("native to parent"); }); - it("rejects a versioned Claude family before it can stay pinned", async () => { - const input = { ...options("claude"), model: "claude-fable-9-9" }; - await expect(runLane(input)).rejects.toThrow( - "normalize it to fable before invoking the runner" - ); - expect(existsSync(input.outputPath)).toBe(false); - expect(existsSync(input.receiptPath)).toBe(false); + it("rejects versioned Claude families before they can stay pinned", async () => { + for (const [model, alias] of [ + ["claude-fable-9-9", "fable"], + ["claude-opus-9", "opus"], + ["claude-sonnet-9-9", "sonnet"], + ] as const) { + const input = { ...options("claude", `versioned-${alias}`), model }; + await expect(runLane(input)).rejects.toThrow( + `normalize it to ${alias} before invoking the runner` + ); + expect(existsSync(input.outputPath)).toBe(false); + expect(existsSync(input.receiptPath)).toBe(false); + } }); }); diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 4641881..25ad1b4 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -5,7 +5,7 @@ description: Configure pstack's provider-qualified models, per-family requested # Setup pstack -Configure one portable model sheet for the current parent harness. Read [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md) before probing or writing anything. Its model matrix, descriptor grammar, and route table are the contract. Choose one requested effort per matrix family. Do not add a second configuration file, a runtime resolver, or a weaker-model fallback. +Configure one portable model sheet for the current parent harness. Read [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md) before probing or writing anything. Its model matrix, descriptor grammar, and route table are the contract. Choose one requested effort per active matrix family. Do not add a second configuration file, a runtime resolver, or a weaker-model fallback. Claude Code writes `~/.claude/pstack-models.md` and loads it from `~/.claude/CLAUDE.md` with: @@ -29,59 +29,63 @@ Use the harness and tool surface running this skill: Claude Code or Codex. Envir ### 2. Load current state -Read the current parent-specific sheet when it exists. Before matrix validation, normalize only the rolling-alias predecessors that earlier pstack releases generated. A provider-qualified Claude model is migratable when its model component starts with `claude-fable-` or `claude-opus-` and the remaining revision contains only digits and hyphens. Replace that component in memory with `fable` or `opus`, preserving the provider, effort, role, and lane order. Record each original and normalized descriptor for the confirmation in step 7. This migration is valid loaded state and does not require a separate operator choice. +Read the current parent-specific sheet when it exists. Before matrix validation, normalize only versioned predecessors of the supported rolling aliases. A provider-qualified Claude model is migratable when its model component starts with `claude-fable-`, `claude-opus-`, or `claude-sonnet-` and the remaining revision contains only digits and hyphens. Replace that component in memory with `fable`, `opus`, or `sonnet`, preserving the provider, effort, role, and lane order. Record each original and normalized descriptor for the confirmation in step 8. This migration is valid loaded state and does not require a separate operator choice. -Treat the normalized values as current role-to-family assignments. Overlay those rows on the complete first-run role map in step 7. Materialize any missing documented role row from that map on the next successful write. A duplicate or unknown role row is inconsistent state; report it and resolve it before probing. A bare host-native slug from an older sheet is also invalid because it does not say which provider owns it. A versioned Claude model outside the two migration families remains inconsistent state. If the sheet is missing, use the complete first-run role map and the model matrix's Default effort cells. +Treat the normalized loaded values as current role-to-family assignments. Keep any documented role missing from the sheet as a pending row seeded from the complete first-run role map. A pending row is not current state and does not activate its seeded families. Materialize it only in the next successful write after step 4 assigns every lane to a target family or alias. A duplicate or unknown role row is inconsistent state; report it and resolve it before probing. A bare host-native slug from an older sheet is also invalid because it does not say which provider owns it. A versioned Claude model outside the three migration families remains inconsistent state. If the sheet is missing, use the complete first-run role map. Its non-alias descriptors already equal the matrix rows marked First-run active `yes` at their Default effort. -### 3. Parse per-family efforts +### 3. Parse the role map and active families Read the model matrix. Every non-alias value must match `:@`. Map it to exactly one matrix family by `(provider, model)`, require its effort to appear in that row's Selectable efforts cell, and collect the effort. `inherit-parent` and `auto` rows carry no family effort. -An unmatched provider/model, out-of-domain effort, duplicate role, or unknown role is inconsistent state. Stop, show the conflicting rows verbatim, and ask for an explicit matrix family or alias replacement. If one or more families have mixed efforts, show every conflicting family and role row, then ask for one normalized effort per family from its Selectable efforts cell. Do not invent a precedence rule. Do not probe or write while any inconsistency is unresolved. +An unmatched provider/model, out-of-domain effort, duplicate role, or unknown role is inconsistent state. Stop, show the conflicting rows verbatim, and ask for an explicit matrix family or alias replacement. Do not invent a precedence rule. Do not probe or write while any inconsistency is unresolved. -One distinct effort per family is the current value. A family with no non-alias occurrence is unassigned; use its matrix Default effort as the proposed value and label it unassigned rather than calling it current. +Derive the current active family set from the normalized loaded rows: it is exactly the matrix families with at least one non-alias descriptor. Pending rows do not contribute to this set. Do not read or write a separate active-family value. Record all efforts used by each current active family. A supported family with no loaded non-alias occurrence is inactive. -### 4. Collect one requested effort per family +### 4. Choose the target active set and role assignments -Ask exactly four effort questions, one each for Fable, Sol, Grok, and Opus. Name each model, its current or proposed value, and the Selectable efforts from its matrix row. Empty input keeps a current value or accepts the matrix proposal for an unassigned family. On a first run, state the four matrix defaults before asking. On a rerun, state the four parsed values without offering to reset customized role lanes. +Show the supported matrix families and mark the current active set. On a first run, that set is Fable, Sol, Grok, and Opus because those rows have First-run active `yes`; keep it unchanged by default. On a rerun, keep the derived current set by default. Ask whether to add or remove named supported families. Require at least one target active family. Refuse an alias-only role map because setup could not probe a model family or choose a behavioral-smoke descriptor from it. -### 5. Probe the four requested pairs +Build the final role map in memory before probing. Start from the normalized complete role map from step 2, preserving every loaded row's lane order and family or alias per lane. Present each pending row and its seeded assignments. If a seeded descriptor names a family outside the target set, require the operator to replace that lane with a target family, `inherit-parent`, or `auto`. Ask whether to keep the other assignments or change named roles. Keeping them is the default. Apply only role changes the operator names; never offer a reset of a customized sheet to the first-run assignments. -Probe only the four selected `provider:model@effort` pairs. Run one probe per family, even when two families share a provider. Do not enumerate or offer older models as substitutes. A failed probe writes nothing: report the failing pair and provider, stop, and keep the active sheet plus parent integration bytes unchanged. A failed first run creates neither artifact. +A removed family must have every occurrence replaced with another target family, `inherit-parent`, or `auto`. An added family must appear in at least one role. Other named role changes may use a target family, `inherit-parent`, or `auto`. Refuse an unqualified slug, a model outside the matrix, a provider/model mismatch, or any role map whose derived family set differs from the chosen target set. -| Family | Pair source | Claude parent route | Codex parent route | Availability proof | -|---|---|---|---|---| -| Fable | Fable matrix row + selected effort | native Agent `pstack-fable-` | Claude CLI | native one-turn probe or `claude auth status --json` plus one-turn probe | -| Sol | Sol matrix row + selected effort | `codex exec` | native `spawn_agent` | `codex login status` plus one-turn probe or native one-turn probe | -| Grok | Grok matrix row + selected effort | Grok CLI | Grok CLI | `grok models` must list the requested model; one-turn probe | -| Opus | Opus matrix row + selected effort | native Agent `pstack-opus-` | Claude CLI | native one-turn probe or `claude auth status --json` plus one-turn probe | +### 5. Collect one requested effort per active family -Use a tiny read-only probe that returns a unique marker. A login-status command alone proves credentials, not that the requested model and effort flags run. Record native and external results separately. Never call the external launcher for the parent's own provider. On a Claude parent, the Fable and Opus probes are one-turn runs of the mapped `pstack--` agent. On a Codex parent, the Sol probe is native `spawn_agent` with the selected `reasoning_effort`. Every other pair uses the external runner with the selected effort flag. +Visit the target active families in matrix order and collect one requested effort for each. For a family that currently uses one effort, show it as current. For a newly active family, show its Default effort as proposed. Empty input keeps the current value or accepts that proposal. -Receipts and native transcripts prove the requested effort and the route. They do not prove a provider's hidden applied reasoning depth. There is no implicit timeout, weaker-model fallback, same-provider external fallback, or second mutable configuration source. +If a target family has mixed current efforts, show every conflicting role row and ask for one normalized effort from its Selectable efforts cell. Do not ask for an effort from a removed or inactive family. On a first run with the unchanged target set, state the four matrix defaults before asking. -### 6. Render, preserving role families +### 6. Probe the target active set -Build the new sheet in memory. Do not write it yet. +Probe only the target active `provider:model@effort` pairs. Run one probe per active family, even when families share a provider. Do not probe inactive families. Do not enumerate or offer older models as substitutes. A failed probe writes nothing: report the failing pair and provider, stop, and keep the active sheet plus parent integration bytes unchanged. A failed first run creates neither artifact. -- First run: start from the complete role assignments in step 7. -- Rerun: start from the normalized complete role map from step 2, preserving each loaded row's lane order and family (or alias) per lane. +| Provider | Claude parent route | Codex parent route | Availability proof | +|---|---|---|---| +| Claude | native Agent `pstack--` | Claude CLI | native one-turn probe or `claude auth status --json` plus one-turn probe | +| Codex | `codex exec` | native `spawn_agent` | `codex login status` plus one-turn probe or native one-turn probe | +| Grok | Grok CLI | Grok CLI | `grok models` must list the requested model; one-turn probe | -After effort selection, ask whether to keep those role-to-family assignments or change named roles. Keeping them is the default. Apply only role changes the operator names; never offer a reset of a customized sheet to the first-run assignments. A changed role may use one of the four probed matrix families, `inherit-parent`, or `auto`. +Use a tiny read-only probe that returns a unique marker. A login-status command alone proves credentials, not that the requested model and effort flags run. Record native and external results separately. Never call the external launcher for the parent's own provider. On a Claude parent, active Claude-family probes are one-turn runs of their mapped `pstack--` agents. On a Codex parent, active Codex-family probes are native `spawn_agent` calls with the selected model and `reasoning_effort`. Every other active pair uses the external runner with the selected effort flag. + +Receipts and native transcripts prove the requested effort and the route. They do not prove a provider's hidden applied reasoning depth. There is no implicit timeout, weaker-model fallback, same-provider external fallback, or second mutable configuration source. + +### 7. Render the final role map + +Build the new sheet in memory. Do not write it yet. -Require the final role map to contain at least one descriptor from each matrix family. The sheet stores effort only in role descriptors, so an unassigned family's selection cannot persist without adding a second source of truth. +Use the final role assignments from step 4. Rewrite every target-family descriptor to `provider:model@`. Leave `inherit-parent` and `auto` unchanged. An effort-only rerun cannot change a role's family. Changing Grok's effort updates every Grok occurrence and does not move a Sol role onto Grok. -Rewrite every matrix-family descriptor to `provider:model@`. Leave `inherit-parent` and `auto` unchanged. An effort-only rerun cannot change a role's family. Changing Grok's effort updates every Grok occurrence and does not move a Sol role onto Grok. Refuse an unqualified slug, an unavailable route, a model other than the four matrix families, or a provider/model mismatch. +Derive the rendered active family set again and require it to equal both the chosen target set and the set of probed families. This equality is the persistence rule: the sheet stores active membership and effort only through role descriptors. -### 7. Confirm and commit +### 8. Confirm and commit Show any rolling-alias migrations as original and normalized descriptors. Then show the route table for this parent and every rendered role and descriptor. Ask for confirmation before writing. Why and Reflect require the parent's live MCP surface. Keep their investigator, reviewer, and synthesizer roles on `inherit-parent` or `auto`; the bounded external runner deliberately omits ambient MCPs. `inherit-parent` and `auto` always validate, but say when they reduce a panel's provider diversity. For panel roles, one lane runs per entry. The list length is the fan-out count. `arena cross-judge pool` is a list from which Arena chooses a provider different from the parent and base candidate when possible. `swarm workers` is the default for every worker unless a race explicitly assigns another descriptor. -Every non-alias value must match `:@` and must have passed step 5. +Every non-alias value must match `:@` and its family must have passed step 6. -After the operator confirms, write the in-memory render from step 6. Never paste the example below as the result. It is only the complete first-run role map used to seed step 2; selected efforts and explicit role changes always replace its example values before writing. +After the operator confirms, write the in-memory render from step 7. Never paste the example below as the result. It is only the complete first-run role map used to seed step 2; selected efforts and explicit role changes always replace its example values before writing. ```markdown # pstack model configuration @@ -106,16 +110,16 @@ architect runners: claude:fable@max, codex:gpt-5.6-sol@max, grok:grok-4.6@xhigh, interrogate reviewers: claude:fable@max, codex:gpt-5.6-sol@max, grok:grok-4.6@xhigh, claude:opus@xhigh ``` -### 8. Wire it in +### 9. Wire it in Render the parent integration in memory before either write. On Claude, the integration is the single `@~/.claude/pstack-models.md` include in `~/.claude/CLAUDE.md`. On Codex, it is the exact sheet bytes between one `` and `` pair in `~/.codex/AGENTS.md`. Replace that whole bounded block on a rerun. Insert one block at the end on first run. If either marker is missing, duplicated, or reversed, stop and report inconsistent state instead of guessing a boundary. -Snapshot every target's current bytes. Write the sheet and parent integration only after all four probes pass and the operator confirms. Read both targets back and compare them with the in-memory render. If either write or readback fails, restore every snapshot and report the failure. An unchanged rerun must produce byte-identical sheet and integration content after normalization. +Snapshot every target's current bytes. Write the sheet and parent integration only after every target-active probe passes and the operator confirms. Read both targets back and compare them with the in-memory render. If either write or readback fails, restore every snapshot and report the failure. An unchanged rerun must produce byte-identical sheet and integration content after normalization. Do not copy the model sheet between harnesses without rerunning the parent-specific probes; route availability can differ even on the same host. -### 9. Behavioral smoke +### 10. Behavioral smoke -Before declaring setup complete, run one small read-only mixed panel from this parent: all four chosen descriptors, distinct output/receipt paths, and an independent cross-judge. Launch Claude-native agents and every external process in the background with retained handles, then drain them. Verify the native transcript entries and every external receipt. A structural config check or unit test is not a substitute. +Before declaring setup complete, run one small read-only mixed panel from this parent: one chosen descriptor for every target active family, distinct output/receipt paths, and an independent cross-judge chosen from that same set. Do not smoke an inactive family. Launch native agents and every external process in the background with retained handles, then drain them. Verify the native transcript entries and every external receipt. A structural config check or unit test is not a substitute. Report the sheet path, parent route table, requested-effort probe results, smoke results, and external elapsed/token/cost receipts. Re-running this skill re-probes and updates the same sheet. Do not claim the provider exposed hidden applied-effort observability. diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index 6b66608..1ee23ac 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -51,7 +51,7 @@ fi legacy_model_pins="$( grep -REn \ --include='*.md' --include='*.ts' --include='*.sh' \ - 'claude:claude-(fable|opus)-[0-9]|^model: claude-(fable|opus)-[0-9]|--model claude-(fable|opus)-[0-9]' \ + 'claude:claude-[a-z0-9-]+-[0-9]|^model: claude-[a-z0-9-]+-[0-9]|--model claude-[a-z0-9-]+-[0-9]' \ "$repo/plugins/pstack" "$repo/tests" "$repo/README.md" "$repo/docs/reference.md" \ 2>/dev/null || true )" @@ -59,25 +59,23 @@ standalone_code_pins="$( grep -REn \ --include='*.ts' --include='*.js' \ --exclude='*.test.ts' --exclude='*.test.js' \ - "['\"]claude-(fable|opus)-[0-9]" \ + "['\"]claude-[a-z0-9-]+-[0-9]" \ "$repo/plugins/pstack" \ 2>/dev/null || true )" if [ -n "$legacy_model_pins" ] || [ -n "$standalone_code_pins" ]; then - note "FAIL: active Fable or Opus configuration still pins a model revision:" + note "FAIL: active Fable, Opus, or Sonnet configuration still pins a model revision:" [ -z "$legacy_model_pins" ] || note "$legacy_model_pins" [ -z "$standalone_code_pins" ] || note "$standalone_code_pins" fail=1 else - note "ok: active Fable and Opus configuration uses rolling aliases" + note "ok: active Fable, Opus, and Sonnet configuration uses rolling aliases" fi -# Static invariant (CHANGES maintenance note): provider-dispatch owns the default -# provider/model quad and the four panel skills plus setup-pstack copy it verbatim. setup="$repo/plugins/pstack/skills/setup-pstack/SKILL.md" dispatch="$repo/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md" -quad_of() { { grep -oE '(claude|codex|grok):[a-z0-9.-]+@(low|medium|high|xhigh|max)' || true; } | tr '\n' ' ' | sed 's/ $//'; } -canon_quad="$(awk ' +descriptor_list_of() { { grep -oE '(claude|codex|grok):[a-z0-9.-]+@(low|medium|high|xhigh|max)' || true; } | tr '\n' ' ' | sed 's/ $//'; } +default_panel="$(awk ' $0 == "## Model matrix" { in_matrix = 1; next } in_matrix && /^## / { exit } in_matrix && /^\|/ { @@ -91,6 +89,8 @@ canon_quad="$(awk ' } family = cells[1] if (family == "Family" || family ~ /^:?-+:?$/) next + first_run_active = cells[8] + if (first_run_active != "yes") next provider = cells[3] model = cells[4] effort = cells[5] @@ -99,36 +99,32 @@ canon_quad="$(awk ' } END { print out } ' "$dispatch")" -quad_bad="" -[ -n "$canon_quad" ] || quad_bad="could not read the canonical quad from $dispatch"$'\n' -# Anchor on the quad's last slug rather than a hard-coded one, so a model swap in -# setup-pstack cannot leave this check hunting for a slug nobody ships any more. -anchor="${canon_quad##* }" -# arena, architect, and how each state the quad on one line; interrogate lists it -# as one slug per row of its Reviewer A/B/C/D table (upstream #167). +panel_mismatches="" +[ -n "$default_panel" ] || panel_mismatches="could not read the default panel from $dispatch"$'\n' +last_default_descriptor="${default_panel##* }" for name in arena architect how; do skill="$repo/plugins/pstack/skills/$name/SKILL.md" - n="$(grep -Fc "$anchor" "$skill" || true)" + n="$(grep -Fc "$last_default_descriptor" "$skill" || true)" if [ "$n" != "1" ]; then - quad_bad="$quad_bad$skill: expected exactly 1 default-quad line, found $n"$'\n' + panel_mismatches="$panel_mismatches$skill: expected exactly 1 default-panel line, found $n"$'\n' continue fi - got="$(grep -F "$anchor" "$skill" | quad_of)" - [ "$got" = "$canon_quad" ] || quad_bad="$quad_bad$skill: [$got] != [$canon_quad]"$'\n' + got="$(grep -F "$last_default_descriptor" "$skill" | descriptor_list_of)" + [ "$got" = "$default_panel" ] || panel_mismatches="$panel_mismatches$skill: [$got] != [$default_panel]"$'\n' done interrogate="$repo/plugins/pstack/skills/interrogate/SKILL.md" -got="$(grep -E '^\| Reviewer [A-Z] \|' "$interrogate" | quad_of)" -[ "$got" = "$canon_quad" ] || quad_bad="$quad_bad$interrogate reviewer table: [$got] != [$canon_quad]"$'\n' +got="$(grep -E '^\| Reviewer [A-Z] \|' "$interrogate" | descriptor_list_of)" +[ "$got" = "$default_panel" ] || panel_mismatches="$panel_mismatches$interrogate reviewer table: [$got] != [$default_panel]"$'\n' while IFS= read -r line; do - got="$(printf '%s\n' "$line" | quad_of)" - [ "$got" = "$canon_quad" ] || quad_bad="$quad_bad$setup role row: [$got] != [$canon_quad]"$'\n' + got="$(printf '%s\n' "$line" | descriptor_list_of)" + [ "$got" = "$default_panel" ] || panel_mismatches="$panel_mismatches$setup role row: [$got] != [$default_panel]"$'\n' done < <(grep -E '^(arena runners|arena cross-judge pool|architect runners|interrogate reviewers|how critics):' "$setup") -if [ -n "$quad_bad" ]; then - note "FAIL: the default model quad is not identical across provider dispatch, the panel skills, and setup-pstack:" - note "$quad_bad" +if [ -n "$panel_mismatches" ]; then + note "FAIL: the default model panel is not identical across provider dispatch, the panel skills, and setup-pstack:" + note "$panel_mismatches" fail=1 else - note "ok: default model quad identical across provider dispatch + 4 panel skills + setup-pstack ($canon_quad)" + note "ok: default model panel identical across provider dispatch + 4 panel skills + setup-pstack ($default_panel)" fi plugin="$repo/plugins/pstack"