From 76e993fd3168b9ede524408a0fa1c7e11cf782ad Mon Sep 17 00:00:00 2001 From: "Eric A. Litman" Date: Tue, 1 Sep 2026 16:13:09 -0400 Subject: [PATCH 1/2] fix(pstack): use rolling Claude model aliases --- .claude-plugin/marketplace.json | 2 +- CHANGES.md | 6 +++++ NOTICE.md | 2 +- README.md | 6 +++-- UPSTREAM.md | 4 +-- docs/reference.md | 10 +++---- plugins/pstack/.claude-plugin/plugin.json | 2 +- plugins/pstack/.codex-plugin/plugin.json | 2 +- plugins/pstack/agents/pstack-fable-high.md | 4 +-- plugins/pstack/agents/pstack-fable-low.md | 4 +-- plugins/pstack/agents/pstack-fable-max.md | 4 +-- plugins/pstack/agents/pstack-fable-medium.md | 4 +-- plugins/pstack/agents/pstack-fable-xhigh.md | 4 +-- plugins/pstack/agents/pstack-opus-high.md | 4 +-- plugins/pstack/agents/pstack-opus-low.md | 4 +-- plugins/pstack/agents/pstack-opus-max.md | 4 +-- plugins/pstack/agents/pstack-opus-medium.md | 4 +-- plugins/pstack/agents/pstack-opus-xhigh.md | 4 +-- plugins/pstack/skills/architect/SKILL.md | 2 +- plugins/pstack/skills/arena/SKILL.md | 2 +- plugins/pstack/skills/how/SKILL.md | 6 ++--- plugins/pstack/skills/interrogate/SKILL.md | 4 +-- plugins/pstack/skills/poteto-mode/SKILL.md | 4 +-- .../poteto-mode/references/codex-tools.md | 2 +- .../references/provider-dispatch.md | 16 ++++++++--- .../scripts/runner/commands.test.ts | 10 +++---- .../scripts/runner/model-aliases.ts | 22 +++++++++++++++ .../scripts/runner/model-matrix.test.ts | 25 +++++++++++++++++ .../scripts/runner/parse-output.test.ts | 26 ++++++++++++------ .../scripts/runner/parse-output.ts | 27 +++++++++++++++---- .../poteto-mode/scripts/runner/run.test.ts | 21 +++++++++++++-- .../skills/poteto-mode/scripts/runner/run.ts | 11 +++++++- plugins/pstack/skills/setup-pstack/SKILL.md | 22 ++++++++------- tests/skill-collision-repro.sh | 20 +++++++++++++- 34 files changed, 215 insertions(+), 79 deletions(-) create mode 100644 plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index a210942..0bf6ae8 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ "name": "pstack", "source": "./plugins/pstack", "description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence.", - "version": "1.2.0", + "version": "1.2.1", "author": { "name": "Lauren Tan (original)" }, diff --git a/CHANGES.md b/CHANGES.md index 8953a58..37ec978 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,12 @@ 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. +## 1.2.1 keeps Fable and Opus on their latest Claude revisions + +Open Pstack now stores `claude:fable@` and `claude:opus@` in its model matrix, role defaults, and generated setup sheets. Claude Code resolves those aliases to the latest family revision. Native Claude agents and external runner calls pass the same aliases. + +Runtime dispatch normalizes the provider-qualified versioned Fable and Opus descriptors written by earlier releases before it chooses a route, so an installed sheet stops pinning as soon as the plugin updates. It does not write user files and reports that setup should persist the migration. `setup-pstack` applies the same rule before validation, preserves roles, lane order, and effort selections, and writes only after the existing probes and confirmation. The runner rejects any version pin that reaches its boundary, records both the requested alias and Claude's concrete reported revision, and verifies that the report belongs to the requested family. Static checks reject new active version pins. + ## 1.2.0 adds verified multi-PR plans, earlier runtime diagnostics, and shared review-bot triage Plans with several stages now use one checklist instead of an overview and separate files for each stage. It has one ordered section for every pull request and keeps all ten ways of testing the real product, unit tests, live and performance proof, checks for how changes work together, merge rules, and supporting details in one place. A Node-based checker with no extra dependencies rejects missing or out-of-order sections, fake screenshots, empty definitions of success, incomplete performance proof, incorrectly written review checks, unsupported punctuation, and incorrect command use. Claude Code and Codex use the same installed skill and checker through their existing parent-controlled setup. If a provider fails, it is identified by name and treated as a dropout. No backup provider or hidden time limit was added. diff --git a/NOTICE.md b/NOTICE.md index 0c4500a..eb7d24c 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -31,7 +31,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 pin model plus requested effort for every selectable Claude-native pair in the provider-dispatch model matrix. +- 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. - 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 diff --git a/README.md b/README.md index 315d51b..b5ae57a 100644 --- a/README.md +++ b/README.md @@ -80,7 +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 5, GPT-5.6 Sol, Grok 4.6, and Opus 5. +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. + +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. ### 2. Use poteto-mode @@ -151,7 +153,7 @@ This repository also keeps: ## Staying close to Lauren's pstack -Open Pstack 1.2.0 tracks pstack 0.14.3 at Cursor commit [`bdf7aa355337897f167153e05069aca505dae17c`](https://github.com/cursor/plugins/commit/bdf7aa355337897f167153e05069aca505dae17c). +Open Pstack 1.2.1 tracks pstack 0.14.3 at Cursor commit [`bdf7aa355337897f167153e05069aca505dae17c`](https://github.com/cursor/plugins/commit/bdf7aa355337897f167153e05069aca505dae17c). The two projects have separate version numbers. The pstack version identifies Lauren's upstream content. The Open Pstack version identifies the Claude Code and Codex package built from it. diff --git a/UPSTREAM.md b/UPSTREAM.md index c29e210..610bf0b 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -10,9 +10,9 @@ open-pstack tracks [Cursor's pstack](https://github.com/cursor/plugins/tree/main | Path | `pstack/` | | Commit | `bdf7aa355337897f167153e05069aca505dae17c` | | Upstream version | `0.14.3` | -| open-pstack version | `1.2.0` | +| open-pstack version | `1.2.1` | -The table above is the current Cursor sync point. Open Pstack 1.2.0 consolidates this 0.14.3 sync. `README-UPSTREAM.md` preserves its pstack README verbatim. `CHANGES.md` and `NOTICE.md` describe the adaptations and provenance. +The table above is the current Cursor sync point. Open Pstack 1.2.1 keeps this 0.14.3 sync. `README-UPSTREAM.md` preserves its pstack README verbatim. `CHANGES.md` and `NOTICE.md` describe the adaptations and provenance. ## Check for changes diff --git a/docs/reference.md b/docs/reference.md index e0089d5..09e687f 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -2,7 +2,7 @@ This page contains the full skill, dependency, runtime, and porting reference. For the plain-English introduction and quick start, see the [main README](../README.md). -[Poteto](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack), adapted to run in Claude Code and Codex without Cursor. One shared skill tree serves both harnesses; Grok remains available as a model-provider lane. Version 1.2.0 is synced to Cursor pstack v0.14.3 at `bdf7aa355337897f167153e05069aca505dae17c`. See [UPSTREAM.md](../UPSTREAM.md) for the exact sync contract. +[Poteto](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack), adapted to run in Claude Code and Codex without Cursor. One shared skill tree serves both harnesses; Grok remains available as a model-provider lane. Version 1.2.1 is synced to Cursor pstack v0.14.3 at `bdf7aa355337897f167153e05069aca505dae17c`. See [UPSTREAM.md](../UPSTREAM.md) for the exact sync contract. Original by Lauren Tan. This distribution builds on Michael Denyer's [pstack-claude](https://github.com/michael-denyer/pstack-claude) port and retains its history and MIT attribution. It imports seven MIT-licensed skills from [cursor-team-kit](https://github.com/cursor/plugins/tree/main/cursor-team-kit): `deslop`, `thermo-nuclear-code-quality-review`, `make-pr-easy-to-review`, `fix-ci`, `fix-merge-conflicts`, `get-pr-comments`, `what-did-i-get-done`. @@ -86,7 +86,7 @@ The Codex build shares one `skills/` tree with the Claude Code build. Nothing is - **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. - **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 5 max, GPT-5.6 Sol max, Grok 4.6 xhigh, and Opus 5 xhigh. A rerun keeps each role's family and rewrites that family's 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. +- **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. 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)). @@ -159,7 +159,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 pins model and 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 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. ## Differences from upstream @@ -180,7 +180,7 @@ The port is editorial, not mechanical. Anywhere upstream pstack assumed Cursor-s | Upstream (Cursor) | This port (Claude Code) | | --- | --- | -| `Task` tool, `subagent_type: generalPurpose`, `readonly: false/true` | `Agent` tool with model/effort pins and `disallowedTools`; access mode is assigned by the parent, with writers isolated in worktrees | +| `Task` tool, `subagent_type: generalPurpose`, `readonly: false/true` | `Agent` tool with model selection, requested effort, and `disallowedTools`; access mode is assigned by the parent, with writers isolated in worktrees | | `AskQuestion` tool | `AskUserQuestion` tool | | Cursor's built-in `/loop` | Claude Code's built-in `loop` skill | | Cursor's built-in `/babysit` | `babysit` skill bundled in this plugin. From v0.14.0 upstream routes PR-status requests inside poteto-mode to `playbooks/babysit.md` instead; the port does the same, and `/babysit` stays the standalone entry point | @@ -194,7 +194,7 @@ The port is editorial, not mechanical. Anywhere upstream pstack assumed Cursor-s | Cursor's `/goal` (standing objective across turns) | The program objective written into the run's standing orders and restated in the todolist | | The Cursor agent store (path in the system prompt) | `~/.claude/orchestrate//`, which survives the session restarts a multi-day program expects | | 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:claude-fable-5@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:claude-opus-5@xhigh`. Same-provider lanes stay native; external lanes use the bundled runner. | +| 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. | ### Cross-vendor dispatch diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index 0a8b01f..a11025a 100644 --- a/plugins/pstack/.claude-plugin/plugin.json +++ b/plugins/pstack/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "pstack", "displayName": "pstack", - "version": "1.2.0", + "version": "1.2.1", "description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Ported from cursor/plugins/pstack for Claude Code and Codex.", "author": { "name": "Lauren Tan" diff --git a/plugins/pstack/.codex-plugin/plugin.json b/plugins/pstack/.codex-plugin/plugin.json index 54207b8..c664412 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "1.2.0", + "version": "1.2.1", "description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence. Codex port of the Claude Code plugin; skills are shared, tool names resolve via skills/poteto-mode/references/codex-tools.md.", "author": { "name": "Lauren Tan" diff --git a/plugins/pstack/agents/pstack-fable-high.md b/plugins/pstack/agents/pstack-fable-high.md index bed0e14..d3a2091 100644 --- a/plugins/pstack/agents/pstack-fable-high.md +++ b/plugins/pstack/agents/pstack-fable-high.md @@ -1,7 +1,7 @@ --- name: pstack-fable-high -description: Native Claude lane for pstack roles configured as claude:claude-fable-5@high. -model: claude-fable-5 +description: Native Claude lane for pstack roles configured as claude:fable@high. +model: fable effort: high background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-fable-low.md b/plugins/pstack/agents/pstack-fable-low.md index 7bb4d62..53fd559 100644 --- a/plugins/pstack/agents/pstack-fable-low.md +++ b/plugins/pstack/agents/pstack-fable-low.md @@ -1,7 +1,7 @@ --- name: pstack-fable-low -description: Native Claude lane for pstack roles configured as claude:claude-fable-5@low. -model: claude-fable-5 +description: Native Claude lane for pstack roles configured as claude:fable@low. +model: fable effort: low background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-fable-max.md b/plugins/pstack/agents/pstack-fable-max.md index be91b73..ae1d66f 100644 --- a/plugins/pstack/agents/pstack-fable-max.md +++ b/plugins/pstack/agents/pstack-fable-max.md @@ -1,7 +1,7 @@ --- name: pstack-fable-max -description: Native Claude lane for pstack roles configured as claude:claude-fable-5@max. -model: claude-fable-5 +description: Native Claude lane for pstack roles configured as claude:fable@max. +model: fable effort: max background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-fable-medium.md b/plugins/pstack/agents/pstack-fable-medium.md index 0a38cdc..a626d8c 100644 --- a/plugins/pstack/agents/pstack-fable-medium.md +++ b/plugins/pstack/agents/pstack-fable-medium.md @@ -1,7 +1,7 @@ --- name: pstack-fable-medium -description: Native Claude lane for pstack roles configured as claude:claude-fable-5@medium. -model: claude-fable-5 +description: Native Claude lane for pstack roles configured as claude:fable@medium. +model: fable effort: medium background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-fable-xhigh.md b/plugins/pstack/agents/pstack-fable-xhigh.md index 4cfa977..eb0070e 100644 --- a/plugins/pstack/agents/pstack-fable-xhigh.md +++ b/plugins/pstack/agents/pstack-fable-xhigh.md @@ -1,7 +1,7 @@ --- name: pstack-fable-xhigh -description: Native Claude lane for pstack roles configured as claude:claude-fable-5@xhigh. -model: claude-fable-5 +description: Native Claude lane for pstack roles configured as claude:fable@xhigh. +model: fable effort: xhigh background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-opus-high.md b/plugins/pstack/agents/pstack-opus-high.md index a0208f5..1b8b534 100644 --- a/plugins/pstack/agents/pstack-opus-high.md +++ b/plugins/pstack/agents/pstack-opus-high.md @@ -1,7 +1,7 @@ --- name: pstack-opus-high -description: Native Claude lane for pstack roles configured as claude:claude-opus-5@high. -model: claude-opus-5 +description: Native Claude lane for pstack roles configured as claude:opus@high. +model: opus effort: high background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-opus-low.md b/plugins/pstack/agents/pstack-opus-low.md index 9270346..09f0d4b 100644 --- a/plugins/pstack/agents/pstack-opus-low.md +++ b/plugins/pstack/agents/pstack-opus-low.md @@ -1,7 +1,7 @@ --- name: pstack-opus-low -description: Native Claude lane for pstack roles configured as claude:claude-opus-5@low. -model: claude-opus-5 +description: Native Claude lane for pstack roles configured as claude:opus@low. +model: opus effort: low background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-opus-max.md b/plugins/pstack/agents/pstack-opus-max.md index b3d0200..e1b3b4f 100644 --- a/plugins/pstack/agents/pstack-opus-max.md +++ b/plugins/pstack/agents/pstack-opus-max.md @@ -1,7 +1,7 @@ --- name: pstack-opus-max -description: Native Claude lane for pstack roles configured as claude:claude-opus-5@max. -model: claude-opus-5 +description: Native Claude lane for pstack roles configured as claude:opus@max. +model: opus effort: max background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-opus-medium.md b/plugins/pstack/agents/pstack-opus-medium.md index 38c5e6c..3e5a19d 100644 --- a/plugins/pstack/agents/pstack-opus-medium.md +++ b/plugins/pstack/agents/pstack-opus-medium.md @@ -1,7 +1,7 @@ --- name: pstack-opus-medium -description: Native Claude lane for pstack roles configured as claude:claude-opus-5@medium. -model: claude-opus-5 +description: Native Claude lane for pstack roles configured as claude:opus@medium. +model: opus effort: medium background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/agents/pstack-opus-xhigh.md b/plugins/pstack/agents/pstack-opus-xhigh.md index 9acff75..0e0bf18 100644 --- a/plugins/pstack/agents/pstack-opus-xhigh.md +++ b/plugins/pstack/agents/pstack-opus-xhigh.md @@ -1,7 +1,7 @@ --- name: pstack-opus-xhigh -description: Native Claude lane for pstack roles configured as claude:claude-opus-5@xhigh. -model: claude-opus-5 +description: Native Claude lane for pstack roles configured as claude:opus@xhigh. +model: opus effort: xhigh background: true disallowedTools: Agent, Task diff --git a/plugins/pstack/skills/architect/SKILL.md b/plugins/pstack/skills/architect/SKILL.md index 9938ee8..e8ac3d1 100644 --- a/plugins/pstack/skills/architect/SKILL.md +++ b/plugins/pstack/skills/architect/SKILL.md @@ -31,7 +31,7 @@ Skip Phase A only when the work is genuinely greenfield with no surrounding syst Run the **arena** skill with the design-sketch task and the Phase A grounding artifacts. Pass `references/runner-prompt.md` as each runner's prompt. Each candidate produces a design package shaped per `references/rationale-template.md`: the caller's usage written first, then the type sketch, function signatures, module map, and prose rationale derived from it. -Use your configured architect runners (defaults `claude:claude-fable-5@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:claude-opus-5@xhigh`). +Use your configured architect runners (defaults `claude:fable@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:opus@xhigh`). Design it twice. Require at least two structurally distinct candidates before synthesis, even when the first looks sufficient. This is the **exhaust-the-design-space** principle skill made concrete. Whole-shape alternatives, not point fixes inside one shape. diff --git a/plugins/pstack/skills/arena/SKILL.md b/plugins/pstack/skills/arena/SKILL.md index c8b8853..7174936 100644 --- a/plugins/pstack/skills/arena/SKILL.md +++ b/plugins/pstack/skills/arena/SKILL.md @@ -26,7 +26,7 @@ The N candidates will receive the same prompt, so the prompt is the contract. Ge 1. State the artifact each candidate is producing. 2. Derive the rubric. State what success looks like for *this* task, then turn it into 3-6 concrete gradeable criteria. Concrete: `Adds a --dry-run flag that skips writes`. Vague: `code is correct`. The rubric is the picker's tool in Phase D; candidates only see the task. -3. Pick the runners. Use `arena runners` from the current harness's pstack model sheet when present. Otherwise default to `claude:claude-fable-5@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:claude-opus-5@xhigh`. Spawn more when the arena covers multiple design directions. Same descriptor N times when the work is generation-bound rather than judgment-sensitive. +3. Pick the runners. Use `arena runners` from the current harness's pstack model sheet when present. Otherwise default to `claude:fable@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:opus@xhigh`. Spawn more when the arena covers multiple design directions. Same descriptor N times when the work is generation-bound rather than judgment-sensitive. 4. Assign output paths. Each candidate writes to its own location (a git worktree where possible, otherwise `/tmp/arena-/candidate-/`). N candidates writing to the same path is shared mutable state and fails the the **separate-before-serializing-shared-state** principle skill test. ## Phase B: Fan out diff --git a/plugins/pstack/skills/how/SKILL.md b/plugins/pstack/skills/how/SKILL.md index b4e9bc5..f6ac8cd 100644 --- a/plugins/pstack/skills/how/SKILL.md +++ b/plugins/pstack/skills/how/SKILL.md @@ -59,7 +59,7 @@ Then proceed to Step 3. ### Step 2b. Direct Explain (simple questions) -Dispatch one read-only lane that explores and explains in one pass using your configured how-explainer descriptor (default `claude:claude-fable-5@max`). +Dispatch one read-only lane that explores and explains in one pass using your configured how-explainer descriptor (default `claude:fable@max`). The agent does its own exploration (Glob, Grep, Read) and writes the explanation directly. Read `references/explainer-prompt.md` for the communication style and output format. Same structure, just no explorer findings as input. @@ -67,7 +67,7 @@ Proceed to Step 4. ### Step 3. Synthesize (complex questions only) -Once all explorers return, dispatch one read-only lane to synthesize their findings into one coherent explanation using your configured how-explainer descriptor (default `claude:claude-fable-5@max`). +Once all explorers return, dispatch one read-only lane to synthesize their findings into one coherent explanation using your configured how-explainer descriptor (default `claude:fable@max`). The explainer gets all explorers' findings and writes the human-facing explanation (output format below). Read `references/explainer-prompt.md` for the full prompt template. The explainer reconciles overlapping findings, resolves contradictions, and weaves the slices into a unified picture. @@ -99,7 +99,7 @@ Run the full explain flow above (Steps 1-4). You must understand the architectur ### Step 2. Spawn Critics -After the explanation is complete, start one architectural critic per descriptor in your configured how-critics list (defaults `claude:claude-fable-5@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:claude-opus-5@xhigh`) in one fan-out phase. +After the explanation is complete, start one architectural critic per descriptor in your configured how-critics list (defaults `claude:fable@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:opus@xhigh`) in one fan-out phase. Route each critic descriptor in `read-only` mode. These are minimum reasoning levels. The lead may raise effort within the same current-frontier model when the architecture warrants it, but must not substitute providers silently. diff --git a/plugins/pstack/skills/interrogate/SKILL.md b/plugins/pstack/skills/interrogate/SKILL.md index 00abba8..85d9b09 100644 --- a/plugins/pstack/skills/interrogate/SKILL.md +++ b/plugins/pstack/skills/interrogate/SKILL.md @@ -38,10 +38,10 @@ Start all reviewers in one fan-out phase. Use `interrogate reviewers` from the c | Subagent | Default model | |----------|---------------| -| Reviewer A | `claude:claude-fable-5@max` | +| Reviewer A | `claude:fable@max` | | Reviewer B | `codex:gpt-5.6-sol@max` | | Reviewer C | `grok:grok-4.6@xhigh` | -| Reviewer D | `claude:claude-opus-5@xhigh` | +| Reviewer D | `claude:opus@xhigh` | For each reviewer, route the configured descriptor with `read-only` access and a unique output/receipt path. If the descriptor is `inherit-parent` or `auto`, use the parent subagent primitive without a model override. If a provider, login, or model is unavailable, record a dropout and continue with the completed reviewers. Never pick the closest model or silently fall back; that destroys the meaning of cross-provider agreement. diff --git a/plugins/pstack/skills/poteto-mode/SKILL.md b/plugins/pstack/skills/poteto-mode/SKILL.md index e6a15bf..a72e5f9 100644 --- a/plugins/pstack/skills/poteto-mode/SKILL.md +++ b/plugins/pstack/skills/poteto-mode/SKILL.md @@ -85,9 +85,9 @@ Read the leaf skill in full for any principle you apply. Each entry names when i ## Subagents -For `inherit-parent`, `auto`, or an unconfigured native ad-hoc helper, prefer `poteto-agent`. `/poteto-mode` and `poteto-agent` route through the same wrapper. A provider-qualified role instead follows provider dispatch: Claude's shipped frontier agent definitions pin native model and effort, Codex passes both to `spawn_agent`, and external providers run through the deterministic launcher. Routed workflow skills set the task and access mode; do not override their choices. +For `inherit-parent`, `auto`, or an unconfigured native ad-hoc helper, prefer `poteto-agent`. `/poteto-mode` and `poteto-agent` route through the same wrapper. A provider-qualified role instead follows provider dispatch: Claude's shipped frontier agent definitions select the model alias and requested effort, Codex passes both to `spawn_agent`, and external providers run through the deterministic launcher. Routed workflow skills set the task and access mode; do not override their choices. -**Defaults for every delegation.** Start independent lanes together, use file pointers rather than inlined dumps, preserve only the tools or MCPs the task needs, and assign every writer a worktree or unique output directory. `/setup-pstack` configures the descriptor per role. Upstream defaults use Grok 4.6 xhigh for feature/refactoring, exploration, and swarm work; GPT-5.6 Sol max for bug fixes, performance work, hillclimbing, and tooling review; Fable 5 max for judgment, prose, explanation, synthesis, and hardest tasks; and the four-provider frontier panel for model-diverse judgment. The panel defaults are enumerated in `arena`, `architect`, `interrogate`, and `how`. `inherit-parent` and `auto` use the parent model natively and reduce provider diversity when used in a panel. +**Defaults for every delegation.** Start independent lanes together, use file pointers rather than inlined dumps, preserve only the tools or MCPs the task needs, and assign every writer a worktree or unique output directory. `/setup-pstack` configures the descriptor per role. Upstream defaults use Grok 4.6 xhigh for feature/refactoring, exploration, and swarm work; GPT-5.6 Sol max for bug fixes, performance work, hillclimbing, and tooling review; Fable max for judgment, prose, explanation, synthesis, and hardest tasks; and the four-provider frontier panel for model-diverse judgment. The panel defaults are enumerated in `arena`, `architect`, `interrogate`, and `how`. `inherit-parent` and `auto` use the parent model natively and reduce provider diversity when used in a panel. You own every subagent's work. Review the diff and write your own summary, don't pass through what it said. Interrupt-chained resumes silently drop directives, so fire a fresh subagent with consolidated scope rather than trusting a "done" summary. A second opinion is the same prompt against a different model. Agreement is high-signal. diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 8bed083..5a444d2 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -41,7 +41,7 @@ poteto-mode's Subagents section sets Claude-specific defaults (`subagent_type: " ## Models and providers -Do not replace every configured entry with a Codex model. `/setup-pstack` writes portable descriptors such as `claude:claude-fable-5@max`, `codex:gpt-5.6-sol@max`, and `grok:grok-4.6@xhigh`. In a Codex parent, only `codex:*` is native. Route Claude and Grok descriptors through the external launcher exactly as `provider-dispatch.md` specifies. The current default panel intentionally keeps four-provider frontier diversity and contains no older GPT or Claude substitute. +Do not replace every configured entry with a Codex model. `/setup-pstack` writes portable descriptors such as `claude:fable@max`, `codex:gpt-5.6-sol@max`, and `grok:grok-4.6@xhigh`. In a Codex parent, only `codex:*` is native. Route Claude and Grok descriptors through the external launcher exactly as `provider-dispatch.md` specifies. The current default panel intentionally keeps four-provider frontier diversity and contains no older GPT or Claude substitute. ## Claude built-in skills pstack references diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 9b24d38..74ab90d 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -10,13 +10,21 @@ pstack model choices are provider-qualified descriptors: | Family | Upstream pstack choice | Provider | Model | Default effort | Selectable efforts | Claude-native agent stem | |---|---|---|---|---|---|---| -| fable | claude-fable-5-thinking-max | claude | claude-fable-5 | max | low medium high xhigh max | fable | +| 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 | claude-opus-5-thinking-xhigh | claude | claude-opus-5 | xhigh | low medium high xhigh max | opus | +| opus | opus | claude | opus | xhigh | low medium high xhigh max | opus | 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--`. +`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. + +## 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. + +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. + `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`. ## The parent owns the route @@ -34,7 +42,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 pin model, 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. 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. @@ -82,7 +90,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"`. 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` 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. 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 dbb597f..ea697ff 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -49,12 +49,12 @@ describe("invocationCommand", () => { expect(spec.args).not.toContain("danger-full-access"); }); - it("pins Claude model, effort, permissions, and no-recursion controls", () => { + it("passes Claude model, effort, permissions, and no-recursion controls", () => { const spec = invocationCommand( options({ parent: "codex", provider: "claude", - model: "claude-fable-5", + model: "fable", }) ); expect(spec.command).toBe("claude"); @@ -62,7 +62,7 @@ describe("invocationCommand", () => { expect(spec.args).toEqual([ "-p", "--model", - "claude-fable-5", + "fable", "--effort", "max", "--permission-mode", @@ -134,7 +134,7 @@ describe("invocationCommand", () => { expect(grok.args).not.toContain("--always-approve"); const claude = invocationCommand( - options({ provider: "claude", model: "claude-fable-5", mode: "isolated-write" }) + options({ provider: "claude", model: "fable", mode: "isolated-write" }) ); expect(claude.args).toEqual( expect.arrayContaining([ @@ -150,7 +150,7 @@ describe("invocationCommand", () => { const cases = [ { provider: "claude" as const, - model: "claude-fable-5", + model: "fable", flag: (effort: "low" | "medium" | "high") => ["--effort", effort], }, { diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts new file mode 100644 index 0000000..9762c70 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/model-aliases.ts @@ -0,0 +1,22 @@ +export type RollingClaudeAlias = "fable" | "opus"; + +export function isRollingClaudeAlias( + model: string +): model is RollingClaudeAlias { + return model === "fable" || model === "opus"; +} + +export function versionedClaudeAlias( + model: string +): RollingClaudeAlias | null { + if (/^claude-fable-[0-9]+(?:-[0-9]+)*$/.test(model)) return "fable"; + if (/^claude-opus-[0-9]+(?:-[0-9]+)*$/.test(model)) return "opus"; + return null; +} + +export function concreteModelMatchesRollingAlias( + requested: string, + reported: string +): boolean { + return versionedClaudeAlias(reported) === requested; +} 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 16a48c0..9f82d0d 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 @@ -223,6 +223,14 @@ describe("model matrix", () => { ["grok", "xhigh"], ["opus", "xhigh"], ]); + expect( + rows + .filter((row) => row.family === "fable" || row.family === "opus") + .map((row) => [row.family, row.model]) + ).toEqual([ + ["fable", "fable"], + ["opus", "opus"], + ]); }); it("ships exactly the declared Claude-native frontier agents", () => { @@ -313,6 +321,9 @@ describe("model matrix", () => { expect(setup).toContain("A failed probe writes nothing:"); expect(setup).toContain("Run one probe per family"); expect(setup).toContain("normalized complete role map from step 2"); + expect(setup).toContain("starts with `claude-fable-` or `claude-opus-`"); + 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."); expect(setup).toContain("An effort-only rerun cannot change a role's family."); expect(setup).toContain(""); @@ -331,4 +342,18 @@ describe("model matrix", () => { ); expect(nativeLanes).toContain("`pstack--`"); }); + + it("normalizes old rolling-family pins before any runtime route", () => { + const dispatch = readFileSync(DISPATCH_PATH, "utf8"); + const normalizationStart = dispatch.indexOf("## Read-time normalization"); + const parentStart = dispatch.indexOf("## The parent owns the route"); + expect(normalizationStart).toBeGreaterThan(-1); + expect(parentStart).toBeGreaterThan(normalizationStart); + const normalization = dispatch.slice(normalizationStart, parentStart); + expect(normalization).toContain("replace that model component in memory"); + 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"); + }); }); 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 924a255..b4ebc04 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 @@ -10,14 +10,14 @@ describe("parseProviderOutput", () => { session_id: "claude-session", usage: { input_tokens: 10, output_tokens: 3 }, total_cost_usd: 0.05, - modelUsage: { "claude-fable-5": { inputTokens: 10 } }, + modelUsage: { "claude-fable-9-9": { inputTokens: 10 } }, }), "", - "claude-fable-5" + "fable" ); expect(parsed).toMatchObject({ text: "CLAUDE_OK", - reportedModel: "claude-fable-5", + reportedModel: "claude-fable-9-9", sessionId: "claude-session", usage: { inputTokens: 10, outputTokens: 3 }, costUsd: 0.05, @@ -89,7 +89,7 @@ describe("parseProviderOutput", () => { ); expect(parsed.text).toBe("GROK_OK"); expect(parsed.reportedModel).toBe("grok-4.6-build"); - expect(reportedModelMatches("grok-4.6", parsed.reportedModel)).toBe( + expect(reportedModelMatches("grok", "grok-4.6", parsed.reportedModel)).toBe( true ); }); @@ -101,18 +101,28 @@ describe("parseProviderOutput", () => { result: "CLAUDE_OK", modelUsage: { "claude-haiku-4-5-20251001": {}, - "claude-fable-5": {}, + "claude-fable-9-9": {}, }, }), "", - "claude-fable-5" + "fable" ); - expect(parsed.reportedModel).toBe("claude-fable-5"); + expect(parsed.reportedModel).toBe("claude-fable-9-9"); + }); + + 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", "fable", "claude-opus-9")).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); + expect(reportedModelMatches("grok", "fable", "claude-fable-9-9")).toBe(false); }); it("rejects malformed or textless responses", () => { expect(() => - parseProviderOutput("claude", "not-json", "", "claude-fable-5") + parseProviderOutput("claude", "not-json", "", "fable") ).toThrow("valid JSON"); expect(() => parseProviderOutput( diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts index 9990a82..81ed53d 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts @@ -3,6 +3,10 @@ import type { ParsedOutput, Provider, } from "./types.ts"; +import { + concreteModelMatchesRollingAlias, + isRollingClaudeAlias, +} from "./model-aliases.ts"; type JsonObject = Record; @@ -42,11 +46,17 @@ function normalizedUsage(value: unknown): NormalizedUsage | null { : null; } -function modelFromUsage(value: unknown, requestedModel: string): string | null { +function modelFromUsage( + value: unknown, + provider: Provider, + requestedModel: string +): string | null { const usage = object(value); if (usage === null) return null; const models = Object.keys(usage); - return models.find((model) => reportedModelMatches(requestedModel, model)) + return models.find((model) => + reportedModelMatches(provider, requestedModel, model) + ) ?? models[0] ?? null; } @@ -67,7 +77,7 @@ function parseClaude(stdout: string, requestedModel: string): ParsedOutput { return { text, - reportedModel: modelFromUsage(value.modelUsage, requestedModel), + reportedModel: modelFromUsage(value.modelUsage, "claude", requestedModel), sessionId: nullableString(value.session_id ?? value.sessionId), usage: normalizedUsage(value.usage), costUsd: finiteNumber(value.total_cost_usd) ?? null, @@ -97,7 +107,7 @@ function parseGrok(stdout: string, requestedModel: string): ParsedOutput { return { text, - reportedModel: modelFromUsage(result.modelUsage, requestedModel), + reportedModel: modelFromUsage(result.modelUsage, "grok", requestedModel), sessionId: nullableString(result.session_id), usage: normalizedUsage(result.usage), costUsd: finiteNumber(result.total_cost_usd) ?? null, @@ -164,9 +174,16 @@ export function parseProviderOutput( } export function reportedModelMatches( + provider: Provider, requested: string, reported: string | null ): boolean { if (reported === null) return false; - return reported === requested || reported.startsWith(`${requested}-`); + if (provider === "claude" && isRollingClaudeAlias(requested)) { + return concreteModelMatchesRollingAlias(requested, reported); + } + if (reported === requested || reported.startsWith(`${requested}-`)) { + return true; + } + return 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 deac398..20743b5 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts @@ -89,6 +89,11 @@ if (name === "grok" && args[0] === "models") { } const modelIndex = args.findIndex((value) => value === "--model"); const model = modelIndex >= 0 ? args[modelIndex + 1] : "unknown"; +const reportedModel = model === "fable" + ? "claude-fable-9-9" + : model === "opus" + ? "claude-opus-9" + : model; if (process.env.FAKE_INVALID_MODEL === "1") { console.error("The requested model is not supported with this account."); process.exit(1); @@ -110,7 +115,7 @@ if (stage === "model" && process.env.FAKE_SELF_SIGNAL) { await Bun.sleep(5_000); } if (name === "claude") { - console.log(JSON.stringify({result:"CLAUDE_OK",session_id:"c1",usage:{input_tokens:10,output_tokens:2},total_cost_usd:0.01,modelUsage:{[model]:{}}})); + console.log(JSON.stringify({result:"CLAUDE_OK",session_id:"c1",usage:{input_tokens:10,output_tokens:2},total_cost_usd:0.01,modelUsage:{[reportedModel]:{}}})); } else if (name === "codex") { console.log(JSON.stringify({type:"thread.started",thread_id:"o1"})); console.log(JSON.stringify({type:"item.completed",item:{type:"agent_message",text:"CODEX_OK"}})); @@ -134,7 +139,7 @@ function options(provider: Provider, suffix: string = provider): RunnerOptions { const parent = provider === "codex" ? "claude" : "codex"; const model = provider === "claude" - ? "claude-fable-5" + ? "fable" : provider === "codex" ? "gpt-5.6-sol" : "grok-4.6"; @@ -283,6 +288,9 @@ describe("runLane", () => { modelEvidence: provider === "codex" ? "pinned-argv" : "provider-report", preflight: { status: "passed" }, }); + if (provider === "claude") { + expect(receipt(input.receiptPath).reportedModel).toBe("claude-fable-9-9"); + } }); } @@ -887,6 +895,15 @@ describe("runLane", () => { const input = { ...options("claude"), parent: "claude" as const }; 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); + }); }); describe("childEnvironment", () => { diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts index d1f72d8..054564a 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts @@ -10,6 +10,7 @@ import { } from "node:fs"; import { dirname, resolve } from "node:path"; import { invocationCommand, preflightCommand, type CommandSpec } from "./commands.ts"; +import { versionedClaudeAlias } from "./model-aliases.ts"; import { parseProviderOutput, reportedModelMatches } from "./parse-output.ts"; import type { Provider, @@ -442,7 +443,7 @@ function modelProof( readonly modelVerified: boolean; readonly modelEvidence: "provider-report" | "pinned-argv" | null; } { - if (reportedModelMatches(requested, reported)) { + if (reportedModelMatches(provider, requested, reported)) { return { reportedModel: reported, modelVerified: true, @@ -488,6 +489,14 @@ export function validateOptions(options: RunnerOptions): void { ); } if (options.model.trim().length === 0) throw new UsageError("model must not be empty"); + const staleAlias = options.provider === "claude" + ? versionedClaudeAlias(options.model) + : null; + if (staleAlias !== null) { + throw new UsageError( + `Claude model ${options.model} is a version pin; normalize it to ${staleAlias} before invoking the runner` + ); + } if ( options.timeoutMs !== null && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 1a4c04b..4641881 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -29,7 +29,9 @@ 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. Treat its values as current role-to-family assignments. Overlay its 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. If the sheet is missing, use the complete first-run role map and the model matrix's Default effort cells. +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. + +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. ### 3. Parse per-family efforts @@ -73,7 +75,7 @@ Rewrite every matrix-family descriptor to `provider:model@/dev/null || true +)" +if [ -n "$legacy_model_pins" ]; then + note "FAIL: active Fable or Opus configuration still pins a model revision:" + note "$legacy_model_pins" + fail=1 +else + note "ok: active Fable and Opus 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" @@ -170,7 +188,7 @@ Then stop. Do not invoke any skill or tool. EOF run() { - claude -p --plugin-dir "$scratch" --model claude-fable-5 --effort max --max-turns 3 "$1" < /dev/null 2>&1 + claude -p --plugin-dir "$scratch" --model fable --effort max --max-turns 3 "$1" < /dev/null 2>&1 } check() { # $1 label, $2 expected marker, $3 output From 91dfdec041db540bb7a655eb62a82d09effa6e7a Mon Sep 17 00:00:00 2001 From: "Eric A. Litman" Date: Tue, 1 Sep 2026 16:34:49 -0400 Subject: [PATCH 2/2] test(pstack): catch standalone Claude model pins --- tests/skill-collision-repro.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index 1017ada..57605a3 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -55,9 +55,18 @@ legacy_model_pins="$( "$repo/plugins/pstack" "$repo/tests" "$repo/README.md" "$repo/docs/reference.md" \ 2>/dev/null || true )" -if [ -n "$legacy_model_pins" ]; then +standalone_code_pins="$( + grep -REn \ + --include='*.ts' --include='*.js' \ + --exclude='*.test.ts' --exclude='*.test.js' \ + "['\"]claude-(fable|opus)-[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 "$legacy_model_pins" + [ -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"