diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 1f5a1a5..d4a473d 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -6,6 +6,7 @@ "plugins": [ { "name": "pstack", + "version": "1.4.0", "source": { "source": "local", "path": "./plugins/pstack" diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 935bb12..5b2014f 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.3.0", + "version": "1.4.0", "author": { "name": "Lauren Tan (original)" }, diff --git a/CHANGES.md b/CHANGES.md index ca4c99e..80ec3c0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,37 @@ 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.4.0 adds opt-in Conductor orchestration + +**Opt-in Conductor orchestration.** A repository can now select a strict +Conductor route with `.conductor/poteto-mode.json`. The Claude or Codex session +that invokes Poteto Mode remains the fixed coordinator. Every delegated Claude, +Codex, or Cursor worker receives an isolated Conductor workspace and branch. +Cursor provides the Grok lane through its exact Conductor agent and model pair. + +The new local helper validates the project policy and live model catalog, +persists fixed run budgets and retry-safe message IDs, parses untrusted MCP +responses, verifies post-create and post-run session receipts, accepts one +attempt-matched assistant result, and emits run-scoped cleanup targets. It does +not make network calls or read credentials. Missing identity, catalog entries, +response fields, or receipts fail the lane without a native, portable-runner, +effort, agent, or model fallback. + +Setup detects Conductor projects before portable model-sheet configuration. +It validates `whoami`, the coordinator session status, and `list_models`, then +runs one isolated marker smoke while leaving both user model sheets untouched. +Poteto workers refuse recursive invocation before loading the skill. Arena, +Architect, How, Interrogate, Reflect, and Swarm follow the dispatch route chosen +by the parent instead of bypassing it. + +The Conductor unit, strict type, and static contract suites cover policy, +state transitions, uncertain create reconciliation, exact receipts, transcript +trust, CLI lifecycle, cleanup safety, and worker refusal. Published-contract +fixtures are deliberately labeled as non-live. Release is gated on running the +exact candidate from real Claude and Codex Conductor coordinator sessions. + +**Claude poteto-agent preload.** The Claude Code agent definition preloads `pstack:poteto-mode` through its `skills` frontmatter. Claude subagents start with isolated context, so the upstream prompt alone exposed the skill name but not its body. The shared skill tree and the Codex path are unchanged. The static invariants preserve the binding, and the behavioral check proves that the packaged agent can read a named principle without invoking `Skill` or reading a file. + ## 1.3.0 syncs to Cursor pstack 0.14.7 Open Pstack now tracks Cursor pstack 0.14.7 at `efa2a531985e0a8084d36ff3cf87233be8a9f34b`. @@ -197,7 +228,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`, `.agents/plugins/marketplace.json`, `plugins/pstack/.codex-plugin/plugin.json`, and the current-version row in `UPSTREAM.md`. A version bump must update all five. `tests/skill-collision-repro.sh` checks that they match. 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. ## 0.9.2 sync (against upstream `e46364b`) diff --git a/NOTICE.md b/NOTICE.md index 49d832d..5876ea6 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -32,6 +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. +- `plugins/pstack/agents/poteto-agent.md` keeps the upstream prompt and adds Claude Code `skills` frontmatter so the isolated subagent preloads `pstack:poteto-mode`. - 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). diff --git a/UPSTREAM.md b/UPSTREAM.md index 9c94deb..dfbe918 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 | `efa2a531985e0a8084d36ff3cf87233be8a9f34b` | | Upstream version | `0.14.7` | -| open-pstack version | `1.3.0` | +| open-pstack version | `1.4.0` | -The table above is the current Cursor sync point. Open Pstack 1.3.0 keeps this 0.14.7 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.4.0 keeps this 0.14.7 sync. `README-UPSTREAM.md` preserves its pstack README verbatim. `CHANGES.md` and `NOTICE.md` describe the adaptations and provenance. ## Upstream-only exclusions diff --git a/docs/reference.md b/docs/reference.md index 5e69907..154d4c8 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -60,8 +60,8 @@ The marketplace install is the normal user path. Direct links are only for testi │ ├── .claude-plugin/plugin.json # Claude Code manifest │ ├── .codex-plugin/plugin.json # Codex manifest (skills: ./skills/) │ ├── skills/ # 52 skills shared by Claude Code and Codex -│ │ ├── 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 +│ │ ├── poteto-mode/references/{codex-tools,provider-dispatch,conductor-dispatch}.md +│ │ └── poteto-mode/scripts/ # bun/bash/node tooling, including the Conductor boundary helper │ ├── 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 ├── tests/skill-collision-repro.sh # native-skill package invariants and Claude invocation checks @@ -79,7 +79,7 @@ Plugin-internal `skills//` path references in the docs below are relative ## Running on Codex -The Codex build shares one `skills/` tree with the Claude Code build. Nothing is forked or generated. Two narrow references keep runtime translation separate: `codex-tools.md` maps harness primitives and `provider-dispatch.md` maps model providers. pstack otherwise keeps the upstream Claude-native prose and adds a one-line Platform note to each skill that names a Claude primitive, so the port stays in lockstep with upstream sync. +The Codex build shares one `skills/` tree with the Claude Code build. Nothing is forked or generated. `codex-tools.md` maps harness primitives. Projects without Conductor mode use `provider-dispatch.md` for model-provider routing. Conductor projects use `conductor-dispatch.md` instead. pstack otherwise keeps the upstream Claude-native prose and adds a one-line Platform note to each skill that names a Claude primitive, so the port stays in lockstep with upstream sync. - **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)). @@ -90,6 +90,46 @@ The Codex build shares one `skills/` tree with the Claude Code build. Nothing is 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)). +## Conductor project mode + +Conductor mode is opt-in per repository. It activates only when the nearest +`.conductor/poteto-mode.json` passes the strict version-one parser and declares +`"mode": "conductor"`. Projects without that policy keep the portable routing +described above. + +The invoking Claude or Codex session remains the coordinator. It binds the +run to `CONDUCTOR_SESSION_ID`, validates the authenticated workspace through +`whoami` and `get_session_status`, and resolves every role from committed +project policy. It does not elect another coordinator or use a personal model +sheet. + +Every delegated worker gets a separate Conductor workspace and branch. This +includes read-only exploration and review. Claude, Codex, and Cursor are agent +choices at the Conductor boundary, so a project can route Grok through Cursor +without installing a local Grok launcher. Each worker receives a complete +prompt plus `PSTACK_WORKER=1`, which prevents recursive Poteto dispatch. + +The `scripts/conductor/pstack-conductor` executable is a local policy and state +boundary. It parses saved MCP responses, fixes run ceilings, records stable +message IDs, validates post-create and post-run model receipts, accepts one +attempt-matched assistant result, and emits cleanup targets. It does not call +Conductor over HTTP or read authentication data. The coordinator performs the +MCP calls documented in +[`conductor-dispatch.md`](../plugins/pstack/skills/poteto-mode/references/conductor-dispatch.md). + +Setup calls the hosted [Conductor MCP +server](https://www.conductor.build/docs/api/mcp), checks the live catalog, and +runs one isolated marker smoke. It leaves `~/.claude/pstack-models.md` and +`~/.codex/pstack-models.md` unchanged. Missing authentication, unavailable +models, receipt mismatches, unknown workspace creation, or malformed results +fail closed. There is no native-agent, portable-runner, lower-effort, or model +fallback. + +The implementation ships contract fixtures derived from Conductor's published +API shapes. They are not live evidence. A release remains blocked until the +exact candidate completes the acceptance matrix from real Claude and Codex +Conductor coordinator sessions. + ## Dependencies Nothing is declared in `plugin.json`. Install the one companion plugin yourself: @@ -110,6 +150,7 @@ Not declared as deps, but referenced in skill bodies: - **`bun`** — runs the vendored `skills/poteto-mode/scripts/` tooling (`watch-pr`, `orch`, `runner`). Install via [`brew install oven-sh/bun/bun`](https://bun.sh). `bootstrap.ts` installs dependencies for `watch-pr` and `orch`; the runner uses only Bun and Node built-ins, so it launches directly without an install/re-exec layer. - **`node`** — runs `skills/poteto-mode/scripts/check-plan.mjs`. The checker uses only Node built-ins and does not need Bun. - **Claude Code, Codex, and Grok Build CLIs** — the external runner uses the assigned subscribed CLI directly. Install and authenticate only the providers present in your model sheet. Same-provider work stays native; the runner refuses it. +- **Conductor MCP server.** Required only for repositories that enable Conductor mode. Connect `https://api.conductor.build/mcp` through OAuth or the harness's supported bearer-token environment. The local helper never receives that credential. - **`jq` and `rg` (ripgrep)** — only for `scripts/worktree-audit.sh` (the Worktree cleanup playbook). Without them the audit still runs but blanks its PR and LAST_CHAT columns, so it warns on stderr rather than returning a table that looks complete. No third-party plugins. The harsher-critique escape hatch lives in the bundled `thermo-nuclear-code-quality-review` skill (imported from cursor-team-kit), not in an external plugin. @@ -154,7 +195,7 @@ The table uses the short upstream names. Claude Code exposes each native skill w ## Subagents -`poteto-agent` ships unchanged. Spawn from a parent with `subagent_type: "poteto-agent"`. +`poteto-agent` keeps the upstream prompt and adds Claude Code `skills` frontmatter. The field preloads `pstack:poteto-mode` because Claude subagents do not inherit the parent's loaded skills. Spawn the agent from a parent with `subagent_type: "poteto-agent"`. `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. diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index 69bd391..3ae7468 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.3.0", + "version": "1.4.0", "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 3951103..632c126 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "1.3.0", + "version": "1.4.0", "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/poteto-agent.md b/plugins/pstack/agents/poteto-agent.md index 838a0cd..8d3ced2 100644 --- a/plugins/pstack/agents/poteto-agent.md +++ b/plugins/pstack/agents/poteto-agent.md @@ -1,8 +1,12 @@ --- name: poteto-agent description: Routing target for `/poteto-mode` and any request for poteto's style. Resume an existing `poteto-agent` for the conversation rather than spawning a sibling. Reads the `poteto-mode` skill's `SKILL.md` in full before any work, including its inline Principles index. Substituting `general-purpose` skips that read and drifts. +skills: + - pstack:poteto-mode --- # Poteto subagent +If `PSTACK_WORKER=1`, stop before reading the skill or doing any work. A Conductor Poteto worker cannot invoke Poteto Mode or dispatch another worker. + You are operating as poteto-mode's full agent style. Read the `poteto-mode` skill's `SKILL.md` in full before doing any work, including its inline Principles index. Navigate to a leaf `principle-*` skill whenever you apply that principle. diff --git a/plugins/pstack/skills/architect/SKILL.md b/plugins/pstack/skills/architect/SKILL.md index e8ac3d1..b34eecd 100644 --- a/plugins/pstack/skills/architect/SKILL.md +++ b/plugins/pstack/skills/architect/SKILL.md @@ -7,7 +7,7 @@ description: "Sketch types, signatures, and module structure before code, then s Design before implementing. Sketch types, function signatures, class shapes, and module boundaries with `not implemented` bodies and pseudocode. Synthesize across multiple model perspectives, then fill in code against the chosen sketch. If implementation proves the sketch wrong, throw it out and redesign. -**Dispatch contract.** Architect's Arena uses the centralized provider routing in [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md). Configured entries are descriptors, not native model slugs. On Codex, resolve remaining Claude tool names via [`codex-tools.md`](../poteto-mode/references/codex-tools.md). +**Dispatch contract.** Architect's Arena follows the parent-selected Poteto dispatch reference. Conductor mode resolves `architect-runners` from project policy and isolates every sketch in its own workspace. Provider descriptors and native tool mappings apply only to the portable route. ## Start @@ -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:fable@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, `claude:opus@xhigh`). +Use the configured architect runners. Conductor mode reads `architect-runners` from project policy. The portable defaults are `claude:fable@max`, `codex:gpt-5.6-sol@max`, `grok:grok-4.6@xhigh`, and `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 7174936..f4196e6 100644 --- a/plugins/pstack/skills/arena/SKILL.md +++ b/plugins/pstack/skills/arena/SKILL.md @@ -7,7 +7,7 @@ description: "Spawn N parallel candidates at the same task, pick a base, graft t Fan out N parallel attempts at the same task. Read every candidate end to end. Pick the strongest as the base. Graft the best ideas from the others into it. Verify the synthesized result. -**Dispatch contract.** Read [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md) before fan-out. Configured values are provider-qualified descriptors, not host-native model slugs. The parent starts native and external lanes directly; children never route themselves. On Codex, resolve the remaining Claude tool names via [`codex-tools.md`](../poteto-mode/references/codex-tools.md). +**Dispatch contract.** Follow the parent-selected Poteto dispatch reference. In Conductor mode, resolve `arena-runners` and `arena-cross-judge-pool` from project policy and give every candidate and judge an isolated workspace. The provider descriptors, model sheet, native agents, and launcher below apply only to the portable route. Children never route themselves. ## Start @@ -26,12 +26,12 @@ 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: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. +3. Pick the runners. In Conductor mode, use the `arena-runners` policy route. On the portable route, 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. Use the same lane 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 -Start all N lanes in one fan-out phase through the provider-dispatch contract. Native lanes are background subagents. External lanes are direct background launcher processes with retained task/session handles, never foreground calls and never subagents supervising subprocesses. Give every lane the task, the path to the shared grounding, its own output path, and instructions to produce both the artifact and a short rationale. +Start all N lanes in one fan-out phase through the selected dispatch contract. In Conductor mode, each lane is a distinct persisted workspace attempt. On the portable route, native lanes are background subagents and external lanes are background launcher processes with retained handles. Give every lane the task, the path to the shared grounding, its own output path, and instructions to produce both the artifact and a short rationale. The rationale is mandatory. Without it, the parent cannot tell whether a candidate's structure is principled or accidental, which makes Phase E grafting unreliable. Each rationale names the alternatives the candidate considered and what it rejected. @@ -39,7 +39,7 @@ An external lane counts only when its receipt says `complete` and carries either ## Phase C: Cross-judge -After all Phase B candidates complete, choose the judge descriptor from `arena cross-judge pool` in the current harness's pstack model sheet when present, otherwise from the runner defaults above. Prefer a provider different from the parent and the likely base candidate. Dispatch one read-only judge through the provider contract. It sees the rubric and completed candidates by path label, scores each criterion, and recommends a base with rationale. It runs in parallel with the parent's reading in Phase D, not with the candidates themselves. Starting it while candidates are still writing means the judge sees partial or empty outputs and reports them as dropouts. +After all Phase B candidates complete, choose the judge from the selected `arena-cross-judge-pool` route. On the portable route, use the current harness model sheet when present, otherwise use the runner defaults above. Prefer a provider different from the parent and the likely base candidate. Dispatch one read-only judge through the selected contract. It sees the rubric and completed candidates by path label, scores each criterion, and recommends a base with rationale. It runs in parallel with the parent's reading in Phase D, not with the candidates themselves. Starting it while candidates are still writing means the judge sees partial or empty outputs and reports them as dropouts. ## Phase D: Pick a base diff --git a/plugins/pstack/skills/how/SKILL.md b/plugins/pstack/skills/how/SKILL.md index f6ac8cd..5eb0a65 100644 --- a/plugins/pstack/skills/how/SKILL.md +++ b/plugins/pstack/skills/how/SKILL.md @@ -7,7 +7,7 @@ description: "Use for \"how does X work\", code walkthroughs before changing som Explore the codebase to answer "how does X work?" questions. Produce clear architectural explanations at the level of a senior engineer onboarding onto a subsystem. Enough to build a working mental model, not annotated source code. -**Dispatch contract.** Resolve every configured role through [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md). Values are provider-qualified descriptors; the parent chooses native versus external execution. On Codex, resolve remaining Claude tool names via [`codex-tools.md`](../poteto-mode/references/codex-tools.md). +**Dispatch contract.** Follow the parent-selected Poteto dispatch reference. Conductor mode resolves `how-explorer`, `how-explainer`, and `how-critics` from project policy and isolates every delegated read in a workspace. Provider descriptors and native tool mappings apply only to the portable route. Two modes: @@ -44,7 +44,7 @@ Decompose the question into 2-4 parallel exploration angles, each a distinct sli The right decomposition depends on the question. Use your judgment. Narrow questions: 2 explorers is fine. Broad subsystems: up to 4. -Start all explorers in one fan-out phase through provider dispatch. Use your configured how-explorer descriptor (default `grok:grok-4.6@xhigh`) in `read-only` mode. A native lane uses the parent subagent primitive; an external lane uses the launcher directly. +Start all explorers in one fan-out phase through the selected dispatch contract. Use the configured `how-explorer` route in read-only mode. Its portable default is `grok:grok-4.6@xhigh`. Each explorer gets the same base prompt from `references/explorer-prompt.md` plus a specific exploration angle naming its slice. Each explorer should: - Start broad: Glob for relevant directories, Grep for key types/interfaces/class names diff --git a/plugins/pstack/skills/interrogate/SKILL.md b/plugins/pstack/skills/interrogate/SKILL.md index 85d9b09..3171c38 100644 --- a/plugins/pstack/skills/interrogate/SKILL.md +++ b/plugins/pstack/skills/interrogate/SKILL.md @@ -9,7 +9,7 @@ Spawn one reviewer per configured model to adversarially review code changes. Ea The deliverable is a synthesized verdict. Do NOT auto-apply changes. -**Dispatch contract.** Read [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md) before launching reviewers. Configured entries are provider-qualified descriptors; the parent starts native and external read-only lanes directly. On Codex, resolve remaining Claude tool names via [`codex-tools.md`](../poteto-mode/references/codex-tools.md). +**Dispatch contract.** Follow the parent-selected Poteto dispatch reference. Conductor mode resolves `interrogate-reviewers` from project policy and gives every reviewer an isolated workspace. Provider descriptors, model sheets, native agents, and the launcher apply only to the portable route. ## Step 1, Determine Scope @@ -34,7 +34,7 @@ Write one clear paragraph. Reviewers challenge whether the work achieves the int ## Step 3, Spawn Reviewers -Start all reviewers in one fan-out phase. Use `interrogate reviewers` from the current harness's pstack model sheet when present, one reviewer per entry, extending or shrinking the Reviewer A/B/C/D labels below to the configured entry count; otherwise use the table defaults. Native reviewers use the parent subagent primitive. External reviewers use the launcher directly and must return a complete, model-verified receipt. +Start all reviewers in one fan-out phase. In Conductor mode, use `interrogate-reviewers` from project policy and require a strict Conductor receipt from every attempt. On the portable route, use `interrogate reviewers` from the current harness model sheet when present, one reviewer per entry, otherwise use the table defaults. | Subagent | Default model | |----------|---------------| @@ -43,7 +43,7 @@ Start all reviewers in one fan-out phase. Use `interrogate reviewers` from the c | Reviewer C | `grok:grok-4.6@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. +For each reviewer, route the configured lane with read-only access and a unique output and receipt path. On the portable route, `inherit-parent` and `auto` use the parent subagent primitive without a model override. If an agent, login, or model is unavailable, record a dropout and continue only when the playbook permits reduced coverage. Never pick the closest model or silently fall back. Read `references/reviewer-prompt.md` and fill in the template with: 1. The stated intent diff --git a/plugins/pstack/skills/poteto-mode/SKILL.md b/plugins/pstack/skills/poteto-mode/SKILL.md index 271b82d..abde43d 100644 --- a/plugins/pstack/skills/poteto-mode/SKILL.md +++ b/plugins/pstack/skills/poteto-mode/SKILL.md @@ -5,9 +5,18 @@ description: poteto's agent style for concise, detailed responses, deliberate su # Poteto mode +## Dispatch mode + +Choose the dispatch route before playbook matching or model selection. + +1. If `PSTACK_WORKER=1`, stop. A Poteto worker cannot invoke Poteto Mode or dispatch another worker. +2. Search from the repository root for `.conductor/poteto-mode.json`. +3. When the validated `mode` is `conductor`, read [`references/conductor-dispatch.md`](references/conductor-dispatch.md) and use only that route. +4. Otherwise use the portable route in [`references/provider-dispatch.md`](references/provider-dispatch.md). + ## Platform Adaptation -These skills share one tree across Claude Code and Codex. Read [`references/provider-dispatch.md`](references/provider-dispatch.md) whenever a configured role launches. It defines the provider-qualified model descriptors, native/external route table, launcher, isolation, receipts, and dropout policy. Children never choose routes. When a skill names a Claude tool or built-in skill (`run`, `verify`, `plugin-dev:skill-development`), read [`references/codex-tools.md`](references/codex-tools.md) for the Codex equivalent. +These skills share one tree across Claude Code and Codex. Use the reference selected by **Dispatch mode** whenever a configured role launches. Conductor projects use only `conductor-dispatch.md`. Other projects use `provider-dispatch.md`, which defines the portable native and external routes. Children never choose routes. When a skill names a Claude tool or built-in skill (`run`, `verify`, `plugin-dev:skill-development`), read [`references/codex-tools.md`](references/codex-tools.md) for the Codex equivalent. ## Non-negotiables @@ -85,7 +94,7 @@ 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 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. +In a Conductor project, every delegation follows `conductor-dispatch.md`; do not use `poteto-agent`, a native subagent, or the portable runner. Outside Conductor mode, prefer `poteto-agent` for `inherit-parent`, `auto`, or an unconfigured native ad-hoc helper. A provider-qualified role follows portable provider dispatch. 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 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. diff --git a/plugins/pstack/skills/poteto-mode/references/conductor-dispatch.md b/plugins/pstack/skills/poteto-mode/references/conductor-dispatch.md new file mode 100644 index 0000000..cdc2be7 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/references/conductor-dispatch.md @@ -0,0 +1,110 @@ +# Conductor dispatch + +Use this route only when the nearest `.conductor/poteto-mode.json` is valid +and its `mode` is `conductor`. The policy replaces user model sheets for this +repository. Do not read or use the portable provider route in this mode. + +The Claude or Codex session that invoked Poteto Mode is the coordinator. It +must match `CONDUCTOR_SESSION_ID` for the life of the run. There is no election +or ownership transfer. A session with `PSTACK_WORKER=1` must stop before policy +loading, model resolution, workspace creation, or message dispatch. + +The coordinator calls Conductor through the hosted MCP. The local +`scripts/conductor/pstack-conductor` helper parses saved MCP response JSON, +validates policy and receipts, renders payloads, and persists bounded run +state. It makes no network calls and reads no credentials. + +## Prepare the run + +1. Call `whoami` and save its response as `identity.json`. This proves + authentication and returns the current workspace, but it does not return a + session ID. +2. Call `get_session_status` for `CONDUCTOR_SESSION_ID` and save + `coordinator-status.json`. Its session ID must equal the environment value + and its workspace ID must equal `identity.json`. +3. Call `list_models`, follow every page until `hasMore` is false, and save + `models.json`. +4. Run `pstack-conductor policy validate` with `identity.json`, + `coordinator-status.json`, and `models.json`. A missing exact agent, model, + effort, or fast-mode field ends the route. Never substitute another target. +5. Resolve the store with `git rev-parse --git-path pstack/conductor-runs`. + Run `pstack-conductor run start` with the identity, coordinator status, and + an immutable budget at or below the project ceiling. With no run ID, resume + the only active run owned by this session. Several matches require an + explicit run ID. + +Keep bounded work in the coordinator when the policy route is `coordinator`. +Do not create a native helper. Every delegated task, including read-only +review, gets one isolated Conductor workspace and branch. + +## Dispatch one attempt + +1. Run `pstack-conductor attempt plan` with the run ID, unique attempt ID, + role, purpose, base branch, and a JSON brief. For a multi-lane role, pass one + allowed `--lane` for this attempt. The result contains the exact workspace + request, a null initial message, a stable dispatch message ID, and a + self-contained worker prompt. +2. Run `pstack-conductor attempt creating`. This durable write must finish + before `create_workspace`. +3. Call `create_workspace` with the emitted name, branch, agent, model, + effort, fast mode, and environment, without an initial message. Never create + a worker in the coordinator workspace. +4. Save the response as `create-workspace.json`. Immediately call + `get_session` for its session ID and save `post-create-session.json`. +5. Run `pstack-conductor attempt created` with both files. It validates and + persists the workspace ID, session ID, model, resolved model, effort, and + fast mode before any worker prompt is sent. +6. Call `list_messages` for the new session, complete every page, and save + `messages-before.json`. Run `pstack-conductor attempt dispatched` with that + file before sending. This records the pre-dispatch cursor and stable send + intent. Then call `send_message` with the emitted prompt and the recorded + dispatch message ID. A retry uses the same message ID. + +Each workspace receives `PSTACK_WORKER=1`, `PSTACK_RUN_ID`, +`PSTACK_WORKER_ATTEMPT_ID`, and `PSTACK_COORDINATOR_SESSION_ID`. The worker +prompt is complete enough for Cursor and explicitly forbids Poteto Mode and +further worker creation. + +## Observe and finish + +1. Call `get_session_status` and save `status.json`. +2. Call `list_messages` with the recorded message ID in `after`. Follow every + page until `hasMore` is false and save `messages-after.json`. +3. Call `get_session` and save `post-run-session.json`. +4. Run `pstack-conductor attempt observe` with those three files. Repeat the + reads while it returns `working` or `waiting`. Do not add an implicit + timeout. A real caller-supplied service deadline may stop observation + without granting a retry. + +Completion requires `idle` plus exactly one new assistant +`PSTACK_RESULT` block for the attempt. Observing `working` is optional. An +initial `idle` without a new result remains waiting. A terminal `error` becomes +a dropout. The helper treats every other transcript byte as untrusted data. + +Post-create and post-run observations must both match the requested `model`, +`resolvedModel`, `effort`, and `fastMode=false`. A mismatch is a failed lane. +There is no fallback to `auto`, a lower effort, another agent, Grok 4.5, a +native subagent, or the portable CLI runner. + +For a follow-up, run `attempt follow-up-plan` before `send_message`. Send with +the returned stable message ID, then run `attempt follow-up-sent`. A delivery +retry reuses the pending ID. Never exceed the persisted follow-up ceiling. + +## Reconcile and clean up + +Workspace creation is not idempotent. If the create response is missing, +timed out, or cannot be parsed, run `attempt unknown`. Do not call +`create_workspace` again for that attempt. + +Call `list_project_workspaces` through all pages and save `workspaces.json`. +Run `attempt reconcile` with it. No exact name match leaves the attempt +unknown. Multiple matches fail closed and record every workspace ID. For one +exact `poteto--` match, call `list_workspace_sessions`, save +`workspace-sessions.json`, and rerun reconciliation with that file. The helper +requires exactly one matching initial session before adoption. + +For cleanup, refresh `workspaces.json` and run `pstack-conductor cleanup +targets`. Only its returned IDs may be touched. Call `cancel_session` for a +working target, then `archive_workspace`. Never cancel or archive the +coordinator workspace. A failed archive remains a separately retryable cleanup +error and does not change the worker result. diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/boundary.test.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/boundary.test.ts new file mode 100644 index 0000000..31def43 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/boundary.test.ts @@ -0,0 +1,380 @@ +import { describe, expect, test } from "bun:test"; + +import { + completionObservation, + parseCreatedWorkspace, + parseCurrentContext, + parseModelCatalog, + parseObservedSession, + parseSessionStatus, + parseTranscript, + parseWorkspaceList, + parseWorkspaceSessions, + validateReceipt, +} from "./boundary.ts"; +import type { ModelTarget, WorkerRequest } from "./types.ts"; + +const target: ModelTarget = { + agent: "cursor", + model: "grok-4.6", + resolvedModel: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +const request: WorkerRequest = { + attemptId: "attempt-1", + role: "feature", + purpose: "write", + baseBranch: "origin/main", + workspaceName: "poteto-run-1-attempt-1", + agent: "cursor", + model: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +const observed = { + model: "grok-4.6", + resolvedModel: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +const idle = { + workspaceId: "worker-workspace", + sessionId: "worker-session", + status: "idle", + updatedAt: "2026-09-04T00:00:00Z", +}; + +function transcript( + data: readonly Record[], + hasMore = false +): unknown { + return { data, offset: 0, hasMore }; +} + +function message( + id: string, + type: "user" | "assistant", + content: unknown +): Record { + return { + id, + sessionId: "worker-session", + sessionIndex: Number(id.replace("message-", "")), + type, + content, + receivedAt: "2026-09-04T00:00:00Z", + }; +} + +describe("Conductor response boundary", () => { + test("binds the environment session through authenticated workspace status", () => { + expect( + parseCurrentContext( + { + userId: "user-fixture", + authMethod: "access-jwt", + workspaceId: "workspace-fixture", + }, + { + workspaceId: "workspace-fixture", + sessionId: "session-fixture", + status: "idle", + updatedAt: "2026-09-04T00:00:00Z", + }, + "session-fixture" + ) + ).toEqual({ + sessionId: "session-fixture", + workspaceId: "workspace-fixture", + }); + }); + + test("rejects missing and mismatched current identity fields", () => { + expect(() => parseCurrentContext({}, idle, "worker-session")).toThrow( + "workspaceId" + ); + expect(() => + parseCurrentContext( + { workspaceId: "other-workspace" }, + idle, + "worker-session" + ) + ).toThrow("workspace mismatch"); + expect(() => + parseCurrentContext( + { workspaceId: "worker-workspace" }, + idle, + "other-session" + ) + ).toThrow("session mismatch"); + }); + + test("parses an MCP text envelope and a created workspace", () => { + const raw = { + content: [ + { + type: "text", + text: JSON.stringify({ + workspaceId: "worker-workspace", + sessionId: "worker-session", + deepLink: "https://example.invalid/workspace", + }), + }, + ], + }; + expect(parseCreatedWorkspace(raw)).toEqual({ + workspaceId: "worker-workspace", + sessionId: "worker-session", + }); + expect(() => parseCreatedWorkspace({ workspaceId: "only-one" })).toThrow( + "sessionId" + ); + }); + + test("normalizes the nested model catalog without changing identifiers", () => { + const catalog = parseModelCatalog({ + agents: [ + { + agent: "cursor", + models: [ + { + id: "grok-4.6", + efforts: ["high", "xhigh"], + supportsFastMode: false, + }, + ], + }, + ], + }); + expect(catalog).toEqual([ + { + agent: "cursor", + model: "grok-4.6", + efforts: ["high", "xhigh"], + supportsFastMode: false, + }, + ]); + expect(() => parseModelCatalog({ agents: {} })).toThrow("array"); + expect(() => + parseModelCatalog({ + agents: [ + { + agent: "unknown", + models: [ + { id: "model", efforts: ["high"], supportsFastMode: false }, + ], + }, + ], + }) + ).toThrow("unknown agent"); + }); + + test("requires every exact session receipt field and fast mode off", () => { + expect(parseObservedSession({ id: "session", ...observed })).toEqual( + observed + ); + const { resolvedModel: _removed, ...withoutResolvedModel } = observed; + expect(() => parseObservedSession(withoutResolvedModel)).toThrow( + "resolvedModel" + ); + expect(() => + parseObservedSession({ ...observed, fastMode: true }) + ).toThrow("fastMode"); + expect(() => + validateReceipt(target, request, observed, { + ...observed, + resolvedModel: "grok-4.5", + }) + ).toThrow("receipt mismatch"); + expect(validateReceipt(target, request, observed, observed)).toEqual( + observed + ); + }); + + test("parses status errors and requires exact status IDs", () => { + expect(parseSessionStatus(idle)).toEqual({ + workspaceId: "worker-workspace", + sessionId: "worker-session", + status: "idle", + error: null, + }); + expect( + parseSessionStatus({ + ...idle, + status: "error", + lastError: "worker failed", + }) + ).toEqual({ + workspaceId: "worker-workspace", + sessionId: "worker-session", + status: "error", + error: "worker failed", + }); + expect(() => parseSessionStatus({ ...idle, status: "paused" })).toThrow( + "status" + ); + }); + + test("parses complete transcripts and rejects partial pages", () => { + expect( + parseTranscript( + transcript([ + message("message-1", "user", { text: "task" }), + message("message-2", "assistant", [ + { type: "text", text: "result" }, + ]), + ]) + ) + ).toEqual({ + messages: [ + { + id: "message-1", + sessionId: "worker-session", + index: 1, + role: "user", + text: "task", + }, + { + id: "message-2", + sessionId: "worker-session", + index: 2, + role: "assistant", + text: "result", + }, + ], + lastMessageId: "message-2", + afterCursor: null, + }); + expect(() => parseTranscript(transcript([], true))).toThrow("hasMore"); + }); + + test("returns every workspace and resolves a single initial session", () => { + expect( + parseWorkspaceList({ + data: [ + { + id: "workspace-a", + name: "poteto-run-1-attempt-1", + state: "ready", + repoUrl: "https://example.invalid/repo.git", + createdAt: "2026-09-04T00:00:00Z", + deepLink: "https://example.invalid/workspace-a", + }, + { + id: "workspace-b", + name: "poteto-run-1-attempt-1", + state: "ready", + repoUrl: "https://example.invalid/repo.git", + createdAt: "2026-09-04T00:00:00Z", + deepLink: "https://example.invalid/workspace-b", + }, + ], + offset: 0, + hasMore: false, + }) + ).toEqual([ + { + workspaceId: "workspace-a", + workspaceName: "poteto-run-1-attempt-1", + }, + { + workspaceId: "workspace-b", + workspaceName: "poteto-run-1-attempt-1", + }, + ]); + expect( + parseWorkspaceSessions( + "workspace-a", + { data: [{ id: "session-a", ...observed }], offset: 0, hasMore: false }, + target + ) + ).toEqual({ + ids: { workspaceId: "workspace-a", sessionId: "session-a" }, + observation: observed, + }); + expect(() => + parseWorkspaceSessions( + "workspace-a", + { data: [], offset: 0, hasMore: false }, + target + ) + ).toThrow("exactly one session"); + }); + + test("requires idle plus one new assistant result", () => { + const result = `\n${JSON.stringify({ + attemptId: "attempt-1", + status: "complete", + summary: "marker returned", + evidence: ["marker:42"], + changedFiles: [], + })}\n`; + const before = parseTranscript( + transcript([message("message-1", "user", "task")]) + ); + const noResult = completionObservation( + parseSessionStatus(idle), + before, + "message-1", + "attempt-1" + ); + expect(noResult.kind).toBe("waiting"); + + const complete = completionObservation( + parseSessionStatus(idle), + parseTranscript( + transcript([ + message("message-1", "user", "task"), + message("message-2", "assistant", result), + ]) + ), + "message-1", + "attempt-1" + ); + expect(complete.kind).toBe("complete"); + + const filtered = completionObservation( + parseSessionStatus(idle), + parseTranscript( + transcript([message("message-2", "assistant", result)]), + "message-1" + ), + "message-1", + "attempt-1" + ); + expect(filtered.kind).toBe("complete"); + + const unscoped = completionObservation( + parseSessionStatus(idle), + parseTranscript(transcript([message("message-2", "assistant", result)])), + "message-1", + "attempt-1" + ); + expect(unscoped.kind).toBe("waiting"); + + expect( + completionObservation( + parseSessionStatus({ ...idle, status: "working" }), + before, + "message-1", + "attempt-1" + ).kind + ).toBe("working"); + expect( + completionObservation( + parseSessionStatus({ + ...idle, + status: "error", + lastError: "terminal failure", + }), + before, + "message-1", + "attempt-1" + ) + ).toEqual({ kind: "dropout", error: "terminal failure" }); + }); +}); diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/boundary.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/boundary.ts new file mode 100644 index 0000000..066d630 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/boundary.ts @@ -0,0 +1,370 @@ +import { parseWorkerResult } from "./prompt.ts"; +import type { + CatalogEntry, + CompletionObservation, + CurrentContext, + Effort, + ModelTarget, + ObservedSession, + SessionStatus, + Transcript, + TranscriptMessage, + WorkerAgent, + WorkerIds, + WorkerRequest, + WorkspaceListing, +} from "./types.ts"; + +export class BoundaryError extends Error { + override readonly name = "BoundaryError"; +} + +function fail(detail: string): never { + throw new BoundaryError(`invalid Conductor response: ${detail}`); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function record(value: unknown, label: string): Record { + if (!isRecord(value)) { + fail(`${label} must be an object`); + } + return value; +} + +function text(value: unknown, label: string): string { + if (typeof value !== "string" || value.length === 0) { + fail(`${label} must be a non-empty string`); + } + return value; +} + +function integer(value: unknown, label: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) { + fail(`${label} must be a non-negative integer`); + } + return value; +} + +function unwrap(raw: unknown): unknown { + if (!isRecord(raw)) { + return raw; + } + if (raw.structuredContent !== undefined) { + return raw.structuredContent; + } + if (!Array.isArray(raw.content)) { + return raw; + } + const blocks = raw.content.filter( + (item) => isRecord(item) && item.type === "text" && typeof item.text === "string" + ); + if (blocks.length !== 1) { + fail("MCP envelope must contain one JSON text block"); + } + const block = blocks[0]; + if (block === undefined || typeof block.text !== "string") { + fail("MCP envelope text is missing"); + } + try { + return JSON.parse(block.text); + } catch { + return fail("MCP envelope text must be valid JSON"); + } +} + +function pagedData(raw: unknown, label: string): readonly unknown[] { + const page = record(unwrap(raw), label); + if (!Array.isArray(page.data)) { + fail(`${label}.data must be an array`); + } + if (page.hasMore !== false) { + fail(`${label}.hasMore must be false after pagination`); + } + return page.data; +} + +function agent(value: unknown): WorkerAgent { + if (value === "claude" || value === "codex" || value === "cursor") { + return value; + } + return fail(`unknown agent: ${String(value)}`); +} + +function effort(value: unknown): Effort { + if ( + value === "low" || + value === "medium" || + value === "high" || + value === "xhigh" + ) { + return value; + } + return fail(`unsupported effort: ${String(value)}`); +} + +function efforts(value: unknown, label: string): readonly Effort[] { + if (!Array.isArray(value) || value.length === 0) { + fail(`${label} must be a non-empty array`); + } + return value.map(effort); +} + +function catalogModel( + value: unknown, + inheritedAgent?: WorkerAgent +): CatalogEntry { + const item = record(value, "model catalog entry"); + const itemAgent = inheritedAgent ?? agent(item.agent); + const model = text(item.model ?? item.id, "model catalog model"); + const supportedEfforts = efforts( + item.efforts ?? item.effortLevels, + `${itemAgent}:${model} efforts` + ); + const supportsFastMode = item.supportsFastMode ?? item.fastModeSupported; + if (typeof supportsFastMode !== "boolean") { + fail(`${itemAgent}:${model} supportsFastMode must be boolean`); + } + return { + agent: itemAgent, + model, + efforts: supportedEfforts, + supportsFastMode, + }; +} + +export function parseCurrentContext( + identityRaw: unknown, + sessionStatusRaw: unknown, + expectedSessionId: string +): CurrentContext { + const identity = record(unwrap(identityRaw), "whoami"); + const workspaceId = text(identity.workspaceId, "whoami.workspaceId"); + const status = parseSessionStatus(sessionStatusRaw); + if (status.sessionId !== expectedSessionId) { + throw new BoundaryError("Conductor coordinator session mismatch"); + } + if (status.workspaceId !== workspaceId) { + throw new BoundaryError("Conductor coordinator workspace mismatch"); + } + return { sessionId: status.sessionId, workspaceId }; +} + +export function parseModelCatalog(raw: unknown): readonly CatalogEntry[] { + const value = unwrap(raw); + if (Array.isArray(value)) { + return value.map((item) => catalogModel(item)); + } + const root = record(value, "model catalog"); + if (Array.isArray(root.agents)) { + const entries: CatalogEntry[] = []; + for (const rawAgent of root.agents) { + const group = record(rawAgent, "model catalog agent"); + const groupAgent = agent(group.agent); + if (!Array.isArray(group.models)) { + fail(`${groupAgent} models must be an array`); + } + entries.push( + ...group.models.map((item) => catalogModel(item, groupAgent)) + ); + } + return entries; + } + if (Array.isArray(root.data)) { + return root.data.map((item) => catalogModel(item)); + } + return fail("model catalog agents must be an array"); +} + +export function parseCreatedWorkspace(raw: unknown): WorkerIds { + const created = record(unwrap(raw), "created workspace"); + return { + workspaceId: text(created.workspaceId, "created workspace.workspaceId"), + sessionId: text(created.sessionId, "created workspace.sessionId"), + }; +} + +export function parseObservedSession( + raw: unknown, + expectedSessionId?: string +): ObservedSession { + const session = record(unwrap(raw), "session"); + if ( + expectedSessionId !== undefined && + text(session.id, "session.id") !== expectedSessionId + ) { + throw new BoundaryError("Conductor session ID mismatch"); + } + if (session.fastMode !== false) { + fail("session.fastMode must be false"); + } + return { + model: text(session.model, "session.model"), + resolvedModel: text(session.resolvedModel, "session.resolvedModel"), + effort: text(session.effort, "session.effort"), + fastMode: false, + }; +} + +export function parseSessionStatus(raw: unknown): SessionStatus { + const value = record(unwrap(raw), "session status"); + if (value.status !== "idle" && value.status !== "working" && value.status !== "error") { + fail("session status.status is unsupported"); + } + const errorValue = value.lastError ?? value.errorMessage ?? null; + if (errorValue !== null && typeof errorValue !== "string") { + fail("session status error must be a string"); + } + return { + workspaceId: text(value.workspaceId, "session status.workspaceId"), + sessionId: text(value.sessionId, "session status.sessionId"), + status: value.status, + error: errorValue, + }; +} + +function messageRole(value: string): TranscriptMessage["role"] { + if (value === "assistant" || value === "assistant_message") { + return "assistant"; + } + if (value === "user" || value === "user_message") { + return "user"; + } + return "other"; +} + +function contentText(value: unknown): string { + if (typeof value === "string") { + return value; + } + if (isRecord(value) && typeof value.text === "string") { + return value.text; + } + if (Array.isArray(value)) { + const parts: string[] = []; + for (const block of value) { + if (!isRecord(block) || block.type !== "text" || typeof block.text !== "string") { + fail("message content contains a non-text block"); + } + parts.push(block.text); + } + return parts.join("\n"); + } + return fail("message content must be text"); +} + +export function parseTranscript( + raw: unknown, + afterCursor: string | null = null +): Transcript { + const values = pagedData(raw, "messages"); + const messages = values.map((value, index): TranscriptMessage => { + const item = record(value, `message ${index}`); + const type = text(item.type, `message ${index}.type`); + return { + id: text(item.id, `message ${index}.id`), + sessionId: text(item.sessionId, `message ${index}.sessionId`), + index: integer(item.sessionIndex, `message ${index}.sessionIndex`), + role: messageRole(type), + text: contentText(item.content), + }; + }); + return { + messages, + lastMessageId: messages.at(-1)?.id ?? afterCursor, + afterCursor, + }; +} + +export function parseWorkspaceList(raw: unknown): readonly WorkspaceListing[] { + return pagedData(raw, "workspaces").map((value, index) => { + const workspace = record(value, `workspace ${index}`); + return { + workspaceId: text(workspace.id, `workspace ${index}.id`), + workspaceName: text(workspace.name, `workspace ${index}.name`), + }; + }); +} + +export function parseWorkspaceSessions( + workspaceId: string, + raw: unknown, + target: ModelTarget +): { readonly ids: WorkerIds; readonly observation: ObservedSession } { + const sessions = pagedData(raw, "workspace sessions"); + if (sessions.length !== 1) { + fail("reconciled workspace must contain exactly one session"); + } + const session = record(sessions[0], "workspace session"); + const sessionId = text(session.id, "workspace session.id"); + const observation = parseObservedSession(session); + if ( + observation.model !== target.model || + observation.resolvedModel !== target.resolvedModel || + observation.effort !== target.effort + ) { + throw new BoundaryError("Conductor reconciled session receipt mismatch"); + } + return { ids: { workspaceId, sessionId }, observation }; +} + +export function validateReceipt( + target: ModelTarget, + request: WorkerRequest, + postCreate: ObservedSession, + postRun: ObservedSession +): ObservedSession { + const matches = (value: ObservedSession): boolean => + value.model === request.model && + value.model === target.model && + value.resolvedModel === target.resolvedModel && + value.effort === request.effort && + value.effort === target.effort && + value.fastMode === false; + if (!matches(postCreate) || !matches(postRun)) { + throw new BoundaryError("Conductor receipt mismatch"); + } + return postRun; +} + +function messagesAfter( + transcript: Transcript, + cursor: string | null +): readonly TranscriptMessage[] { + if (cursor === null) { + return transcript.messages; + } + if (transcript.afterCursor === cursor) { + return transcript.messages; + } + const index = transcript.messages.findIndex((message) => message.id === cursor); + return index < 0 ? [] : transcript.messages.slice(index + 1); +} + +export function completionObservation( + status: SessionStatus, + transcript: Transcript, + cursor: string | null, + attemptId: string +): CompletionObservation { + if (status.status === "error") { + return { kind: "dropout", error: status.error ?? "Conductor session error" }; + } + if (status.status === "working") { + return { kind: "working" }; + } + const candidates = messagesAfter(transcript, cursor).filter( + (message) => message.role === "assistant" && message.text.includes(" { + directory = await mkdtemp(join(tmpdir(), "pstack-conductor-cli-")); + storeDirectory = join(directory, "store"); + identityPath = join(directory, "identity.json"); + coordinatorStatusPath = join(directory, "coordinator-status.json"); + budgetPath = join(directory, "budget.json"); + briefPath = join(directory, "brief.json"); + emptyMessagesPath = join(directory, "empty-messages.json"); + await Promise.all([ + writeFile( + identityPath, + JSON.stringify({ + userId: "user-fixture", + authMethod: "access-jwt", + workspaceId: "coordinator-workspace", + }) + ), + writeFile( + coordinatorStatusPath, + JSON.stringify({ + workspaceId: "coordinator-workspace", + sessionId: "coordinator-session", + status: "idle", + updatedAt: "2026-09-04T00:00:00Z", + }) + ), + writeFile( + budgetPath, + JSON.stringify({ + maxWorkspaces: 5, + maxConcurrentWorkspaces: 4, + maxAttempts: 6, + maxFollowUpsPerAttempt: 2, + }) + ), + writeFile( + briefPath, + JSON.stringify({ + repository: "https://example.invalid/repo.git", + allowedFiles: ["src/marker.ts"], + questions: [], + requiredEvidence: ["marker:42"], + task: "Create the marker file.", + }) + ), + writeFile( + emptyMessagesPath, + JSON.stringify({ data: [], offset: 0, hasMore: false }) + ), + ]); +}); + +afterEach(async () => { + await rm(directory, { recursive: true, force: true }); +}); + +interface Invocation { + readonly exitCode: number; + readonly stdout: string; + readonly stderr: string; +} + +async function invoke( + args: readonly string[], + environment: Readonly> = { + CONDUCTOR_SESSION_ID: "coordinator-session", + } +): Promise { + let stdout = ""; + let stderr = ""; + const exitCode = await main( + ["--store", storeDirectory, "--policy", policyPath, ...args], + environment, + { + stdout: (value) => { + stdout += value; + }, + stderr: (value) => { + stderr += value; + }, + } + ); + return { exitCode, stdout, stderr }; +} + +function output(invocation: Invocation): Record { + expect(invocation.exitCode).toBe(0); + expect(invocation.stderr).toBe(""); + const value: unknown = JSON.parse(invocation.stdout); + if (!isRecord(value)) { + throw new Error("CLI output must be an object"); + } + return value; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +async function startRun(): Promise> { + return output( + await invoke([ + "run", + "start", + "--run-id", + "run-1", + "--coordinator-agent", + "codex", + "--coordinator-model", + "gpt-5.6-sol", + "--identity", + identityPath, + "--coordinator-status", + coordinatorStatusPath, + "--budget", + budgetPath, + ]) + ); +} + +describe("pstack-conductor CLI", () => { + test("validates policy against authenticated context and the exact catalog", async () => { + const result = output( + await invoke([ + "policy", + "validate", + "--identity", + identityPath, + "--coordinator-status", + coordinatorStatusPath, + "--models", + modelsPath, + ]) + ); + expect(result.valid).toBe(true); + expect(result.context).toEqual({ + sessionId: "coordinator-session", + workspaceId: "coordinator-workspace", + }); + }); + + test("rejects missing identity, worker recursion, and Cursor coordination", async () => { + const missing = await invoke( + [ + "run", + "start", + "--coordinator-agent", + "codex", + "--coordinator-model", + "gpt-5.6-sol", + "--identity", + identityPath, + "--coordinator-status", + coordinatorStatusPath, + "--budget", + budgetPath, + ], + {} + ); + expect(missing.exitCode).toBe(64); + expect(missing.stderr).toContain("CONDUCTOR_SESSION_ID"); + + const worker = await invoke( + [ + "run", + "start", + "--coordinator-agent", + "codex", + "--coordinator-model", + "gpt-5.6-sol", + "--identity", + identityPath, + "--coordinator-status", + coordinatorStatusPath, + "--budget", + budgetPath, + ], + { CONDUCTOR_SESSION_ID: "coordinator-session", PSTACK_WORKER: "1" } + ); + expect(worker.exitCode).toBe(64); + expect(worker.stderr).toContain("worker cannot coordinate"); + + const cursor = await invoke([ + "run", + "start", + "--coordinator-agent", + "cursor", + "--coordinator-model", + "grok-4.6", + "--identity", + identityPath, + "--coordinator-status", + coordinatorStatusPath, + "--budget", + budgetPath, + ]); + expect(cursor.exitCode).toBe(64); + expect(cursor.stderr).toContain("claude or codex"); + }); + + test("runs the deterministic attempt lifecycle and emits only MCP data", async () => { + expect((await startRun()).runId).toBe("run-1"); + + const planned = output( + await invoke([ + "attempt", + "plan", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--role", + "feature", + "--purpose", + "write", + "--base-branch", + "origin/main", + "--brief", + briefPath, + ]) + ); + expect(planned.workspace).toEqual({ + name: "poteto-run-1-attempt-1", + branch: "origin/main", + agent: "cursor", + model: "grok-4.6", + effort: "xhigh", + fastMode: false, + env: { + PSTACK_WORKER: "1", + PSTACK_RUN_ID: "run-1", + PSTACK_WORKER_ATTEMPT_ID: "attempt-1", + PSTACK_COORDINATOR_SESSION_ID: "coordinator-session", + }, + }); + expect(planned.initialMessage).toBeNull(); + expect(planned.prompt).toContain("Do not invoke Poteto Mode"); + expect(JSON.stringify(planned)).not.toMatch( + /scripts\/runner\/pstack-runner|bearer|spawn_agent|native Agent/ + ); + + expect( + output( + await invoke([ + "attempt", + "creating", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + ]) + ).state + ).toBe("creating"); + + expect( + output( + await invoke([ + "attempt", + "created", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--workspace-response", + createdPath, + "--session-response", + sessionPath, + ]) + ).state + ).toBe("queued"); + + expect( + output( + await invoke([ + "attempt", + "dispatched", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--messages-before", + emptyMessagesPath, + ]) + ).state + ).toBe("queued"); + + const followUp = output( + await invoke([ + "attempt", + "follow-up-plan", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + ]) + ); + expect(followUp.messageId).toBe("poteto-run-1-attempt-1-followup-1"); + output( + await invoke([ + "attempt", + "follow-up-sent", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--message-id", + "poteto-run-1-attempt-1-followup-1", + ]) + ); + + const completed = output( + await invoke([ + "attempt", + "observe", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--session-response", + sessionPath, + "--status-response", + idlePath, + "--messages-response", + messagesPath, + ]) + ); + expect(completed.outcome).toBe("complete"); + + const cleanup = output( + await invoke([ + "cleanup", + "targets", + "--run-id", + "run-1", + "--workspaces-response", + workspacesPath, + ]) + ); + expect(cleanup.targets).toEqual([ + { + attemptId: "attempt-1", + workspaceId: "worker-workspace", + sessionId: "worker-session", + expectedWorkspaceName: "poteto-run-1-attempt-1", + }, + ]); + }); + + test("resumes only the run owned by the same coordinator", async () => { + await startRun(); + const resumed = output( + await invoke([ + "run", + "start", + "--coordinator-agent", + "codex", + "--coordinator-model", + "gpt-5.6-sol", + "--identity", + identityPath, + "--coordinator-status", + coordinatorStatusPath, + "--budget", + budgetPath, + ]) + ); + expect(resumed.runId).toBe("run-1"); + + const otherStatus = join(directory, "other-status.json"); + await writeFile( + otherStatus, + JSON.stringify({ + workspaceId: "coordinator-workspace", + sessionId: "other-session", + status: "idle", + updatedAt: "2026-09-04T00:00:00Z", + }) + ); + const refused = await invoke( + [ + "run", + "start", + "--run-id", + "run-1", + "--coordinator-agent", + "codex", + "--coordinator-model", + "gpt-5.6-sol", + "--identity", + identityPath, + "--coordinator-status", + otherStatus, + "--budget", + budgetPath, + ], + { CONDUCTOR_SESSION_ID: "other-session" } + ); + expect(refused.exitCode).toBe(64); + expect(refused.stderr).toContain("cannot take ownership"); + }); + + test("reconciles an uncertain create through the exact workspace and its session", async () => { + await startRun(); + output( + await invoke([ + "attempt", + "plan", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--role", + "feature", + "--purpose", + "write", + "--base-branch", + "origin/main", + "--brief", + briefPath, + ]) + ); + output( + await invoke([ + "attempt", + "creating", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + ]) + ); + output( + await invoke([ + "attempt", + "unknown", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--error", + "create response was lost", + ]) + ); + + const reconciled = output( + await invoke([ + "attempt", + "reconcile", + "--run-id", + "run-1", + "--attempt-id", + "attempt-1", + "--workspaces-response", + workspacesPath, + "--workspace-sessions-response", + join(fixtureDirectory, "workspace-sessions.json"), + ]) + ); + expect(reconciled.kind).toBe("adopted"); + expect(reconciled.worker).toMatchObject({ + state: "queued", + ids: { + workspaceId: "worker-workspace", + sessionId: "worker-session", + }, + }); + }); +}); diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/cli.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/cli.ts new file mode 100644 index 0000000..afabdb8 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/cli.ts @@ -0,0 +1,851 @@ +import { randomUUID } from "node:crypto"; +import { readFile } from "node:fs/promises"; + +import { Command, CommanderError } from "commander"; + +import { + completionObservation, + parseCreatedWorkspace, + parseCurrentContext, + parseModelCatalog, + parseObservedSession, + parseSessionStatus, + parseTranscript, + parseWorkspaceList, + parseWorkspaceSessions, + validateReceipt, +} from "./boundary.ts"; +import { readPolicy, resolveRole, validateCatalog } from "./policy.ts"; +import { renderWorkerPrompt } from "./prompt.ts"; +import { + adoptUnknown, + assertCoordinator, + cleanupTargets, + createRun, + markCreating, + markUnknown, + openRunStore, + planAttempt, + planFollowUp, + reconcileUnknown, + recordComplete, + recordCreated, + recordDispatch, + recordDropout, + recordFollowUpSent, + recordWorking, +} from "./store.ts"; +import type { + ConductorPolicy, + CoordinatorAgent, + LaneName, + ModelTarget, + PotetoRun, + RunBudget, + WorkerAttempt, + WorkerBrief, + WorkerPurpose, +} from "./types.ts"; + +export interface CliIo { + readonly stdout: (value: string) => void; + readonly stderr: (value: string) => void; +} + +interface GlobalOptions { + readonly store: string; + readonly policy?: string; +} + +type CliEnvironment = Readonly>; + +const defaultIo: CliIo = { + stdout: (value) => process.stdout.write(value), + stderr: (value) => process.stderr.write(value), +}; + +class CliError extends Error { + override readonly name = "CliError"; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function object(value: unknown, label: string): Record { + if (!isRecord(value)) { + throw new CliError(`${label} must be a JSON object`); + } + return value; +} + +async function readJson(path: string): Promise { + try { + return JSON.parse(await readFile(path, "utf8")); + } catch (error) { + throw new CliError( + `cannot read JSON file ${path}: ${error instanceof Error ? error.message : String(error)}` + ); + } +} + +function exactKeys( + value: Record, + expected: readonly string[], + label: string +): void { + const keys = new Set(expected); + for (const key of Object.keys(value)) { + if (!keys.has(key)) { + throw new CliError(`${label} has unknown key ${key}`); + } + } + for (const key of expected) { + if (!(key in value)) { + throw new CliError(`${label} is missing ${key}`); + } + } +} + +function nonEmpty(value: unknown, label: string): string { + if (typeof value !== "string" || value.trim().length === 0) { + throw new CliError(`${label} must be a non-empty string`); + } + return value; +} + +function safeId(value: string, label: string): string { + if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(value)) { + throw new CliError(`${label} must contain only letters, digits, _ or -`); + } + return value; +} + +function positiveInteger(value: unknown, label: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) { + throw new CliError(`${label} must be a positive integer`); + } + return value; +} + +function parseBudget(value: unknown, ceiling: RunBudget): RunBudget { + const budget = object(value, "run budget"); + exactKeys( + budget, + [ + "maxWorkspaces", + "maxConcurrentWorkspaces", + "maxAttempts", + "maxFollowUpsPerAttempt", + ], + "run budget" + ); + const parsed: RunBudget = { + maxWorkspaces: positiveInteger(budget.maxWorkspaces, "maxWorkspaces"), + maxConcurrentWorkspaces: positiveInteger( + budget.maxConcurrentWorkspaces, + "maxConcurrentWorkspaces" + ), + maxAttempts: positiveInteger(budget.maxAttempts, "maxAttempts"), + maxFollowUpsPerAttempt: positiveInteger( + budget.maxFollowUpsPerAttempt, + "maxFollowUpsPerAttempt" + ), + }; + if (parsed.maxConcurrentWorkspaces > parsed.maxWorkspaces) { + throw new CliError("maxConcurrentWorkspaces cannot exceed maxWorkspaces"); + } + if (parsed.maxWorkspaces > ceiling.maxWorkspaces) { + throw new CliError("run budget exceeds project maxWorkspaces"); + } + if (parsed.maxConcurrentWorkspaces > ceiling.maxConcurrentWorkspaces) { + throw new CliError("run budget exceeds project maxConcurrentWorkspaces"); + } + if (parsed.maxAttempts > ceiling.maxAttempts) { + throw new CliError("run budget exceeds project maxAttempts"); + } + if (parsed.maxFollowUpsPerAttempt > ceiling.maxFollowUpsPerAttempt) { + throw new CliError("run budget exceeds project maxFollowUpsPerAttempt"); + } + return parsed; +} + +function coordinatorAgent(value: string): CoordinatorAgent { + if (value === "claude" || value === "codex") { + return value; + } + throw new CliError("coordinator agent must be claude or codex"); +} + +function purpose(value: string): WorkerPurpose { + if (value === "review" || value === "write") { + return value; + } + throw new CliError("purpose must be review or write"); +} + +function stringArray(value: unknown, label: string): readonly string[] { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + throw new CliError(`${label} must be a string array`); + } + return value; +} + +function parseBrief(value: unknown): WorkerBrief { + const brief = object(value, "worker brief"); + exactKeys( + brief, + ["repository", "allowedFiles", "questions", "requiredEvidence", "task"], + "worker brief" + ); + return { + repository: nonEmpty(brief.repository, "worker brief repository"), + allowedFiles: stringArray(brief.allowedFiles, "worker brief allowedFiles"), + questions: stringArray(brief.questions, "worker brief questions"), + requiredEvidence: stringArray( + brief.requiredEvidence, + "worker brief requiredEvidence" + ), + task: nonEmpty(brief.task, "worker brief task"), + }; +} + +function emit(io: CliIo, value: unknown): void { + io.stdout(`${JSON.stringify(value)}\n`); +} + +function sessionId(environment: CliEnvironment): string { + const value = environment.CONDUCTOR_SESSION_ID; + if (value === undefined || value.length === 0) { + throw new CliError("CONDUCTOR_SESSION_ID is required"); + } + return value; +} + +function guardCoordinator(environment: CliEnvironment): string { + if (environment.PSTACK_WORKER === "1") { + throw new CliError("a Poteto worker cannot coordinate or dispatch workers"); + } + return sessionId(environment); +} + +function requiredPolicyPath(program: Command): string { + const path = program.opts().policy; + if (path === undefined) { + throw new CliError("--policy is required for this command"); + } + return path; +} + +function attempt(run: PotetoRun, attemptId: string): WorkerAttempt { + const value = run.workers.find( + (candidate) => candidate.request.attemptId === attemptId + ); + if (value === undefined) { + throw new CliError(`unknown attempt ID: ${attemptId}`); + } + return value; +} + +function targetForAttempt( + policy: ConductorPolicy, + worker: WorkerAttempt +): ModelTarget { + const target = Object.values(policy.lanes).find( + (candidate) => + candidate.agent === worker.request.agent && + candidate.model === worker.request.model && + candidate.effort === worker.request.effort + ); + if (target === undefined) { + throw new CliError("attempt target is absent from the current policy"); + } + return target; +} + +function selectedTarget( + policy: ConductorPolicy, + role: string, + requestedLane: string | undefined +): ModelTarget { + const route = resolveRole(policy, role); + if (route === "coordinator") { + throw new CliError(`role ${role} must remain in the coordinator`); + } + let lane: LaneName; + if (requestedLane === undefined) { + if (route.length !== 1) { + throw new CliError(`role ${role} requires an explicit --lane`); + } + const only = route[0]; + if (only === undefined) { + throw new CliError(`role ${role} has no worker lane`); + } + lane = only; + } else { + const parsedLane = laneName(requestedLane); + if (!route.includes(parsedLane)) { + throw new CliError(`lane ${requestedLane} is not allowed for role ${role}`); + } + lane = parsedLane; + } + return policy.lanes[lane]; +} + +function laneName(value: string): LaneName { + if ( + value === "judgment" || + value === "hard-review" || + value === "implementation" || + value === "exploration" + ) { + return value; + } + throw new CliError(`unknown policy lane: ${value}`); +} + +async function withStore( + directory: string, + action: (store: ReturnType) => Promise +): Promise { + const store = openRunStore(directory); + try { + return await action(store); + } finally { + await store.close(); + } +} + +function requireAttemptIds(worker: WorkerAttempt): { + readonly workspaceId: string; + readonly sessionId: string; +} { + if ( + worker.state !== "queued" && + worker.state !== "working" && + worker.state !== "complete" + ) { + throw new CliError(`attempt ${worker.request.attemptId} has no active session`); + } + return worker.ids; +} + +function verifyObservationIds( + worker: WorkerAttempt, + workspaceId: string, + observedSessionId: string +): void { + const ids = requireAttemptIds(worker); + if (ids.workspaceId !== workspaceId || ids.sessionId !== observedSessionId) { + throw new CliError("Conductor observation IDs do not match the attempt"); + } +} + +function buildProgram( + environment: CliEnvironment, + io: CliIo +): Command { + const program = new Command("pstack-conductor") + .description("Validate and persist Poteto Mode Conductor operations") + .requiredOption("--store ", "durable run store") + .option("--policy ", "Conductor project policy") + .configureOutput({ writeOut: io.stdout, writeErr: io.stderr }) + .exitOverride(); + + program + .command("policy") + .command("validate") + .requiredOption("--identity ") + .requiredOption("--coordinator-status ") + .requiredOption("--models ") + .action(async (options: { + identity: string; + coordinatorStatus: string; + models: string; + }) => { + const expectedSessionId = guardCoordinator(environment); + const policy = await readPolicy(requiredPolicyPath(program)); + const context = parseCurrentContext( + await readJson(options.identity), + await readJson(options.coordinatorStatus), + expectedSessionId + ); + validateCatalog(policy, parseModelCatalog(await readJson(options.models))); + emit(io, { valid: true, context }); + }); + + program + .command("run") + .command("start") + .option("--run-id ") + .requiredOption("--coordinator-agent ") + .requiredOption("--coordinator-model ") + .requiredOption("--identity ") + .requiredOption("--coordinator-status ") + .requiredOption("--budget ") + .action(async (options: { + runId?: string; + coordinatorAgent: string; + coordinatorModel: string; + identity: string; + coordinatorStatus: string; + budget: string; + }) => { + const expectedSessionId = guardCoordinator(environment); + const context = parseCurrentContext( + await readJson(options.identity), + await readJson(options.coordinatorStatus), + expectedSessionId + ); + const policy = await readPolicy(requiredPolicyPath(program)); + const budget = parseBudget(await readJson(options.budget), policy.budget); + const globals = program.opts(); + const run = await withStore(globals.store, async (store) => { + const runs = await store.list(); + let existing: PotetoRun | undefined; + let runId: string; + if (options.runId !== undefined) { + runId = safeId(options.runId, "run ID"); + existing = runs.find((candidate) => candidate.runId === runId); + } else { + const owned = runs.filter( + (candidate) => + candidate.status === "active" && + candidate.coordinator.sessionId === context.sessionId + ); + if (owned.length > 1) { + throw new CliError("multiple active runs require an explicit --run-id"); + } + existing = owned[0]; + runId = existing?.runId ?? randomUUID(); + } + if (existing !== undefined) { + assertCoordinator(existing, context.sessionId); + } + const next = createRun({ + runId, + coordinator: { + ...context, + agent: coordinatorAgent(options.coordinatorAgent), + model: nonEmpty(options.coordinatorModel, "coordinator model"), + }, + budget, + existing, + }); + await store.save(next); + return next; + }); + emit(io, run); + }); + + const attemptCommand = program.command("attempt"); + + attemptCommand + .command("plan") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--role ") + .option("--lane ") + .requiredOption("--purpose ") + .requiredOption("--base-branch ") + .requiredOption("--brief ") + .action(async (options: { + runId: string; + attemptId: string; + role: string; + lane?: string; + purpose: string; + baseBranch: string; + brief: string; + }) => { + const coordinatorSessionId = guardCoordinator(environment); + const policy = await readPolicy(requiredPolicyPath(program)); + const target = selectedTarget(policy, options.role, options.lane); + const brief = parseBrief(await readJson(options.brief)); + const runId = safeId(options.runId, "run ID"); + const attemptId = safeId(options.attemptId, "attempt ID"); + const globals = program.opts(); + const next = await withStore(globals.store, async (store) => { + const run = await store.load(runId); + assertCoordinator(run, coordinatorSessionId); + const updated = planAttempt(run, { + attemptId, + role: options.role, + purpose: purpose(options.purpose), + baseBranch: nonEmpty(options.baseBranch, "base branch"), + target, + dispatchMessageId: `poteto-${runId}-${attemptId}-dispatch`, + }); + await store.save(updated); + return updated; + }); + const worker = attempt(next, attemptId); + emit(io, { + workspace: { + name: worker.request.workspaceName, + branch: worker.request.baseBranch, + agent: worker.request.agent, + model: worker.request.model, + effort: worker.request.effort, + fastMode: false, + env: { + PSTACK_WORKER: "1", + PSTACK_RUN_ID: runId, + PSTACK_WORKER_ATTEMPT_ID: attemptId, + PSTACK_COORDINATOR_SESSION_ID: coordinatorSessionId, + }, + }, + initialMessage: null, + prompt: renderWorkerPrompt({ + runId, + coordinatorSessionId, + request: worker.request, + target, + brief, + }), + dispatchMessageId: worker.dispatchMessageId, + }); + }); + + attemptCommand + .command("creating") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .action(async (options: { runId: string; attemptId: string }) => { + const owner = guardCoordinator(environment); + const globals = program.opts(); + const updated = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const next = markCreating(run, safeId(options.attemptId, "attempt ID")); + await store.save(next); + return attempt(next, options.attemptId); + }); + emit(io, updated); + }); + + attemptCommand + .command("created") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--workspace-response ") + .requiredOption("--session-response ") + .action(async (options: { + runId: string; + attemptId: string; + workspaceResponse: string; + sessionResponse: string; + }) => { + const owner = guardCoordinator(environment); + const policy = await readPolicy(requiredPolicyPath(program)); + const ids = parseCreatedWorkspace(await readJson(options.workspaceResponse)); + const observation = parseObservedSession( + await readJson(options.sessionResponse), + ids.sessionId + ); + const globals = program.opts(); + const updated = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const worker = attempt(run, safeId(options.attemptId, "attempt ID")); + validateReceipt(targetForAttempt(policy, worker), worker.request, observation, observation); + const next = recordCreated(run, options.attemptId, ids, observation); + await store.save(next); + return attempt(next, options.attemptId); + }); + emit(io, updated); + }); + + attemptCommand + .command("unknown") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--error ") + .action(async (options: { runId: string; attemptId: string; error: string }) => { + const owner = guardCoordinator(environment); + const globals = program.opts(); + const worker = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const next = markUnknown( + run, + safeId(options.attemptId, "attempt ID"), + nonEmpty(options.error, "error") + ); + await store.save(next); + return attempt(next, options.attemptId); + }); + emit(io, worker); + }); + + attemptCommand + .command("reconcile") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--workspaces-response ") + .option("--workspace-sessions-response ") + .action(async (options: { + runId: string; + attemptId: string; + workspacesResponse: string; + workspaceSessionsResponse?: string; + }) => { + const owner = guardCoordinator(environment); + const policy = await readPolicy(requiredPolicyPath(program)); + const listings = parseWorkspaceList( + await readJson(options.workspacesResponse) + ); + const globals = program.opts(); + const result = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const attemptId = safeId(options.attemptId, "attempt ID"); + const decision = reconcileUnknown(run, attemptId, listings); + if (decision.kind === "unresolved") { + return decision; + } + if (decision.kind === "ambiguous") { + await store.save(decision.run); + return decision; + } + if (options.workspaceSessionsResponse === undefined) { + throw new CliError( + "--workspace-sessions-response is required for one exact workspace" + ); + } + const worker = attempt(run, attemptId); + const reconciled = parseWorkspaceSessions( + decision.workspaceId, + await readJson(options.workspaceSessionsResponse), + targetForAttempt(policy, worker) + ); + const next = adoptUnknown( + run, + attemptId, + reconciled.ids, + reconciled.observation + ); + await store.save(next); + return { kind: "adopted", worker: attempt(next, attemptId) }; + }); + emit(io, result); + }); + + attemptCommand + .command("dispatched") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--messages-before ") + .action(async (options: { + runId: string; + attemptId: string; + messagesBefore: string; + }) => { + const owner = guardCoordinator(environment); + const transcript = parseTranscript(await readJson(options.messagesBefore)); + const globals = program.opts(); + const worker = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const next = recordDispatch( + run, + safeId(options.attemptId, "attempt ID"), + transcript.lastMessageId + ); + await store.save(next); + return attempt(next, options.attemptId); + }); + emit(io, worker); + }); + + attemptCommand + .command("observe") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--session-response ") + .requiredOption("--status-response ") + .requiredOption("--messages-response ") + .action(async (options: { + runId: string; + attemptId: string; + sessionResponse: string; + statusResponse: string; + messagesResponse: string; + }) => { + const owner = guardCoordinator(environment); + const policy = await readPolicy(requiredPolicyPath(program)); + const globals = program.opts(); + const result = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const attemptId = safeId(options.attemptId, "attempt ID"); + const worker = attempt(run, attemptId); + if (worker.state !== "queued" && worker.state !== "working") { + throw new CliError(`attempt ${attemptId} is not active`); + } + const ids = requireAttemptIds(worker); + const observed = parseObservedSession( + await readJson(options.sessionResponse), + ids.sessionId + ); + const status = parseSessionStatus(await readJson(options.statusResponse)); + verifyObservationIds(worker, status.workspaceId, status.sessionId); + const cursor = + worker.state === "queued" + ? worker.dispatch.state === "sent" + ? worker.dispatch.transcriptCursorBeforeDispatch + : null + : worker.state === "working" + ? worker.transcriptCursorBeforeDispatch + : null; + if (worker.state === "queued" && worker.dispatch.state !== "sent") { + throw new CliError(`attempt ${attemptId} has not been dispatched`); + } + const transcript = parseTranscript( + await readJson(options.messagesResponse), + cursor + ); + for (const message of transcript.messages) { + if (message.sessionId !== ids.sessionId) { + throw new CliError("transcript message belongs to another session"); + } + } + const observation = completionObservation( + status, + transcript, + cursor, + attemptId + ); + if (observation.kind === "waiting") { + return { outcome: "waiting" }; + } + if (observation.kind === "working") { + if (worker.state === "queued") { + const next = recordWorking(run, attemptId); + await store.save(next); + } + return { outcome: "working" }; + } + if (observation.kind === "dropout") { + const next = recordDropout(run, attemptId, observation.error); + await store.save(next); + return { outcome: "dropout", error: observation.error }; + } + validateReceipt( + targetForAttempt(policy, worker), + worker.request, + worker.postCreateSession, + observed + ); + if (observation.result.status === "dropout") { + const next = recordDropout(run, attemptId, observation.result.summary); + await store.save(next); + return { outcome: "dropout", result: observation.result }; + } + const next = recordComplete( + run, + attemptId, + observed, + observation.message.id + ); + await store.save(next); + return { outcome: "complete", result: observation.result }; + }); + emit(io, result); + }); + + attemptCommand + .command("follow-up-plan") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .action(async (options: { runId: string; attemptId: string }) => { + const owner = guardCoordinator(environment); + const globals = program.opts(); + const delivery = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const next = planFollowUp( + run, + safeId(options.attemptId, "attempt ID") + ); + await store.save(next); + return attempt(next, options.attemptId).followUps.at(-1); + }); + if (delivery === undefined) { + throw new CliError("follow-up planning produced no delivery"); + } + emit(io, delivery); + }); + + attemptCommand + .command("follow-up-sent") + .requiredOption("--run-id ") + .requiredOption("--attempt-id ") + .requiredOption("--message-id ") + .action(async (options: { + runId: string; + attemptId: string; + messageId: string; + }) => { + const owner = guardCoordinator(environment); + const globals = program.opts(); + const worker = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + assertCoordinator(run, owner); + const next = recordFollowUpSent( + run, + safeId(options.attemptId, "attempt ID"), + safeId(options.messageId, "message ID") + ); + await store.save(next); + return attempt(next, options.attemptId); + }); + emit(io, worker); + }); + + program + .command("cleanup") + .command("targets") + .requiredOption("--run-id ") + .requiredOption("--workspaces-response ") + .action(async (options: { runId: string; workspacesResponse: string }) => { + const listings = parseWorkspaceList( + await readJson(options.workspacesResponse) + ); + const names = new Map( + listings.map((workspace) => [ + workspace.workspaceId, + workspace.workspaceName, + ]) + ); + const globals = program.opts(); + const targets = await withStore(globals.store, async (store) => { + const run = await store.load(safeId(options.runId, "run ID")); + return cleanupTargets(run).filter( + (target) => + names.get(target.workspaceId) === target.expectedWorkspaceName + ); + }); + emit(io, { targets }); + }); + + return program; +} + +export async function main( + argv: readonly string[], + environment: CliEnvironment = process.env, + io: CliIo = defaultIo +): Promise { + try { + await buildProgram(environment, io).parseAsync([...argv], { from: "user" }); + return 0; + } catch (error) { + if (error instanceof CommanderError && error.exitCode === 0) { + return 0; + } + const message = error instanceof Error ? error.message : String(error); + io.stderr(`error: ${message}\n`); + return 64; + } +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/README.md b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/README.md new file mode 100644 index 0000000..9845852 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/README.md @@ -0,0 +1,9 @@ +# Conductor contract fixtures + +These redacted fixtures mirror Conductor's published `/v0` OpenAPI response +objects and MCP documentation. They are contract fixtures, not evidence from an +authenticated acceptance run. Replace or extend them only after comparing the +exact release candidate with live MCP responses from both coordinator harnesses. + +`models.json` follows the documented `list_models` semantics; that MCP-only +operation is not present in the public `/v0` OpenAPI document. diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/create-workspace.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/create-workspace.json new file mode 100644 index 0000000..dd974c4 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/create-workspace.json @@ -0,0 +1,5 @@ +{ + "workspaceId": "worker-workspace", + "sessionId": "worker-session", + "deepLink": "https://example.invalid/workspaces/worker-workspace" +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/messages.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/messages.json new file mode 100644 index 0000000..559493a --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/messages.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "id": "message-1", + "sessionId": "worker-session", + "sessionIndex": 1, + "type": "assistant", + "content": "\n{\"attemptId\":\"attempt-1\",\"status\":\"complete\",\"summary\":\"fixture complete\",\"evidence\":[\"fixture:ok\"],\"changedFiles\":[]}\n", + "receivedAt": "2026-09-04T00:00:00Z" + } + ], + "offset": 0, + "hasMore": false +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/models.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/models.json new file mode 100644 index 0000000..67c15bf --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/models.json @@ -0,0 +1,39 @@ +{ + "agents": [ + { + "agent": "claude", + "models": [ + { + "id": "fable-5-1", + "efforts": ["high"], + "supportsFastMode": true + }, + { + "id": "opus-5-1m", + "efforts": ["high"], + "supportsFastMode": true + } + ] + }, + { + "agent": "codex", + "models": [ + { + "id": "gpt-5.6-sol", + "efforts": ["xhigh"], + "supportsFastMode": true + } + ] + }, + { + "agent": "cursor", + "models": [ + { + "id": "grok-4.6", + "efforts": ["high", "xhigh"], + "supportsFastMode": false + } + ] + } + ] +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/policy.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/policy.json new file mode 100644 index 0000000..69c94fe --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/policy.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "mode": "conductor", + "lanes": { + "judgment": { + "agent": "claude", + "model": "fable-5-1", + "resolvedModel": "fable-5-1", + "effort": "high", + "fastMode": false + }, + "hard-review": { + "agent": "claude", + "model": "opus-5-1m", + "resolvedModel": "opus-5-1m", + "effort": "high", + "fastMode": false + }, + "implementation": { + "agent": "codex", + "model": "gpt-5.6-sol", + "resolvedModel": "gpt-5.6-sol", + "effort": "xhigh", + "fastMode": false + }, + "exploration": { + "agent": "cursor", + "model": "grok-4.6", + "resolvedModel": "grok-4.6", + "effort": "xhigh", + "fastMode": false + } + }, + "roles": { + "feature": ["exploration"], + "refactoring": ["exploration"], + "bug-fix": ["implementation"], + "perf-issue": ["implementation"], + "hillclimb": ["implementation"], + "judgment": ["judgment"], + "prose": ["judgment"], + "hardest-tasks": ["judgment"], + "how-explorer": ["exploration"], + "how-explainer": ["judgment"], + "how-critics": ["judgment", "implementation", "exploration", "hard-review"], + "why": "coordinator", + "reflect": "coordinator", + "arena-runners": ["judgment", "implementation", "exploration", "hard-review"], + "arena-cross-judge-pool": ["judgment", "implementation", "exploration", "hard-review"], + "swarm-workers": ["exploration"], + "architect-runners": ["judgment", "implementation", "exploration", "hard-review"], + "interrogate-reviewers": ["judgment", "implementation", "exploration", "hard-review"] + }, + "budget": { + "maxWorkspaces": 5, + "maxConcurrentWorkspaces": 4, + "maxAttempts": 6, + "maxFollowUpsPerAttempt": 2 + } +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/session.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/session.json new file mode 100644 index 0000000..c815ba7 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/session.json @@ -0,0 +1,8 @@ +{ + "id": "worker-session", + "deepLink": "https://example.invalid/sessions/worker-session", + "model": "grok-4.6", + "resolvedModel": "grok-4.6", + "effort": "xhigh", + "fastMode": false +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/status-error.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/status-error.json new file mode 100644 index 0000000..834dee1 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/status-error.json @@ -0,0 +1,8 @@ +{ + "workspaceId": "worker-workspace", + "sessionId": "worker-session", + "status": "error", + "updatedAt": "2026-09-04T00:00:00Z", + "lastError": "fixture worker failure", + "lastErrorAt": "2026-09-04T00:00:00Z" +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/status-idle.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/status-idle.json new file mode 100644 index 0000000..5677641 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/status-idle.json @@ -0,0 +1,6 @@ +{ + "workspaceId": "worker-workspace", + "sessionId": "worker-session", + "status": "idle", + "updatedAt": "2026-09-04T00:00:00Z" +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/whoami.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/whoami.json new file mode 100644 index 0000000..010753e --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/whoami.json @@ -0,0 +1,8 @@ +{ + "userId": "user-fixture", + "name": "Fixture User", + "email": "fixture@example.invalid", + "organizationId": "organization-fixture", + "workspaceId": "workspace-fixture", + "authMethod": "access-jwt" +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/workspace-sessions.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/workspace-sessions.json new file mode 100644 index 0000000..1966b35 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/workspace-sessions.json @@ -0,0 +1,14 @@ +{ + "data": [ + { + "id": "worker-session", + "deepLink": "https://example.invalid/sessions/worker-session", + "model": "grok-4.6", + "resolvedModel": "grok-4.6", + "effort": "xhigh", + "fastMode": false + } + ], + "offset": 0, + "hasMore": false +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/workspaces.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/workspaces.json new file mode 100644 index 0000000..ed3d223 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/fixtures/workspaces.json @@ -0,0 +1,15 @@ +{ + "data": [ + { + "id": "worker-workspace", + "projectId": "project-fixture", + "name": "poteto-run-1-attempt-1", + "state": "ready", + "repoUrl": "https://example.invalid/repo.git", + "createdAt": "2026-09-04T00:00:00Z", + "deepLink": "https://example.invalid/workspaces/worker-workspace" + } + ], + "offset": 0, + "hasMore": false +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/policy.test.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/policy.test.ts new file mode 100644 index 0000000..fd05fcb --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/policy.test.ts @@ -0,0 +1,244 @@ +import { describe, expect, test } from "bun:test"; + +import { + PolicyError, + parsePolicy, + resolveRole, + validateCatalog, +} from "./policy.ts"; +import type { CatalogEntry } from "./types.ts"; + +function validPolicy(): unknown { + return { + schemaVersion: 1, + mode: "conductor", + lanes: { + judgment: { + agent: "claude", + model: "fable-5-1", + resolvedModel: "fable-5-1", + effort: "high", + fastMode: false, + }, + "hard-review": { + agent: "claude", + model: "opus-5-1m", + resolvedModel: "opus-5-1m", + effort: "high", + fastMode: false, + }, + implementation: { + agent: "codex", + model: "gpt-5.6-sol", + resolvedModel: "gpt-5.6-sol", + effort: "xhigh", + fastMode: false, + }, + exploration: { + agent: "cursor", + model: "grok-4.6", + resolvedModel: "grok-4.6", + effort: "xhigh", + fastMode: false, + }, + }, + roles: { + feature: ["exploration"], + refactoring: ["exploration"], + "bug-fix": ["implementation"], + "perf-issue": ["implementation"], + hillclimb: ["implementation"], + judgment: ["judgment"], + prose: ["judgment"], + "hardest-tasks": ["judgment"], + "how-explorer": ["exploration"], + "how-explainer": ["judgment"], + "how-critics": [ + "judgment", + "implementation", + "exploration", + "hard-review", + ], + why: "coordinator", + reflect: "coordinator", + "arena-runners": [ + "judgment", + "implementation", + "exploration", + "hard-review", + ], + "arena-cross-judge-pool": [ + "judgment", + "implementation", + "exploration", + "hard-review", + ], + "swarm-workers": ["exploration"], + "architect-runners": [ + "judgment", + "implementation", + "exploration", + "hard-review", + ], + "interrogate-reviewers": [ + "judgment", + "implementation", + "exploration", + "hard-review", + ], + }, + budget: { + maxWorkspaces: 5, + maxConcurrentWorkspaces: 4, + maxAttempts: 6, + maxFollowUpsPerAttempt: 2, + }, + }; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function policyRecord(): Record { + const value = structuredClone(validPolicy()); + if (!isRecord(value)) { + throw new Error("valid policy fixture must be an object"); + } + return value; +} + +const exactCatalog: readonly CatalogEntry[] = [ + { + agent: "claude", + model: "fable-5-1", + efforts: ["high"], + supportsFastMode: true, + }, + { + agent: "claude", + model: "opus-5-1m", + efforts: ["high"], + supportsFastMode: true, + }, + { + agent: "codex", + model: "gpt-5.6-sol", + efforts: ["xhigh"], + supportsFastMode: true, + }, + { + agent: "cursor", + model: "grok-4.6", + efforts: ["low", "medium", "high", "xhigh"], + supportsFastMode: true, + }, +]; + +describe("Conductor policy", () => { + test("parses every documented role and exact project ceiling", () => { + const policy = parsePolicy(validPolicy()); + + expect(resolveRole(policy, "bug-fix")).toEqual(["implementation"]); + expect(resolveRole(policy, "why")).toBe("coordinator"); + expect(policy.budget).toEqual({ + maxWorkspaces: 5, + maxConcurrentWorkspaces: 4, + maxAttempts: 6, + maxFollowUpsPerAttempt: 2, + }); + }); + + test("rejects an unknown top-level key", () => { + const value = policyRecord(); + value.fallback = "auto"; + + expect(() => parsePolicy(value)).toThrow("unknown policy key: fallback"); + }); + + test("rejects a missing documented role", () => { + const value = policyRecord(); + const roles = value.roles; + if (!isRecord(roles)) { + throw new Error("roles fixture must be an object"); + } + delete roles.feature; + + expect(() => parsePolicy(value)).toThrow("missing policy role: feature"); + }); + + test("rejects Cursor max effort before catalog validation", () => { + const value = policyRecord(); + const lanes = value.lanes; + if (!isRecord(lanes)) { + throw new Error("lanes fixture must be an object"); + } + const exploration = lanes.exploration; + if (!isRecord(exploration)) { + throw new Error("exploration fixture must be an object"); + } + exploration.effort = "max"; + + expect(() => parsePolicy(value)).toThrow("unsupported effort: max"); + }); + + test("rejects a concurrency ceiling above the workspace ceiling", () => { + const value = policyRecord(); + const budget = value.budget; + if (!isRecord(budget)) { + throw new Error("budget fixture must be an object"); + } + budget.maxConcurrentWorkspaces = 6; + + expect(() => parsePolicy(value)).toThrow( + "maxConcurrentWorkspaces cannot exceed maxWorkspaces" + ); + }); + + test("rejects fast mode and duplicate role lanes", () => { + const fast = policyRecord(); + const fastLanes = fast.lanes; + if (!isRecord(fastLanes)) { + throw new Error("lanes fixture must be an object"); + } + const judgment = fastLanes.judgment; + if (!isRecord(judgment)) { + throw new Error("judgment fixture must be an object"); + } + judgment.fastMode = true; + expect(() => parsePolicy(fast)).toThrow("fastMode must be false"); + + const duplicate = policyRecord(); + const duplicateRoles = duplicate.roles; + if (!isRecord(duplicateRoles)) { + throw new Error("roles fixture must be an object"); + } + duplicateRoles.feature = ["exploration", "exploration"]; + expect(() => parsePolicy(duplicate)).toThrow( + "policy role feature repeats lane exploration" + ); + }); + + test("validates every exact agent, model, and effort against the live catalog", () => { + const policy = parsePolicy(validPolicy()); + + expect(() => validateCatalog(policy, exactCatalog)).not.toThrow(); + expect(() => validateCatalog(policy, exactCatalog.slice(0, 3))).toThrow( + "unavailable Conductor target: cursor:grok-4.6@xhigh" + ); + const lowerEffort: readonly CatalogEntry[] = exactCatalog.map((entry) => + entry.agent === "cursor" ? { ...entry, efforts: ["high"] } : entry + ); + expect(() => validateCatalog(policy, lowerEffort)).toThrow( + "unavailable Conductor target: cursor:grok-4.6@xhigh" + ); + }); + + test("rejects an unknown role at dispatch", () => { + const policy = parsePolicy(validPolicy()); + expect(() => resolveRole(policy, "surprise")).toThrow(PolicyError); + expect(() => resolveRole(policy, "surprise")).toThrow( + "unknown Conductor role: surprise" + ); + }); +}); diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/policy.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/policy.ts new file mode 100644 index 0000000..124ba1e --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/policy.ts @@ -0,0 +1,292 @@ +import { readFile } from "node:fs/promises"; + +import type { + CatalogEntry, + ConductorPolicy, + Effort, + LaneName, + ModelTarget, + RoleRoute, + RunBudget, + WorkerAgent, +} from "./types.ts"; + +const LANE_NAMES: readonly LaneName[] = [ + "judgment", + "hard-review", + "implementation", + "exploration", +]; + +const ROLE_NAMES: readonly string[] = [ + "feature", + "refactoring", + "bug-fix", + "perf-issue", + "hillclimb", + "judgment", + "prose", + "hardest-tasks", + "how-explorer", + "how-explainer", + "how-critics", + "why", + "reflect", + "arena-runners", + "arena-cross-judge-pool", + "swarm-workers", + "architect-runners", + "interrogate-reviewers", +]; + +const EFFORTS: readonly Effort[] = ["low", "medium", "high", "xhigh"]; + +export class PolicyError extends Error { + override readonly name = "PolicyError"; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function record(value: unknown, label: string): Record { + if (!isRecord(value)) { + throw new PolicyError(`${label} must be an object`); + } + return value; +} + +function exactKeys( + value: Record, + expected: readonly string[], + label: string +): void { + const expectedKeys = new Set(expected); + for (const key of Object.keys(value)) { + if (!expectedKeys.has(key)) { + throw new PolicyError(`unknown ${label} key: ${key}`); + } + } + for (const key of expected) { + if (!(key in value)) { + throw new PolicyError(`missing ${label} key: ${key}`); + } + } +} + +function nonEmptyString(value: unknown, label: string): string { + if (typeof value !== "string" || value.trim().length === 0) { + throw new PolicyError(`${label} must be a non-empty string`); + } + return value; +} + +function parseAgent(value: unknown): WorkerAgent { + if (value === "claude" || value === "codex" || value === "cursor") { + return value; + } + throw new PolicyError(`unsupported worker agent: ${String(value)}`); +} + +function parseEffort(value: unknown): Effort { + if (typeof value !== "string") { + throw new PolicyError(`unsupported effort: ${String(value)}`); + } + if ( + value === "low" || + value === "medium" || + value === "high" || + value === "xhigh" + ) { + return value; + } + throw new PolicyError(`unsupported effort: ${value}`); +} + +function parseLaneName(value: unknown, label: string): LaneName { + if (typeof value !== "string") { + throw new PolicyError(`${label} must name a policy lane`); + } + if ( + value === "judgment" || + value === "hard-review" || + value === "implementation" || + value === "exploration" + ) { + return value; + } + throw new PolicyError(`${label} names unknown lane ${value}`); +} + +function parseTarget(value: unknown, lane: LaneName): ModelTarget { + const target = record(value, `policy lane ${lane}`); + exactKeys( + target, + ["agent", "model", "resolvedModel", "effort", "fastMode"], + `policy lane ${lane}` + ); + if (target.fastMode !== false) { + throw new PolicyError("fastMode must be false"); + } + return { + agent: parseAgent(target.agent), + model: nonEmptyString(target.model, `${lane}.model`), + resolvedModel: nonEmptyString( + target.resolvedModel, + `${lane}.resolvedModel` + ), + effort: parseEffort(target.effort), + fastMode: false, + }; +} + +function positiveInteger(value: unknown, label: string): number { + if (!Number.isSafeInteger(value) || typeof value !== "number" || value < 1) { + throw new PolicyError(`${label} must be a positive integer`); + } + return value; +} + +function parseBudget(value: unknown): RunBudget { + const budget = record(value, "policy budget"); + exactKeys( + budget, + [ + "maxWorkspaces", + "maxConcurrentWorkspaces", + "maxAttempts", + "maxFollowUpsPerAttempt", + ], + "policy budget" + ); + const result: RunBudget = { + maxWorkspaces: positiveInteger(budget.maxWorkspaces, "maxWorkspaces"), + maxConcurrentWorkspaces: positiveInteger( + budget.maxConcurrentWorkspaces, + "maxConcurrentWorkspaces" + ), + maxAttempts: positiveInteger(budget.maxAttempts, "maxAttempts"), + maxFollowUpsPerAttempt: positiveInteger( + budget.maxFollowUpsPerAttempt, + "maxFollowUpsPerAttempt" + ), + }; + if (result.maxConcurrentWorkspaces > result.maxWorkspaces) { + throw new PolicyError( + "maxConcurrentWorkspaces cannot exceed maxWorkspaces" + ); + } + return result; +} + +function parseRole(value: unknown, role: string): RoleRoute { + if (value === "coordinator") { + return value; + } + if (!Array.isArray(value) || value.length === 0) { + throw new PolicyError( + `policy role ${role} must be coordinator or a non-empty lane list` + ); + } + const lanes: LaneName[] = []; + for (const item of value) { + const lane = parseLaneName(item, `policy role ${role}`); + if (lanes.includes(lane)) { + throw new PolicyError(`policy role ${role} repeats lane ${lane}`); + } + lanes.push(lane); + } + return lanes; +} + +export function parsePolicy(value: unknown): ConductorPolicy { + const policy = record(value, "policy"); + exactKeys(policy, ["schemaVersion", "mode", "lanes", "roles", "budget"], "policy"); + if (policy.schemaVersion !== 1) { + throw new PolicyError("policy schemaVersion must be 1"); + } + if (policy.mode !== "conductor") { + throw new PolicyError("policy mode must be conductor"); + } + + const laneValues = record(policy.lanes, "policy lanes"); + exactKeys(laneValues, LANE_NAMES, "policy lane"); + const lanes: Record = { + judgment: parseTarget(laneValues.judgment, "judgment"), + "hard-review": parseTarget(laneValues["hard-review"], "hard-review"), + implementation: parseTarget(laneValues.implementation, "implementation"), + exploration: parseTarget(laneValues.exploration, "exploration"), + }; + + const seenTargets = new Set(); + for (const target of Object.values(lanes)) { + const key = `${target.agent}:${target.model}`; + if (seenTargets.has(key)) { + throw new PolicyError(`duplicate policy target: ${key}`); + } + seenTargets.add(key); + } + + const roleValues = record(policy.roles, "policy roles"); + const knownRoles = new Set(ROLE_NAMES); + for (const role of Object.keys(roleValues)) { + if (!knownRoles.has(role)) { + throw new PolicyError(`unknown policy role: ${role}`); + } + } + const roles: Record = {}; + for (const role of ROLE_NAMES) { + if (!(role in roleValues)) { + throw new PolicyError(`missing policy role: ${role}`); + } + roles[role] = parseRole(roleValues[role], role); + } + + return { + schemaVersion: 1, + mode: "conductor", + lanes, + roles, + budget: parseBudget(policy.budget), + }; +} + +export async function readPolicy(path: string): Promise { + const source = await readFile(path, "utf8"); + let value: unknown; + try { + value = JSON.parse(source); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new PolicyError(`invalid Conductor policy JSON: ${message}`); + } + return parsePolicy(value); +} + +export function resolveRole( + policy: ConductorPolicy, + role: string +): RoleRoute { + const route = policy.roles[role]; + if (route === undefined) { + throw new PolicyError(`unknown Conductor role: ${role}`); + } + return route; +} + +export function validateCatalog( + policy: ConductorPolicy, + entries: readonly CatalogEntry[] +): void { + for (const target of Object.values(policy.lanes)) { + const entry = entries.find( + (candidate) => + candidate.agent === target.agent && candidate.model === target.model + ); + if (entry === undefined || !entry.efforts.includes(target.effort)) { + throw new PolicyError( + `unavailable Conductor target: ${target.agent}:${target.model}@${target.effort}` + ); + } + } +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/prompt.test.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/prompt.test.ts new file mode 100644 index 0000000..aee12a6 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/prompt.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, test } from "bun:test"; + +import { parseWorkerResult, renderWorkerPrompt } from "./prompt.ts"; +import type { + ModelTarget, + TranscriptMessage, + WorkerBrief, + WorkerRequest, +} from "./types.ts"; + +const target: ModelTarget = { + agent: "cursor", + model: "grok-4.6", + resolvedModel: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +const request: WorkerRequest = { + attemptId: "attempt-1", + role: "feature", + purpose: "write", + baseBranch: "origin/main", + workspaceName: "poteto-run-1-attempt-1", + agent: "cursor", + model: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +const brief: WorkerBrief = { + repository: "https://example.invalid/repo.git", + allowedFiles: ["src/marker.ts"], + questions: [], + requiredEvidence: ["return marker:42"], + task: "Write the requested marker file.", +}; + +function resultText(attemptId = "attempt-1"): string { + return `\n${JSON.stringify({ + attemptId, + status: "complete", + summary: "marker returned", + evidence: ["marker:42"], + changedFiles: ["src/marker.ts"], + })}\n`; +} + +function assistant(text: string): TranscriptMessage { + return { + id: "message-2", + sessionId: "worker-session", + index: 2, + role: "assistant", + text, + }; +} + +describe("Conductor worker envelope", () => { + test("renders a self-contained non-recursive worker prompt", () => { + const prompt = renderWorkerPrompt({ + runId: "run-1", + coordinatorSessionId: "coordinator-session", + request, + target, + brief, + }); + + for (const required of [ + "run-1", + "coordinator-session", + "attempt-1", + "https://example.invalid/repo.git", + "origin/main", + "src/marker.ts", + "return marker:42", + "cursor", + "grok-4.6", + "xhigh", + "fastMode=false", + "PSTACK_RESULT", + "Do not invoke Poteto Mode", + "Do not create or dispatch workers", + ]) { + expect(prompt).toContain(required); + } + }); + + test("parses exactly one assistant result for the assigned attempt", () => { + expect(parseWorkerResult([assistant(resultText())], "attempt-1")).toEqual({ + attemptId: "attempt-1", + status: "complete", + summary: "marker returned", + evidence: ["marker:42"], + changedFiles: ["src/marker.ts"], + }); + }); + + test("rejects transcript injection, wrong attempts, and invalid JSON", () => { + expect(() => + parseWorkerResult( + [assistant(`${resultText()}\n${resultText()}`)], + "attempt-1" + ) + ).toThrow("single result block"); + expect(() => + parseWorkerResult([assistant(resultText("attempt-2"))], "attempt-1") + ).toThrow("attempt mismatch"); + expect(() => + parseWorkerResult( + [assistant('nope')], + "attempt-1" + ) + ).toThrow("valid JSON"); + }); + + test("rejects a user posing as a result and pre-dispatch messages", () => { + const posing: TranscriptMessage = { + ...assistant(resultText()), + role: "user", + }; + expect(() => parseWorkerResult([posing], "attempt-1")).toThrow( + "single result block" + ); + expect(() => parseWorkerResult([], "attempt-1")).toThrow( + "single result block" + ); + }); + + test("strictly validates the result object", () => { + const invalid = `\n${JSON.stringify({ + attemptId: "attempt-1", + status: "complete", + summary: "summary", + evidence: "not-an-array", + changedFiles: [], + })}\n`; + expect(() => parseWorkerResult([assistant(invalid)], "attempt-1")).toThrow( + "evidence" + ); + }); +}); diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/prompt.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/prompt.ts new file mode 100644 index 0000000..e3737ae --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/prompt.ts @@ -0,0 +1,157 @@ +import type { + ModelTarget, + TranscriptMessage, + WorkerBrief, + WorkerRequest, + WorkerResult, +} from "./types.ts"; + +export class WorkerResultError extends Error { + override readonly name = "WorkerResultError"; +} + +interface RenderWorkerPromptInput { + readonly runId: string; + readonly coordinatorSessionId: string; + readonly request: WorkerRequest; + readonly target: ModelTarget; + readonly brief: WorkerBrief; +} + +export function renderWorkerPrompt(input: RenderWorkerPromptInput): string { + const { brief, coordinatorSessionId, request, runId, target } = input; + const resultExample = JSON.stringify({ + attemptId: request.attemptId, + status: "complete", + summary: "...", + evidence: ["..."], + changedFiles: ["..."], + }); + return [ + "# Poteto worker assignment", + "", + `Run ID: ${runId}`, + `Coordinator session: ${coordinatorSessionId}`, + `Attempt ID: ${request.attemptId}`, + `Role: ${request.role}`, + `Purpose: ${request.purpose}`, + `Repository: ${brief.repository}`, + `Base branch: ${request.baseBranch}`, + `Workspace name: ${request.workspaceName}`, + `Requested runtime: agent=${target.agent}, model=${target.model}, resolvedModel=${target.resolvedModel}, effort=${target.effort}, fastMode=false`, + "", + "## Task", + "", + brief.task, + "", + "Allowed files:", + ...brief.allowedFiles.map((path) => `- ${path}`), + "", + "Questions to answer:", + ...(brief.questions.length === 0 + ? ["- None."] + : brief.questions.map((question) => `- ${question}`)), + "", + "Required evidence:", + ...brief.requiredEvidence.map((item) => `- ${item}`), + "", + "Do not invoke Poteto Mode. Do not create or dispatch workers.", + "Work only inside this isolated workspace and return the result to the coordinator.", + "End with exactly one assistant result block in this form:", + "", + ``, + resultExample, + "", + ].join("\n"); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stringArray(value: unknown, label: string): readonly string[] { + if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) { + throw new WorkerResultError(`${label} must be a string array`); + } + return value; +} + +function parseResultObject(value: unknown): WorkerResult { + if (!isRecord(value)) { + throw new WorkerResultError("worker result must be an object"); + } + const expected = new Set([ + "attemptId", + "status", + "summary", + "evidence", + "changedFiles", + ]); + for (const key of Object.keys(value)) { + if (!expected.has(key)) { + throw new WorkerResultError(`worker result has unknown key ${key}`); + } + } + for (const key of expected) { + if (!(key in value)) { + throw new WorkerResultError(`worker result is missing ${key}`); + } + } + if (typeof value.attemptId !== "string" || value.attemptId.length === 0) { + throw new WorkerResultError("worker result attemptId is invalid"); + } + if (value.status !== "complete" && value.status !== "dropout") { + throw new WorkerResultError("worker result status is invalid"); + } + if (typeof value.summary !== "string" || value.summary.length === 0) { + throw new WorkerResultError("worker result summary is invalid"); + } + return { + attemptId: value.attemptId, + status: value.status, + summary: value.summary, + evidence: stringArray(value.evidence, "worker result evidence"), + changedFiles: stringArray( + value.changedFiles, + "worker result changedFiles" + ), + }; +} + +export function parseWorkerResult( + messages: readonly TranscriptMessage[], + expectedAttemptId: string +): WorkerResult { + const matches: { readonly attemptId: string; readonly body: string }[] = []; + const pattern = /\s*([\s\S]*?)\s*<\/PSTACK_RESULT>/g; + for (const message of messages) { + if (message.role !== "assistant") { + continue; + } + for (const match of message.text.matchAll(pattern)) { + const attemptId = match[1]; + const body = match[2]; + if (attemptId !== undefined && body !== undefined) { + matches.push({ attemptId, body }); + } + } + } + if (matches.length !== 1) { + throw new WorkerResultError("expected a single result block"); + } + const match = matches[0]; + if (match === undefined || match.attemptId !== expectedAttemptId) { + throw new WorkerResultError("worker result attempt mismatch"); + } + let decoded: unknown; + try { + decoded = JSON.parse(match.body); + } catch { + throw new WorkerResultError("worker result must contain valid JSON"); + } + const result = parseResultObject(decoded); + if (result.attemptId !== expectedAttemptId) { + throw new WorkerResultError("worker result attempt mismatch"); + } + return result; +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/pstack-conductor b/plugins/pstack/skills/poteto-mode/scripts/conductor/pstack-conductor new file mode 100755 index 0000000..582e93b --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/pstack-conductor @@ -0,0 +1,5 @@ +#!/usr/bin/env bun + +const { main } = await import("./cli.ts"); + +process.exitCode = await main(process.argv.slice(2)); diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/store.test.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/store.test.ts new file mode 100644 index 0000000..83c1cec --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/store.test.ts @@ -0,0 +1,472 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + RunStateError, + adoptUnknown, + assertCoordinator, + cleanupTargets, + createRun, + markCreating, + markUnknown, + openRunStore, + planAttempt, + planFollowUp, + reconcileUnknown, + recordComplete, + recordCreated, + recordDispatch, + recordDropout, + recordFollowUpSent, + recordWorking, +} from "./store.ts"; +import type { + ModelTarget, + ObservedSession, + PlanAttemptInput, + PotetoRun, + RunBudget, + WorkspaceListing, +} from "./types.ts"; + +const directories: string[] = []; + +afterEach(async () => { + await Promise.all( + directories.splice(0).map((directory) => + rm(directory, { recursive: true, force: true }) + ) + ); +}); + +const target: ModelTarget = { + agent: "cursor", + model: "grok-4.6", + resolvedModel: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +const budget: RunBudget = { + maxWorkspaces: 5, + maxConcurrentWorkspaces: 4, + maxAttempts: 6, + maxFollowUpsPerAttempt: 2, +}; + +function runBudget(overrides: Partial = {}): RunBudget { + return { ...budget, ...overrides }; +} + +function freshRun(overrides: Partial = {}): PotetoRun { + return createRun({ + runId: "run-1", + coordinator: { + workspaceId: "coordinator-workspace", + sessionId: "coordinator-session", + agent: "codex", + model: "gpt-5.6-sol", + }, + budget: runBudget(overrides), + }); +} + +function attemptInput(index: number): PlanAttemptInput { + return { + attemptId: `attempt-${index}`, + role: "feature", + purpose: "write", + baseBranch: "origin/main", + target, + dispatchMessageId: `dispatch-${index}`, + }; +} + +function plan(run: PotetoRun, count: number): PotetoRun { + let next = run; + for (let index = 1; index <= count; index += 1) { + next = planAttempt(next, attemptInput(index)); + } + return next; +} + +function creating(run: PotetoRun, count: number): PotetoRun { + let next = plan(run, count); + for (let index = 1; index <= count; index += 1) { + next = markCreating(next, `attempt-${index}`); + } + return next; +} + +const postCreate: ObservedSession = { + model: "grok-4.6", + resolvedModel: "grok-4.6", + effort: "xhigh", + fastMode: false, +}; + +describe("Conductor run state", () => { + test("binds one coordinator and rejects ownership transfer", () => { + const run = freshRun(); + + expect(run.coordinator.sessionId).toBe("coordinator-session"); + expect(() => assertCoordinator(run, "other-session")).toThrow( + "session other-session cannot take ownership from coordinator-session" + ); + expect(() => assertCoordinator(run, "coordinator-session")).not.toThrow(); + expect(() => + createRun({ + runId: "run-1", + coordinator: { + workspaceId: "coordinator-workspace", + sessionId: "coordinator-session", + agent: "claude", + model: "fable-5-1", + }, + budget, + existing: run, + }) + ).toThrow("persisted run coordinator does not match"); + }); + + test("derives a unique workspace name from the run and attempt", () => { + const run = planAttempt(freshRun(), attemptInput(1)); + + expect(run.workers[0]?.request.workspaceName).toBe( + "poteto-run-1-attempt-1" + ); + expect(run.workers[0]?.state).toBe("planned"); + }); + + test("enforces lifetime attempt and workspace budgets", () => { + const attempts = plan(freshRun({ maxAttempts: 2 }), 2); + expect(() => planAttempt(attempts, attemptInput(3))).toThrow( + "attempt budget exhausted" + ); + + let workspaces = plan(freshRun({ maxWorkspaces: 2 }), 3); + workspaces = markCreating(workspaces, "attempt-1"); + workspaces = markUnknown(workspaces, "attempt-1", "lost response"); + workspaces = markCreating(workspaces, "attempt-2"); + expect(() => markCreating(workspaces, "attempt-3")).toThrow( + "workspace budget exhausted" + ); + }); + + test("enforces concurrent workspace budget", () => { + let run = plan( + freshRun({ maxWorkspaces: 3, maxConcurrentWorkspaces: 2 }), + 3 + ); + run = markCreating(run, "attempt-1"); + run = markCreating(run, "attempt-2"); + + expect(() => markCreating(run, "attempt-3")).toThrow( + "concurrency budget exhausted" + ); + }); + + test("counts an uncertain creation against the concurrent workspace budget", () => { + let run = plan( + freshRun({ maxWorkspaces: 3, maxConcurrentWorkspaces: 2 }), + 3 + ); + run = markUnknown( + markCreating(run, "attempt-1"), + "attempt-1", + "create response was lost" + ); + run = markCreating(run, "attempt-2"); + + expect(() => markCreating(run, "attempt-3")).toThrow( + "concurrency budget exhausted" + ); + }); + + test("requires creating before accepting workspace IDs", () => { + const run = plan(freshRun(), 1); + + expect(() => + recordCreated( + run, + "attempt-1", + { workspaceId: "worker-workspace", sessionId: "worker-session" }, + postCreate + ) + ).toThrow("attempt attempt-1 must be creating"); + }); + + test("keeps uncertain creation unknown until one exact name is found", () => { + const run = markUnknown( + markCreating(plan(freshRun(), 1), "attempt-1"), + "attempt-1", + "lost response" + ); + const wrong: WorkspaceListing = { + workspaceId: "wrong-workspace", + workspaceName: "another-name", + }; + const exact: WorkspaceListing = { + workspaceId: "worker-workspace", + workspaceName: "poteto-run-1-attempt-1", + }; + + expect(reconcileUnknown(run, "attempt-1", []).kind).toBe("unresolved"); + expect(reconcileUnknown(run, "attempt-1", [wrong]).kind).toBe( + "unresolved" + ); + const decision = reconcileUnknown(run, "attempt-1", [wrong, exact]); + expect(decision.kind).toBe("adopt-workspace"); + if (decision.kind !== "adopt-workspace") { + throw new Error("exact workspace should be adopted"); + } + expect( + adoptUnknown( + run, + "attempt-1", + { workspaceId: decision.workspaceId, sessionId: "worker-session" }, + postCreate + ).workers[0]?.state + ).toBe("queued"); + }); + + test("records every duplicate exact-name candidate and fails closed", () => { + const run = markUnknown( + markCreating(plan(freshRun(), 1), "attempt-1"), + "attempt-1", + "lost response" + ); + const candidates: readonly WorkspaceListing[] = [ + { + workspaceId: "worker-a", + workspaceName: "poteto-run-1-attempt-1", + }, + { + workspaceId: "worker-b", + workspaceName: "poteto-run-1-attempt-1", + }, + ]; + + const decision = reconcileUnknown(run, "attempt-1", candidates); + expect(decision.kind).toBe("ambiguous"); + if (decision.kind !== "ambiguous") { + throw new Error("duplicate workspaces should fail closed"); + } + expect(decision.run.workers[0]).toMatchObject({ + state: "unknown", + candidateWorkspaceIds: ["worker-a", "worker-b"], + }); + }); + + test("persists a stable follow-up ID before delivery", () => { + let run = markCreating(plan(freshRun(), 1), "attempt-1"); + run = recordCreated( + run, + "attempt-1", + { workspaceId: "worker-workspace", sessionId: "worker-session" }, + postCreate + ); + run = planFollowUp(run, "attempt-1"); + const retry = planFollowUp(run, "attempt-1"); + + expect(retry).toEqual(run); + expect(retry.workers[0]?.followUps).toEqual([ + { + messageId: "poteto-run-1-attempt-1-followup-1", + state: "planned", + }, + ]); + const sent = recordFollowUpSent( + retry, + "attempt-1", + "poteto-run-1-attempt-1-followup-1" + ); + const second = planFollowUp(sent, "attempt-1"); + expect(second.workers[0]?.followUps).toHaveLength(2); + const secondSent = recordFollowUpSent( + second, + "attempt-1", + "poteto-run-1-attempt-1-followup-2" + ); + expect(() => planFollowUp(secondSent, "attempt-1")).toThrow( + "follow-up budget exhausted" + ); + }); + + test("requires a dispatch cursor and completes from queued or working", () => { + let run = markCreating(plan(freshRun(), 1), "attempt-1"); + run = recordCreated( + run, + "attempt-1", + { workspaceId: "worker-workspace", sessionId: "worker-session" }, + postCreate + ); + expect(() => + recordComplete(run, "attempt-1", postCreate, "result-message") + ).toThrow("dispatch cursor"); + + run = recordDispatch(run, "attempt-1", "message-before-dispatch"); + run = recordWorking(run, "attempt-1"); + run = recordComplete(run, "attempt-1", postCreate, "result-message"); + + expect(run.status).toBe("complete"); + expect(run.workers[0]).toMatchObject({ + state: "complete", + resultMessageId: "result-message", + observedSession: postCreate, + }); + expect(() => markCreating(run, "attempt-1")).toThrow( + "attempt attempt-1 must be planned" + ); + }); + + test("accepts a recorded null cursor when the pre-dispatch transcript is empty", () => { + let run = markCreating(plan(freshRun(), 1), "attempt-1"); + run = recordCreated( + run, + "attempt-1", + { workspaceId: "worker-workspace", sessionId: "worker-session" }, + postCreate + ); + run = recordDispatch(run, "attempt-1", null); + + expect( + recordComplete(run, "attempt-1", postCreate, "result-message").status + ).toBe("complete"); + }); + + test("makes dispatch recording idempotent only for the same cursor", () => { + let run = markCreating(plan(freshRun(), 1), "attempt-1"); + run = recordCreated( + run, + "attempt-1", + { workspaceId: "worker-workspace", sessionId: "worker-session" }, + postCreate + ); + run = recordDispatch(run, "attempt-1", "message-1"); + + expect(recordDispatch(run, "attempt-1", "message-1")).toEqual(run); + expect(() => recordDispatch(run, "attempt-1", "message-2")).toThrow( + "different dispatch cursor" + ); + }); + + test("records a receipt-bearing dropout without inventing worker IDs", () => { + const run = recordDropout( + plan(freshRun(), 1), + "attempt-1", + "Conductor authentication failed" + ); + + expect(run.status).toBe("complete"); + expect(run.workers[0]).toEqual({ + state: "dropout", + request: expect.any(Object), + dispatchMessageId: "dispatch-1", + followUps: [], + ids: null, + error: "Conductor authentication failed", + }); + }); + + test("cleanup returns recorded workers and never the coordinator", () => { + let run = markCreating(plan(freshRun(), 1), "attempt-1"); + run = recordCreated( + run, + "attempt-1", + { workspaceId: "worker-workspace", sessionId: "worker-session" }, + postCreate + ); + + expect(cleanupTargets(run)).toEqual([ + { + attemptId: "attempt-1", + workspaceId: "worker-workspace", + sessionId: "worker-session", + expectedWorkspaceName: "poteto-run-1-attempt-1", + }, + ]); + expect(cleanupTargets(run)).not.toContainEqual( + expect.objectContaining({ workspaceId: "coordinator-workspace" }) + ); + }); + + test("writes one atomic JSON file and restores it after reopen", async () => { + const directory = await mkdtemp(join(tmpdir(), "pstack-conductor-test-")); + directories.push(directory); + const store = openRunStore(directory); + const run = plan(freshRun(), 1); + + await store.save(run); + await store.close(); + const source = await readFile(join(directory, "run-1.json"), "utf8"); + const reopened = openRunStore(directory); + expect(await reopened.load("run-1")).toEqual(run); + await reopened.close(); + expect(JSON.parse(source)).toEqual(run); + }); + + test("rejects malformed persisted state at the storage boundary", async () => { + const directory = await mkdtemp(join(tmpdir(), "pstack-conductor-test-")); + directories.push(directory); + await writeFile( + join(directory, "run-1.json"), + JSON.stringify({ schemaVersion: 1, runId: "run-1", workers: "wrong" }) + ); + const store = openRunStore(directory); + + await expect(store.load("run-1")).rejects.toThrow( + "invalid persisted Conductor run" + ); + await store.close(); + }); + + test("blocks a live writer and accepts the store after lock release", async () => { + const directory = await mkdtemp(join(tmpdir(), "pstack-conductor-test-")); + directories.push(directory); + const first = openRunStore(directory); + const second = openRunStore(directory); + const run = freshRun(); + + await first.save(run); + await expect(second.save(run)).rejects.toThrow("run store is locked"); + await first.close(); + await second.save(run); + await second.close(); + }); + + test("replaces a lock whose recorded process is dead", async () => { + const directory = await mkdtemp(join(tmpdir(), "pstack-conductor-test-")); + directories.push(directory); + await writeFile(join(directory, ".conductor.lock"), "999999999\n"); + const store = openRunStore(directory); + + await store.save(freshRun()); + expect(await store.load("run-1")).toEqual(freshRun()); + await store.close(); + }); + + test("refuses a duplicate attempt ID and an increased resume budget", () => { + const run = plan(freshRun(), 1); + expect(() => planAttempt(run, attemptInput(1))).toThrow( + "duplicate attempt ID: attempt-1" + ); + expect( + () => + createRun({ + runId: run.runId, + coordinator: run.coordinator, + budget: { ...run.budget, maxAttempts: 7 }, + existing: run, + }) + ).toThrow("persisted run budget cannot increase"); + }); + + test("rejects invalid source states", () => { + expect(() => markUnknown(freshRun(), "missing", "error")).toThrow( + RunStateError + ); + }); +}); diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/store.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/store.ts new file mode 100644 index 0000000..ba972a6 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/store.ts @@ -0,0 +1,1080 @@ +import { randomUUID } from "node:crypto"; +import { + mkdir, + open, + readFile, + readdir, + rename, + rm, + unlink, + writeFile, +} from "node:fs/promises"; +import { dirname, join } from "node:path"; + +import type { + AttemptBase, + CleanupTarget, + CoordinatorAgent, + Effort, + FollowUpDelivery, + ObservedSession, + PlanAttemptInput, + PotetoRun, + ReconcileDecision, + RunBudget, + StartRunInput, + WorkerAgent, + WorkerAttempt, + WorkerIds, + WorkerRequest, + WorkspaceListing, +} from "./types.ts"; + +const LOCK_FILE = ".conductor.lock"; + +export class RunStateError extends Error { + override readonly name = "RunStateError"; +} + +function increasedBudget(next: RunBudget, current: RunBudget): boolean { + return ( + next.maxWorkspaces > current.maxWorkspaces || + next.maxConcurrentWorkspaces > current.maxConcurrentWorkspaces || + next.maxAttempts > current.maxAttempts || + next.maxFollowUpsPerAttempt > current.maxFollowUpsPerAttempt + ); +} + +export function createRun(input: StartRunInput): PotetoRun { + if (input.existing !== undefined) { + if ( + input.existing.runId !== input.runId || + input.existing.coordinator.sessionId !== input.coordinator.sessionId || + input.existing.coordinator.workspaceId !== input.coordinator.workspaceId || + input.existing.coordinator.agent !== input.coordinator.agent || + input.existing.coordinator.model !== input.coordinator.model + ) { + throw new RunStateError("persisted run coordinator does not match"); + } + if (increasedBudget(input.budget, input.existing.budget)) { + throw new RunStateError("persisted run budget cannot increase"); + } + return input.existing; + } + return { + schemaVersion: 1, + runId: input.runId, + status: "active", + coordinator: input.coordinator, + budget: input.budget, + workspaceCreationCount: 0, + workers: [], + }; +} + +export function assertCoordinator(run: PotetoRun, sessionId: string): void { + if (run.coordinator.sessionId !== sessionId) { + throw new RunStateError( + `session ${sessionId} cannot take ownership from ${run.coordinator.sessionId}` + ); + } +} + +function attemptIndex(run: PotetoRun, attemptId: string): number { + const index = run.workers.findIndex( + (attempt) => attempt.request.attemptId === attemptId + ); + if (index < 0) { + throw new RunStateError(`unknown attempt ID: ${attemptId}`); + } + return index; +} + +function replaceAttempt( + run: PotetoRun, + attemptId: string, + update: (attempt: WorkerAttempt) => WorkerAttempt +): PotetoRun { + const index = attemptIndex(run, attemptId); + return { + ...run, + workers: run.workers.map((attempt, current) => + current === index ? update(attempt) : attempt + ), + }; +} + +function activeWorkspaceCount(run: PotetoRun): number { + return run.workers.filter( + (attempt) => + attempt.state === "creating" || + attempt.state === "unknown" || + attempt.state === "queued" || + attempt.state === "working" + ).length; +} + +function completeWhenTerminal(run: PotetoRun): PotetoRun { + if ( + run.workers.length > 0 && + run.workers.every( + (attempt) => + attempt.state === "complete" || + attempt.state === "dropout" || + attempt.state === "cancelled" + ) + ) { + return { ...run, status: "complete" }; + } + return run; +} + +export function planAttempt( + run: PotetoRun, + input: PlanAttemptInput +): PotetoRun { + if (run.workers.some((item) => item.request.attemptId === input.attemptId)) { + throw new RunStateError(`duplicate attempt ID: ${input.attemptId}`); + } + if (run.workers.length >= run.budget.maxAttempts) { + throw new RunStateError("attempt budget exhausted"); + } + const request: WorkerRequest = { + attemptId: input.attemptId, + role: input.role, + purpose: input.purpose, + baseBranch: input.baseBranch, + workspaceName: `poteto-${run.runId}-${input.attemptId}`, + agent: input.target.agent, + model: input.target.model, + effort: input.target.effort, + fastMode: false, + }; + return { + ...run, + workers: [ + ...run.workers, + { + state: "planned", + request, + dispatchMessageId: input.dispatchMessageId, + followUps: [], + }, + ], + }; +} + +export function markCreating(run: PotetoRun, attemptId: string): PotetoRun { + if (run.workspaceCreationCount >= run.budget.maxWorkspaces) { + throw new RunStateError("workspace budget exhausted"); + } + if (activeWorkspaceCount(run) >= run.budget.maxConcurrentWorkspaces) { + throw new RunStateError("concurrency budget exhausted"); + } + const next = replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "planned") { + throw new RunStateError(`attempt ${attemptId} must be planned`); + } + return { ...attempt, state: "creating" }; + }); + return { ...next, workspaceCreationCount: next.workspaceCreationCount + 1 }; +} + +function queuedAttempt({ + attempt, + ids, + postCreateSession, +}: { + attempt: AttemptBase; + ids: WorkerIds; + postCreateSession: ObservedSession; +}): WorkerAttempt { + return { + ...attempt, + state: "queued", + ids, + postCreateSession, + dispatch: { state: "pending" }, + }; +} + +export function recordCreated( + run: PotetoRun, + attemptId: string, + ids: WorkerIds, + postCreateSession: ObservedSession +): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "creating") { + throw new RunStateError(`attempt ${attemptId} must be creating`); + } + return queuedAttempt({ attempt, ids, postCreateSession }); + }); +} + +export function markUnknown( + run: PotetoRun, + attemptId: string, + error: string +): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "creating") { + throw new RunStateError(`attempt ${attemptId} must be creating`); + } + return { + ...attempt, + state: "unknown", + candidateWorkspaceIds: [], + error, + }; + }); +} + +export function reconcileUnknown( + run: PotetoRun, + attemptId: string, + candidates: readonly WorkspaceListing[] +): ReconcileDecision { + const attempt = run.workers[attemptIndex(run, attemptId)]; + if (attempt?.state !== "unknown") { + throw new RunStateError(`attempt ${attemptId} must be unknown`); + } + const matches = candidates.filter( + (candidate) => candidate.workspaceName === attempt.request.workspaceName + ); + if (matches.length === 0) { + return { kind: "unresolved", run }; + } + if (matches.length === 1) { + const match = matches[0]; + if (match === undefined) { + throw new RunStateError("workspace reconciliation lost its exact match"); + } + return { + kind: "adopt-workspace", + run, + workspaceId: match.workspaceId, + }; + } + const candidateWorkspaceIds = matches.map((match) => match.workspaceId); + const updated = replaceAttempt(run, attemptId, (current) => { + if (current.state !== "unknown") { + throw new RunStateError(`attempt ${attemptId} must be unknown`); + } + return { + ...current, + candidateWorkspaceIds, + error: "multiple workspaces share the attempt name", + }; + }); + return { + kind: "ambiguous", + run: updated, + candidateWorkspaceIds, + }; +} + +export function adoptUnknown( + run: PotetoRun, + attemptId: string, + ids: WorkerIds, + postCreateSession: ObservedSession +): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "unknown") { + throw new RunStateError(`attempt ${attemptId} must be unknown`); + } + return queuedAttempt({ attempt, ids, postCreateSession }); + }); +} + +export function recordDispatch( + run: PotetoRun, + attemptId: string, + transcriptCursorBeforeDispatch: string | null +): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "queued") { + throw new RunStateError(`attempt ${attemptId} must be queued`); + } + if (attempt.dispatch.state === "sent") { + if ( + attempt.dispatch.transcriptCursorBeforeDispatch !== + transcriptCursorBeforeDispatch + ) { + throw new RunStateError( + `attempt ${attemptId} has a different dispatch cursor` + ); + } + return attempt; + } + return { + ...attempt, + dispatch: { state: "sent", transcriptCursorBeforeDispatch }, + }; + }); +} + +export function recordWorking(run: PotetoRun, attemptId: string): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "queued") { + throw new RunStateError(`attempt ${attemptId} must be queued`); + } + if (attempt.dispatch.state !== "sent") { + throw new RunStateError(`attempt ${attemptId} has no dispatch cursor`); + } + return { + request: attempt.request, + dispatchMessageId: attempt.dispatchMessageId, + followUps: attempt.followUps, + state: "working", + ids: attempt.ids, + postCreateSession: attempt.postCreateSession, + transcriptCursorBeforeDispatch: + attempt.dispatch.transcriptCursorBeforeDispatch, + }; + }); +} + +export function recordComplete( + run: PotetoRun, + attemptId: string, + observedSession: ObservedSession, + resultMessageId: string +): PotetoRun { + const next = replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "queued" && attempt.state !== "working") { + throw new RunStateError(`attempt ${attemptId} must be queued or working`); + } + if (attempt.state === "queued" && attempt.dispatch.state !== "sent") { + throw new RunStateError(`attempt ${attemptId} has no dispatch cursor`); + } + return { + request: attempt.request, + dispatchMessageId: attempt.dispatchMessageId, + followUps: attempt.followUps, + state: "complete", + ids: attempt.ids, + postCreateSession: attempt.postCreateSession, + observedSession, + resultMessageId, + }; + }); + return completeWhenTerminal(next); +} + +export function recordDropout( + run: PotetoRun, + attemptId: string, + error: string +): PotetoRun { + const next = replaceAttempt(run, attemptId, (attempt) => { + if ( + attempt.state === "complete" || + attempt.state === "dropout" || + attempt.state === "cancelled" + ) { + throw new RunStateError(`attempt ${attemptId} is already terminal`); + } + const ids = + attempt.state === "queued" || attempt.state === "working" + ? attempt.ids + : null; + return { + request: attempt.request, + dispatchMessageId: attempt.dispatchMessageId, + followUps: attempt.followUps, + state: "dropout", + ids, + error, + }; + }); + return completeWhenTerminal(next); +} + +export function planFollowUp(run: PotetoRun, attemptId: string): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "queued" && attempt.state !== "working") { + throw new RunStateError(`attempt ${attemptId} cannot receive a follow-up`); + } + const pending = attempt.followUps.at(-1); + if (pending?.state === "planned") { + return attempt; + } + if (attempt.followUps.length >= run.budget.maxFollowUpsPerAttempt) { + throw new RunStateError("follow-up budget exhausted"); + } + const delivery: FollowUpDelivery = { + messageId: `poteto-${run.runId}-${attemptId}-followup-${attempt.followUps.length + 1}`, + state: "planned", + }; + return { ...attempt, followUps: [...attempt.followUps, delivery] }; + }); +} + +export function recordFollowUpSent( + run: PotetoRun, + attemptId: string, + messageId: string +): PotetoRun { + return replaceAttempt(run, attemptId, (attempt) => { + if (attempt.state !== "queued" && attempt.state !== "working") { + throw new RunStateError(`attempt ${attemptId} cannot receive a follow-up`); + } + const last = attempt.followUps.at(-1); + if (last?.state !== "planned" || last.messageId !== messageId) { + throw new RunStateError(`follow-up ${messageId} is not pending`); + } + const followUps = attempt.followUps.map( + (delivery): FollowUpDelivery => + delivery.messageId === messageId + ? { ...delivery, state: "sent" } + : delivery + ); + return { ...attempt, followUps }; + }); +} + +export function cleanupTargets(run: PotetoRun): readonly CleanupTarget[] { + const targets: CleanupTarget[] = []; + for (const attempt of run.workers) { + if ( + attempt.state !== "queued" && + attempt.state !== "working" && + attempt.state !== "complete" && + attempt.state !== "dropout" && + attempt.state !== "cancelled" + ) { + continue; + } + if (attempt.ids === null) { + continue; + } + if (attempt.ids.workspaceId === run.coordinator.workspaceId) { + throw new RunStateError("worker record points at the coordinator workspace"); + } + targets.push({ + attemptId: attempt.request.attemptId, + workspaceId: attempt.ids.workspaceId, + sessionId: attempt.ids.sessionId, + expectedWorkspaceName: attempt.request.workspaceName, + }); + } + return targets; +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function invalidPersisted(detail: string): never { + throw new RunStateError(`invalid persisted Conductor run: ${detail}`); +} + +function exactKeys( + value: Record, + expected: readonly string[], + label: string +): void { + const keys = Object.keys(value); + const wanted = new Set(expected); + for (const key of keys) { + if (!wanted.has(key)) { + invalidPersisted(`${label} has unknown key ${key}`); + } + } + for (const key of expected) { + if (!(key in value)) { + invalidPersisted(`${label} is missing ${key}`); + } + } +} + +function persistedRecord( + value: unknown, + label: string +): Record { + if (!isRecord(value)) { + invalidPersisted(`${label} must be an object`); + } + return value; +} + +function persistedString(value: unknown, label: string): string { + if (typeof value !== "string" || value.length === 0) { + invalidPersisted(`${label} must be a non-empty string`); + } + return value; +} + +function persistedInteger( + value: unknown, + label: string, + minimum: number +): number { + if ( + typeof value !== "number" || + !Number.isSafeInteger(value) || + value < minimum + ) { + invalidPersisted(`${label} must be an integer of at least ${minimum}`); + } + return value; +} + +function persistedCoordinatorAgent(value: unknown): CoordinatorAgent { + if (value === "claude" || value === "codex") { + return value; + } + return invalidPersisted("coordinator agent must be claude or codex"); +} + +function persistedWorkerAgent(value: unknown): WorkerAgent { + if (value === "claude" || value === "codex" || value === "cursor") { + return value; + } + return invalidPersisted("worker agent is unsupported"); +} + +function persistedEffort(value: unknown): Effort { + if ( + value === "low" || + value === "medium" || + value === "high" || + value === "xhigh" + ) { + return value; + } + return invalidPersisted("worker effort is unsupported"); +} + +function persistedBudget(value: unknown): RunBudget { + const budget = persistedRecord(value, "budget"); + exactKeys( + budget, + [ + "maxWorkspaces", + "maxConcurrentWorkspaces", + "maxAttempts", + "maxFollowUpsPerAttempt", + ], + "budget" + ); + const parsed: RunBudget = { + maxWorkspaces: persistedInteger(budget.maxWorkspaces, "maxWorkspaces", 1), + maxConcurrentWorkspaces: persistedInteger( + budget.maxConcurrentWorkspaces, + "maxConcurrentWorkspaces", + 1 + ), + maxAttempts: persistedInteger(budget.maxAttempts, "maxAttempts", 1), + maxFollowUpsPerAttempt: persistedInteger( + budget.maxFollowUpsPerAttempt, + "maxFollowUpsPerAttempt", + 1 + ), + }; + if (parsed.maxConcurrentWorkspaces > parsed.maxWorkspaces) { + invalidPersisted("concurrency budget exceeds workspace budget"); + } + return parsed; +} + +function persistedCoordinator(value: unknown): PotetoRun["coordinator"] { + const coordinator = persistedRecord(value, "coordinator"); + exactKeys( + coordinator, + ["workspaceId", "sessionId", "agent", "model"], + "coordinator" + ); + return { + workspaceId: persistedString( + coordinator.workspaceId, + "coordinator.workspaceId" + ), + sessionId: persistedString( + coordinator.sessionId, + "coordinator.sessionId" + ), + agent: persistedCoordinatorAgent(coordinator.agent), + model: persistedString(coordinator.model, "coordinator.model"), + }; +} + +function persistedIds(value: unknown, label: string): WorkerIds { + const ids = persistedRecord(value, label); + exactKeys(ids, ["workspaceId", "sessionId"], label); + return { + workspaceId: persistedString(ids.workspaceId, `${label}.workspaceId`), + sessionId: persistedString(ids.sessionId, `${label}.sessionId`), + }; +} + +function persistedObservation( + value: unknown, + label: string +): ObservedSession { + const observation = persistedRecord(value, label); + exactKeys( + observation, + ["model", "resolvedModel", "effort", "fastMode"], + label + ); + if (typeof observation.fastMode !== "boolean") { + invalidPersisted(`${label}.fastMode must be boolean`); + } + return { + model: persistedString(observation.model, `${label}.model`), + resolvedModel: persistedString( + observation.resolvedModel, + `${label}.resolvedModel` + ), + effort: persistedString(observation.effort, `${label}.effort`), + fastMode: observation.fastMode, + }; +} + +function persistedRequest(value: unknown): WorkerRequest { + const request = persistedRecord(value, "worker request"); + exactKeys( + request, + [ + "attemptId", + "role", + "purpose", + "baseBranch", + "workspaceName", + "agent", + "model", + "effort", + "fastMode", + ], + "worker request" + ); + if (request.purpose !== "review" && request.purpose !== "write") { + invalidPersisted("worker purpose must be review or write"); + } + if (request.fastMode !== false) { + invalidPersisted("worker fastMode must be false"); + } + return { + attemptId: persistedString(request.attemptId, "worker attemptId"), + role: persistedString(request.role, "worker role"), + purpose: request.purpose, + baseBranch: persistedString(request.baseBranch, "worker baseBranch"), + workspaceName: persistedString( + request.workspaceName, + "worker workspaceName" + ), + agent: persistedWorkerAgent(request.agent), + model: persistedString(request.model, "worker model"), + effort: persistedEffort(request.effort), + fastMode: false, + }; +} + +function persistedFollowUps(value: unknown): readonly FollowUpDelivery[] { + if (!Array.isArray(value)) { + invalidPersisted("worker followUps must be an array"); + } + return value.map((item, index) => { + const delivery = persistedRecord(item, `followUp ${index}`); + exactKeys(delivery, ["messageId", "state"], `followUp ${index}`); + if (delivery.state !== "planned" && delivery.state !== "sent") { + invalidPersisted(`followUp ${index}.state is unsupported`); + } + return { + messageId: persistedString( + delivery.messageId, + `followUp ${index}.messageId` + ), + state: delivery.state, + }; + }); +} + +function persistedBase(value: Record): AttemptBase { + return { + request: persistedRequest(value.request), + dispatchMessageId: persistedString( + value.dispatchMessageId, + "dispatchMessageId" + ), + followUps: persistedFollowUps(value.followUps), + }; +} + +function persistedAttempt(value: unknown, index: number): WorkerAttempt { + const attempt = persistedRecord(value, `worker ${index}`); + const state = attempt.state; + if (state === "planned" || state === "creating") { + exactKeys( + attempt, + ["state", "request", "dispatchMessageId", "followUps"], + `worker ${index}` + ); + return { ...persistedBase(attempt), state }; + } + if (state === "unknown") { + exactKeys( + attempt, + [ + "state", + "request", + "dispatchMessageId", + "followUps", + "candidateWorkspaceIds", + "error", + ], + `worker ${index}` + ); + if ( + !Array.isArray(attempt.candidateWorkspaceIds) || + !attempt.candidateWorkspaceIds.every((item) => typeof item === "string") + ) { + invalidPersisted(`worker ${index}.candidateWorkspaceIds is invalid`); + } + return { + ...persistedBase(attempt), + state, + candidateWorkspaceIds: attempt.candidateWorkspaceIds, + error: persistedString(attempt.error, `worker ${index}.error`), + }; + } + if (state === "queued") { + exactKeys( + attempt, + [ + "state", + "request", + "dispatchMessageId", + "followUps", + "ids", + "postCreateSession", + "dispatch", + ], + `worker ${index}` + ); + const dispatch = persistedRecord(attempt.dispatch, `worker ${index}.dispatch`); + if (dispatch.state === "pending") { + exactKeys(dispatch, ["state"], `worker ${index}.dispatch`); + return { + ...persistedBase(attempt), + state, + ids: persistedIds(attempt.ids, `worker ${index}.ids`), + postCreateSession: persistedObservation( + attempt.postCreateSession, + `worker ${index}.postCreateSession` + ), + dispatch: { state: "pending" }, + }; + } + if (dispatch.state === "sent") { + exactKeys( + dispatch, + ["state", "transcriptCursorBeforeDispatch"], + `worker ${index}.dispatch` + ); + const cursor = dispatch.transcriptCursorBeforeDispatch; + if (cursor !== null && typeof cursor !== "string") { + invalidPersisted(`worker ${index}.dispatch cursor is invalid`); + } + return { + ...persistedBase(attempt), + state, + ids: persistedIds(attempt.ids, `worker ${index}.ids`), + postCreateSession: persistedObservation( + attempt.postCreateSession, + `worker ${index}.postCreateSession` + ), + dispatch: { + state: "sent", + transcriptCursorBeforeDispatch: cursor, + }, + }; + } + return invalidPersisted(`worker ${index}.dispatch state is unsupported`); + } + if (state === "working") { + exactKeys( + attempt, + [ + "state", + "request", + "dispatchMessageId", + "followUps", + "ids", + "postCreateSession", + "transcriptCursorBeforeDispatch", + ], + `worker ${index}` + ); + const cursor = attempt.transcriptCursorBeforeDispatch; + if (cursor !== null && typeof cursor !== "string") { + invalidPersisted(`worker ${index}.transcript cursor is invalid`); + } + return { + ...persistedBase(attempt), + state, + ids: persistedIds(attempt.ids, `worker ${index}.ids`), + postCreateSession: persistedObservation( + attempt.postCreateSession, + `worker ${index}.postCreateSession` + ), + transcriptCursorBeforeDispatch: cursor, + }; + } + if (state === "complete") { + exactKeys( + attempt, + [ + "state", + "request", + "dispatchMessageId", + "followUps", + "ids", + "postCreateSession", + "observedSession", + "resultMessageId", + ], + `worker ${index}` + ); + return { + ...persistedBase(attempt), + state, + ids: persistedIds(attempt.ids, `worker ${index}.ids`), + postCreateSession: persistedObservation( + attempt.postCreateSession, + `worker ${index}.postCreateSession` + ), + observedSession: persistedObservation( + attempt.observedSession, + `worker ${index}.observedSession` + ), + resultMessageId: persistedString( + attempt.resultMessageId, + `worker ${index}.resultMessageId` + ), + }; + } + if (state === "dropout" || state === "cancelled") { + exactKeys( + attempt, + [ + "state", + "request", + "dispatchMessageId", + "followUps", + "ids", + "error", + ], + `worker ${index}` + ); + return { + ...persistedBase(attempt), + state, + ids: + attempt.ids === null + ? null + : persistedIds(attempt.ids, `worker ${index}.ids`), + error: persistedString(attempt.error, `worker ${index}.error`), + }; + } + return invalidPersisted(`worker ${index}.state is unsupported`); +} + +function persistedStatus(value: unknown): PotetoRun["status"] { + if ( + value === "active" || + value === "complete" || + value === "cancelled" || + value === "needs-cleanup" + ) { + return value; + } + return invalidPersisted("run status is unsupported"); +} + +function parsePersistedRun(value: unknown): PotetoRun { + const run = persistedRecord(value, "run"); + exactKeys( + run, + [ + "schemaVersion", + "runId", + "status", + "coordinator", + "budget", + "workspaceCreationCount", + "workers", + ], + "run" + ); + if (run.schemaVersion !== 1) { + invalidPersisted("schemaVersion must be 1"); + } + if (!Array.isArray(run.workers)) { + invalidPersisted("workers must be an array"); + } + const budget = persistedBudget(run.budget); + const workspaceCreationCount = persistedInteger( + run.workspaceCreationCount, + "workspaceCreationCount", + 0 + ); + const workers = run.workers.map(persistedAttempt); + if (workers.length > budget.maxAttempts) { + invalidPersisted("worker count exceeds attempt budget"); + } + if (workspaceCreationCount > budget.maxWorkspaces) { + invalidPersisted("workspace count exceeds workspace budget"); + } + const attemptIds = workers.map((attempt) => attempt.request.attemptId); + if (new Set(attemptIds).size !== attemptIds.length) { + invalidPersisted("attempt IDs must be unique"); + } + return { + schemaVersion: 1, + runId: persistedString(run.runId, "runId"), + status: persistedStatus(run.status), + coordinator: persistedCoordinator(run.coordinator), + budget, + workspaceCreationCount, + workers, + }; +} + +async function atomicWrite(path: string, contents: string): Promise { + await mkdir(dirname(path), { recursive: true }); + const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`; + try { + const handle = await open(temporary, "wx", 0o600); + try { + await handle.writeFile(contents, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + await rename(temporary, path); + } finally { + await rm(temporary, { force: true }); + } +} + +export interface RunStore { + readonly load: (runId: string) => Promise; + readonly save: (run: PotetoRun) => Promise; + readonly list: () => Promise; + readonly close: () => Promise; +} + +export function openRunStore(directory: string): RunStore { + const lockPath = join(directory, LOCK_FILE); + let locked = false; + let closed = false; + + async function recordedOwnerIsDead(): Promise { + let contents: string; + try { + contents = await readFile(lockPath, "utf8"); + } catch { + return false; + } + const owner = contents.trim(); + if (!/^[1-9]\d*$/.test(owner)) { + return false; + } + const pid = Number(owner); + if (!Number.isSafeInteger(pid)) { + return false; + } + try { + process.kill(pid, 0); + return false; + } catch (error) { + return isRecord(error) && error.code === "ESRCH"; + } + } + + async function acquireLock(): Promise { + try { + await writeFile(lockPath, `${process.pid}\n`, { flag: "wx", mode: 0o600 }); + return; + } catch (error) { + if (!(isRecord(error) && error.code === "EEXIST")) { + throw new RunStateError( + `cannot create Conductor run-store lock: ${lockPath}` + ); + } + } + if (!(await recordedOwnerIsDead())) { + throw new RunStateError(`Conductor run store is locked: ${lockPath}`); + } + await unlink(lockPath).catch(() => undefined); + try { + await writeFile(lockPath, `${process.pid}\n`, { flag: "wx", mode: 0o600 }); + } catch { + throw new RunStateError(`Conductor run store is locked: ${lockPath}`); + } + } + + async function lock(): Promise { + if (closed) { + throw new RunStateError("Conductor run store is closed"); + } + if (locked) { + return; + } + await mkdir(directory, { recursive: true }); + await acquireLock(); + locked = true; + } + + return { + async load(runId) { + await lock(); + try { + const source = await readFile(join(directory, `${runId}.json`), "utf8"); + return parsePersistedRun(JSON.parse(source)); + } catch (error) { + if (error instanceof RunStateError) { + throw error; + } + throw new RunStateError( + `invalid persisted Conductor run: ${error instanceof Error ? error.message : String(error)}` + ); + } + }, + async save(run) { + await lock(); + await atomicWrite( + join(directory, `${run.runId}.json`), + `${JSON.stringify(run, null, 2)}\n` + ); + }, + async list() { + await lock(); + const names = (await readdir(directory)) + .filter((name) => name.endsWith(".json")) + .sort(); + return await Promise.all( + names.map(async (name) => { + const value: unknown = JSON.parse( + await readFile(join(directory, name), "utf8") + ); + return parsePersistedRun(value); + }) + ); + }, + async close() { + if (closed) { + return; + } + closed = true; + if (locked) { + await unlink(lockPath).catch(() => undefined); + locked = false; + } + }, + }; +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/tsconfig.json b/plugins/pstack/skills/poteto-mode/scripts/conductor/tsconfig.json new file mode 100644 index 0000000..477af90 --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "allowImportingTsExtensions": true, + "module": "esnext", + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "strict": true, + "target": "esnext", + "types": ["bun-types"] + }, + "include": ["*.ts"] +} diff --git a/plugins/pstack/skills/poteto-mode/scripts/conductor/types.ts b/plugins/pstack/skills/poteto-mode/scripts/conductor/types.ts new file mode 100644 index 0000000..ffcf43b --- /dev/null +++ b/plugins/pstack/skills/poteto-mode/scripts/conductor/types.ts @@ -0,0 +1,224 @@ +export type CoordinatorAgent = "claude" | "codex"; +export type WorkerAgent = CoordinatorAgent | "cursor"; +export type Effort = "low" | "medium" | "high" | "xhigh"; +export type LaneName = + | "judgment" + | "hard-review" + | "implementation" + | "exploration"; + +export interface ModelTarget { + readonly agent: WorkerAgent; + readonly model: string; + readonly resolvedModel: string; + readonly effort: Effort; + readonly fastMode: false; +} + +export type RoleRoute = "coordinator" | readonly LaneName[]; + +export interface RunBudget { + readonly maxWorkspaces: number; + readonly maxConcurrentWorkspaces: number; + readonly maxAttempts: number; + readonly maxFollowUpsPerAttempt: number; +} + +export interface ConductorPolicy { + readonly schemaVersion: 1; + readonly mode: "conductor"; + readonly lanes: Readonly>; + readonly roles: Readonly>; + readonly budget: RunBudget; +} + +export interface CatalogEntry { + readonly agent: WorkerAgent; + readonly model: string; + readonly efforts: readonly Effort[]; + readonly supportsFastMode: boolean; +} + +export type WorkerPurpose = "review" | "write"; + +export interface Coordinator { + readonly workspaceId: string; + readonly sessionId: string; + readonly agent: CoordinatorAgent; + readonly model: string; +} + +export interface WorkerRequest { + readonly attemptId: string; + readonly role: string; + readonly purpose: WorkerPurpose; + readonly baseBranch: string; + readonly workspaceName: string; + readonly agent: WorkerAgent; + readonly model: string; + readonly effort: Effort; + readonly fastMode: false; +} + +export interface WorkerIds { + readonly workspaceId: string; + readonly sessionId: string; +} + +export interface ObservedSession { + readonly model: string; + readonly resolvedModel: string; + readonly effort: string; + readonly fastMode: boolean; +} + +export interface FollowUpDelivery { + readonly messageId: string; + readonly state: "planned" | "sent"; +} + +export interface AttemptBase { + readonly request: WorkerRequest; + readonly dispatchMessageId: string; + readonly followUps: readonly FollowUpDelivery[]; +} + +export type DispatchRecord = + | { readonly state: "pending" } + | { + readonly state: "sent"; + readonly transcriptCursorBeforeDispatch: string | null; + }; + +export type WorkerAttempt = + | (AttemptBase & { readonly state: "planned" | "creating" }) + | (AttemptBase & { + readonly state: "unknown"; + readonly candidateWorkspaceIds: readonly string[]; + readonly error: string; + }) + | (AttemptBase & { + readonly state: "queued"; + readonly ids: WorkerIds; + readonly postCreateSession: ObservedSession; + readonly dispatch: DispatchRecord; + }) + | (AttemptBase & { + readonly state: "working"; + readonly ids: WorkerIds; + readonly postCreateSession: ObservedSession; + readonly transcriptCursorBeforeDispatch: string | null; + }) + | (AttemptBase & { + readonly state: "complete"; + readonly ids: WorkerIds; + readonly postCreateSession: ObservedSession; + readonly observedSession: ObservedSession; + readonly resultMessageId: string; + }) + | (AttemptBase & { + readonly state: "dropout" | "cancelled"; + readonly ids: WorkerIds | null; + readonly error: string; + }); + +export interface PotetoRun { + readonly schemaVersion: 1; + readonly runId: string; + readonly status: "active" | "complete" | "cancelled" | "needs-cleanup"; + readonly coordinator: Coordinator; + readonly budget: RunBudget; + readonly workspaceCreationCount: number; + readonly workers: readonly WorkerAttempt[]; +} + +export interface StartRunInput { + readonly runId: string; + readonly coordinator: Coordinator; + readonly budget: RunBudget; + readonly existing?: PotetoRun; +} + +export interface PlanAttemptInput { + readonly attemptId: string; + readonly role: string; + readonly purpose: WorkerPurpose; + readonly baseBranch: string; + readonly target: ModelTarget; + readonly dispatchMessageId: string; +} + +export interface CleanupTarget extends WorkerIds { + readonly attemptId: string; + readonly expectedWorkspaceName: string; +} + +export type ReconcileDecision = + | { readonly kind: "unresolved"; readonly run: PotetoRun } + | { + readonly kind: "adopt-workspace"; + readonly run: PotetoRun; + readonly workspaceId: string; + } + | { + readonly kind: "ambiguous"; + readonly run: PotetoRun; + readonly candidateWorkspaceIds: readonly string[]; + }; + +export interface CurrentContext { + readonly sessionId: string; + readonly workspaceId: string; +} + +export interface SessionStatus { + readonly workspaceId: string; + readonly sessionId: string; + readonly status: "idle" | "working" | "error"; + readonly error: string | null; +} + +export interface TranscriptMessage { + readonly id: string; + readonly sessionId: string; + readonly index: number; + readonly role: "assistant" | "user" | "other"; + readonly text: string; +} + +export interface Transcript { + readonly messages: readonly TranscriptMessage[]; + readonly lastMessageId: string | null; + readonly afterCursor: string | null; +} + +export interface WorkspaceListing { + readonly workspaceId: string; + readonly workspaceName: string; +} + +export interface WorkerResult { + readonly attemptId: string; + readonly status: "complete" | "dropout"; + readonly summary: string; + readonly evidence: readonly string[]; + readonly changedFiles: readonly string[]; +} + +export interface WorkerBrief { + readonly repository: string; + readonly allowedFiles: readonly string[]; + readonly questions: readonly string[]; + readonly requiredEvidence: readonly string[]; + readonly task: string; +} + +export type CompletionObservation = + | { readonly kind: "waiting" } + | { readonly kind: "working" } + | { readonly kind: "dropout"; readonly error: string } + | { + readonly kind: "complete"; + readonly message: TranscriptMessage; + readonly result: WorkerResult; + }; diff --git a/plugins/pstack/skills/poteto-mode/scripts/package.json b/plugins/pstack/skills/poteto-mode/scripts/package.json index 069d0bc..1e1fc08 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/package.json +++ b/plugins/pstack/skills/poteto-mode/scripts/package.json @@ -3,8 +3,8 @@ "private": true, "type": "module", "scripts": { - "test": "\"$npm_execpath\" test --parallel bootstrap orch watch-pr runner check-plan", - "typecheck": "tsc --project watch-pr/tsconfig.json --noEmit --strict && tsc --project runner/tsconfig.json --noEmit --strict && tsc --project check-plan.tsconfig.json --noEmit --strict" + "test": "\"$npm_execpath\" test --parallel bootstrap orch watch-pr runner conductor check-plan", + "typecheck": "tsc --project watch-pr/tsconfig.json --noEmit --strict && tsc --project runner/tsconfig.json --noEmit --strict && tsc --project conductor/tsconfig.json --noEmit --strict && tsc --project check-plan.tsconfig.json --noEmit --strict" }, "dependencies": { "commander": "14.0.0" diff --git a/plugins/pstack/skills/reflect/SKILL.md b/plugins/pstack/skills/reflect/SKILL.md index 0968453..4b569a7 100644 --- a/plugins/pstack/skills/reflect/SKILL.md +++ b/plugins/pstack/skills/reflect/SKILL.md @@ -7,7 +7,7 @@ description: Spawn three parallel review subagents over the active transcript, s Mine the current conversation for durable learnings, then route them into skill edits. -**Dispatch contract.** Resolve every configured role through [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md). Reviewers need the parent's live MCP surface, so the default and supported portable route is `inherit-parent` (or its `auto` alias). Pass the transcript or digest plus any required evidence paths. On Codex, resolve remaining Claude tool names via [`codex-tools.md`](../poteto-mode/references/codex-tools.md). +**Dispatch contract.** Follow the parent-selected Poteto dispatch reference. The Conductor project route for `reflect` is `coordinator`, so apply the three lenses and synthesis in the invoking session without creating workers. On the portable route, reviewers use `inherit-parent` or `auto` so they retain the parent's live MCP surface. ## When to invoke @@ -35,7 +35,7 @@ For each candidate, read the first JSONL line and check that `message.content[0] ### 2. Spawn three reviewers in parallel -Start all three read-only lanes in one fan-out phase through provider dispatch. Reviewers need MCP access for context lookups (tickets, chat threads, observability traces referenced in the transcript), so keep them native to the parent. The prompt forbids file writes; the parent applies edits. +In Conductor mode, perform all three read-only lenses in the coordinator session. On the portable route, start all three lanes in one native fan-out. Reviewers need MCP access for context lookups. The prompt forbids file writes; the parent applies edits. | Lens | Model descriptor | Prompt template | |---|---|---| @@ -47,7 +47,7 @@ Pass each template verbatim, substituting the transcript path or digest where ma ### 3. Synthesize -Dispatch one lane using your configured reflect-judgment descriptor (default `inherit-parent`). Preserve relevant MCP access because the synthesizer spot-verifies citations. Use `references/synthesizer.md` verbatim, with each reviewer's full output inlined where marked. The synthesizer returns a structured Accepted / Rejected / Backlog list. +In Conductor mode, synthesize in the coordinator. On the portable route, dispatch one lane using the configured reflect-judgment descriptor, whose default is `inherit-parent`. Preserve relevant MCP access because the synthesizer spot-verifies citations. Use `references/synthesizer.md` verbatim, with each reviewer's full output inlined where marked. The synthesizer returns a structured Accepted / Rejected / Backlog list. ### 4. Structural enforcement check diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 4641881..b6a0f0f 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -5,6 +5,40 @@ description: Configure pstack's provider-qualified models, per-family requested # Setup pstack +## Conductor projects + +Before portable setup, search from the repository root for the nearest +`.conductor/poteto-mode.json`. When its validated `mode` is `conductor`, use +this branch and stop after it succeeds. Do not run the nine portable steps. + +1. Read + [`conductor-dispatch.md`](../poteto-mode/references/conductor-dispatch.md) + and locate its `scripts/conductor/pstack-conductor` helper. +2. Refuse immediately when `PSTACK_WORKER=1` or + `CONDUCTOR_SESSION_ID` is missing. +3. Call `whoami`. Then call `get_session_status` for + `CONDUCTOR_SESSION_ID`. Save both redacted responses in a protected + temporary directory. The helper must prove that the returned session and + workspace match the current Conductor context. +4. Call `list_models` through every page. Run `pstack-conductor policy validate` + with the identity, coordinator status, live catalog, and project + policy. All configured agent, model, effort, and fast-mode pairs must pass + before creating a workspace. +5. Run one minimal read-only marker worker through the full Conductor route. + It receives one isolated workspace and no initial message. Verify both + session receipts, the new assistant result, and unchanged coordinator + files. Cancel if still active and archive only the cleanup target emitted by + the helper. +6. Report the bound coordinator session, exact catalog targets, receipt, and + cleanup result. Authentication, catalog, receipt, or cleanup validation + failures stop setup. There is no native, runner, or model fallback. + +Leave both `~/.claude/pstack-models.md` and `~/.codex/pstack-models.md` +untouched. A Conductor project uses its committed policy as the only model +configuration source. + +## Portable projects + 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. Claude Code writes `~/.claude/pstack-models.md` and loads it from `~/.claude/CLAUDE.md` with: diff --git a/plugins/pstack/skills/swarm/SKILL.md b/plugins/pstack/skills/swarm/SKILL.md index 11807b4..44735e0 100644 --- a/plugins/pstack/skills/swarm/SKILL.md +++ b/plugins/pstack/skills/swarm/SKILL.md @@ -7,7 +7,7 @@ description: "Fan out N parallel workers, drain them, and return one report. Use Fan out N parallel workers. They may cover separate slices, race the same brief, or mix both. The parent waits, aggregates, and returns one report. -**Dispatch contract.** Resolve each worker descriptor through [`provider-dispatch.md`](../poteto-mode/references/provider-dispatch.md). The parent starts native and external lanes; workers never route themselves. On Codex, resolve remaining Claude tool names via [`codex-tools.md`](../poteto-mode/references/codex-tools.md). +**Dispatch contract.** Follow the parent-selected Poteto dispatch reference. Conductor mode resolves `swarm-workers` from project policy and gives every worker an isolated workspace. Provider descriptors, model sheets, native agents, and the launcher apply only to the portable route. Workers never route themselves. ## Start @@ -23,12 +23,12 @@ Open a todolist with one entry per phase before launching anything. 1. State the done predicate and the artifact or report the swarm must return. 2. Choose the shape. Partition into slices, race N workers on identical briefs, or mix both. For a race or mixed shape, declare `first pass`, `rank all`, or `best-of` before spawning. 3. Set N from the user or derive it from the shape. N is total workers, not the number that run at once. -4. Pick the worker descriptor from `swarm workers` in the current harness's pstack model sheet when present. Otherwise use `grok:grok-4.6@xhigh`. For a model race, name each arm's descriptor up front. +4. Pick the worker lane from `swarm-workers` in Conductor project policy. On the portable route, use `swarm workers` from the current harness model sheet when present, otherwise use `grok:grok-4.6@xhigh`. For a model race, name each arm up front. 5. Give each worker its own writable output when it writes. Use a worktree, branch, or `/tmp/swarm-/worker-/`. ## Phase B: Fan out -Start all N workers in one fan-out phase through provider dispatch. Native lanes use background subagents; external lanes invoke the launcher as background work with retained task/session handles. Never use Claude's foreground Bash path for a long worker. Every writer runs in its assigned worktree or output directory. Isolation comes from those paths, not the provider. +Start all N workers in one fan-out phase through the selected dispatch contract. In Conductor mode, each worker is a persisted isolated-workspace attempt. On the portable route, native lanes use background subagents and external lanes invoke the launcher as background work with retained handles. Every writer runs in its assigned worktree or output directory. When a worker must start from a non-default branch, check that branch out in the worker's own worktree and name the worktree path in its brief. diff --git a/tests/conductor-mode-repro.sh b/tests/conductor-mode-repro.sh new file mode 100755 index 0000000..60fd2cc --- /dev/null +++ b/tests/conductor-mode-repro.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SKILL="$ROOT/plugins/pstack/skills/poteto-mode/SKILL.md" +REFERENCE="$ROOT/plugins/pstack/skills/poteto-mode/references/conductor-dispatch.md" +SETUP="$ROOT/plugins/pstack/skills/setup-pstack/SKILL.md" +POTETO_AGENT="$ROOT/plugins/pstack/agents/poteto-agent.md" + +test -f "$REFERENCE" +grep -Fq 'PSTACK_WORKER=1' "$SKILL" +grep -Fq '.conductor/poteto-mode.json' "$SKILL" +grep -Fq 'references/conductor-dispatch.md' "$SKILL" +grep -Fq 'mode` is `conductor' "$REFERENCE" +grep -Fq 'CONDUCTOR_SESSION_ID' "$REFERENCE" +grep -Fq 'one isolated Conductor workspace' "$REFERENCE" +grep -Fq 'whoami' "$REFERENCE" +grep -Fq 'list_models' "$REFERENCE" +grep -Fq 'create_workspace' "$REFERENCE" +grep -Fq 'without an initial message' "$REFERENCE" +grep -Fq 'list_project_workspaces' "$REFERENCE" +grep -Fq 'list_workspace_sessions' "$REFERENCE" +grep -Fq 'get_session' "$REFERENCE" +grep -Fq 'send_message' "$REFERENCE" +grep -Fq 'get_session_status' "$REFERENCE" +grep -Fq 'list_messages' "$REFERENCE" +grep -Fq 'cancel_session' "$REFERENCE" +grep -Fq 'archive_workspace' "$REFERENCE" +grep -Fq 'pstack-conductor' "$REFERENCE" +grep -Fq '.conductor/poteto-mode.json' "$SETUP" +grep -Fq 'pstack-conductor policy validate' "$SETUP" +grep -Fq 'PSTACK_WORKER=1' "$POTETO_AGENT" + +for name in arena architect how interrogate reflect swarm; do + grep -Fq 'parent-selected Poteto dispatch reference' \ + "$ROOT/plugins/pstack/skills/$name/SKILL.md" +done + +if grep -REq --include='*.ts' \ + 'https://api\.conductor\.build|Authorization:|CONDUCTOR_API_KEY' \ + "$ROOT/plugins/pstack/skills/poteto-mode/scripts/conductor"; then + printf 'Conductor helper code must not make network calls or read credentials\n' >&2 + exit 1 +fi + +if grep -Fq 'scripts/runner/pstack-runner' "$REFERENCE"; then + printf 'Conductor dispatch must not invoke the legacy runner\n' >&2 + exit 1 +fi diff --git a/tests/skill-collision-repro.sh b/tests/skill-collision-repro.sh index 6b66608..3b56288 100755 --- a/tests/skill-collision-repro.sh +++ b/tests/skill-collision-repro.sh @@ -37,11 +37,12 @@ verof() { { grep -m1 '"version"' "$1" || true; } | sed -E 's/.*"version"[[:space vc="$(verof "$repo/plugins/pstack/.claude-plugin/plugin.json")" vx="$(verof "$repo/plugins/pstack/.codex-plugin/plugin.json")" vm="$(verof "$repo/.claude-plugin/marketplace.json")" +va="$(verof "$repo/.agents/plugins/marketplace.json")" vu="$(sed -n 's/| open-pstack version | `\([^`]*\)` |/\1/p' "$repo/UPSTREAM.md")" -if [ -n "$vc" ] && [ "$vc" = "$vx" ] && [ "$vc" = "$vm" ] && [ "$vc" = "$vu" ]; then - note "ok: open-pstack version matches across UPSTREAM.md and the 3 manifests ($vc)" +if [ -n "$vc" ] && [ "$vc" = "$vx" ] && [ "$vc" = "$vm" ] && [ "$vc" = "$va" ] && [ "$vc" = "$vu" ]; then + note "ok: open-pstack version matches across UPSTREAM.md and the 4 manifests ($vc)" else - note "FAIL: open-pstack version differs: upstream=$vu claude-plugin=$vc codex-plugin=$vx marketplace=$vm" + note "FAIL: open-pstack version differs: upstream=$vu claude-plugin=$vc codex-plugin=$vx claude-marketplace=$vm codex-marketplace=$va" fail=1 fi @@ -132,6 +133,31 @@ else fi plugin="$repo/plugins/pstack" + +poteto_agent="$plugin/agents/poteto-agent.md" +poteto_agent_front="$(sed -n '2,/^---$/p' "$poteto_agent")" +poteto_preload_bad="" +if [ "$(printf '%s\n' "$poteto_agent_front" | grep -cx 'skills:' || true)" != "1" ]; then + poteto_preload_bad="${poteto_agent} must declare one skills list"$'\n' +fi +if [ "$(printf '%s\n' "$poteto_agent_front" | grep -cx ' - pstack:poteto-mode' || true)" != "1" ]; then + poteto_preload_bad="${poteto_preload_bad}${poteto_agent} must preload pstack:poteto-mode"$'\n' +fi +if [ -n "$poteto_preload_bad" ]; then + note "FAIL: Claude poteto-agent does not preload poteto-mode:" + note "$poteto_preload_bad" + fail=1 +else + note "ok: Claude poteto-agent preloads poteto-mode" +fi + +if ! grep -Fq 'PSTACK_WORKER=1' "$poteto_agent"; then + note "FAIL: Claude poteto-agent does not refuse worker recursion" + fail=1 +else + note "ok: Claude poteto-agent refuses worker recursion" +fi + canon="$plugin/skills/poteto-mode/references/bugbot-triage.md" skill="$plugin/skills/babysit/SKILL.md" playbook="$plugin/skills/poteto-mode/playbooks/babysit.md" @@ -437,4 +463,16 @@ invoke='Call the Skill tool with skill "testplug:foo" exactly once and follow wh check "model-initiated Skill-tool invocation" "SKILL-RAN" "$(run "$invoke")" check "user /testplug:foo invocation" "SKILL-RAN" "$(run '/testplug:foo')" +preloaded_agent_output="$( + claude -p \ + 'Use the Agent tool once with subagent_type pstack:poteto-agent. Give it this task verbatim: "Without invoking Skill or reading files, reply with the first Core principle named in the preloaded poteto-mode skill." Return only the child response.' \ + --plugin-dir "$plugin" \ + --model fable \ + --effort max \ + --max-turns 5 \ + --tools Agent \ + < /dev/null 2>&1 +)" +check "poteto-agent preloaded skill" "Laziness Protocol" "$preloaded_agent_output" + exit "$fail"