From 592fbafefc11be60cccf5be09ef0e133e7c25805 Mon Sep 17 00:00:00 2001 From: Aiden Lee Date: Fri, 6 Nov 2026 09:43:56 -0500 Subject: [PATCH 1/7] feat(t3): port fork PR #254's T3 host dispatch section Squashed net diff of fikaventures' PR #254 at 7bb8f55, applied to main 2d92c53. The only conflict was CHANGES.md placement; the fork's entry sits above the 1.17.0 section. Refs #253 Co-Authored-By: Claude Opus 5.5 --- CHANGES.md | 4 ++++ README.md | 1 + .../skills/poteto-mode/references/codex-tools.md | 2 +- .../poteto-mode/references/provider-dispatch.md | 16 +++++++++++++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7b965163..278d2d80 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,10 @@ Open Pstack lands on green: Mergify merges a ready PR to main once verify and exact-head live-gate pass and no review thread is unresolved, and a workflow resolves Unfret's own threads. A PR that touches `.github/` or the Mergify config also waits for Unfret, and workflow changes require operator queueing. 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. +## T3 host route (#253) + +Provider dispatch gains a T3 host section. In a T3 Code session, pstack still chooses each role's model and effort, and T3's `delegate_task` starts, isolates, and tracks the agents in place of the native, external-runner, and receipt rules. `codex-tools.md` points to it. No skill body changes. + ## 1.18.0 gives external providers writers and adds a Muse Code provider Open Pstack 1.18.0 keeps the Cursor pstack 0.15.36 sync. Default roles and panels are unchanged. diff --git a/README.md b/README.md index 8daa9152..5e15c274 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Compared with Lauren's pstack, Open Pstack adds: - **A model and provider registry.** Models, efforts and providers live in one typed [registry](plugins/pstack/skills/poteto-mode/scripts/runner/registry/). The [model catalog](plugins/pstack/skills/poteto-mode/references/model-catalog.md) and the Claude-native agent files are generated from it, and the skills point at the catalog's role defaults instead of naming models. A new model is one registry entry. A new command-line provider is one adapter module and one line in the provider list. See the [design](docs/plans/2026-10-11-model-provider-registry.md). - **One skill tree for Claude Code and Codex.** Both apps read the same skills, and one [reference](plugins/pstack/skills/poteto-mode/references/codex-tools.md) maps how each app starts them. Lauren's pstack does not run in Codex. See [Claude Code and Codex](#claude-code-and-codex) below. - **Models from other providers.** Claude Code and Codex hand work to other providers' models through each provider's own command-line tool, started by one [runner](plugins/pstack/skills/poteto-mode/references/provider-dispatch.md). The runner supports nine providers. Claude, Codex and Grok run lanes that read or write files. Devin, DeepSeek Harness, Antigravity, OpenCode, Amp and Cursor CLI run read-only lanes. The model catalog's [routes table](plugins/pstack/skills/poteto-mode/references/model-catalog.md#routes) lists how each one starts. Each run leaves a receipt with the exact model flags and, where the provider reports it, the model that answered. Every provider except Claude and Codex gets only an allowlisted environment. A Claude lane that writes files runs in Claude Code's Bash sandbox, with writes to the shared Git directory denied. A model that cannot run fails visibly; the runner never switches to a weaker model. +- **Runs inside T3 Code.** In [T3 Code](plugins/pstack/skills/poteto-mode/references/provider-dispatch.md#t3-host), pstack keeps each role's model and effort and lets T3 start and track the agents. - **A checked model sheet.** [`setup-pstack`](plugins/pstack/skills/setup-pstack/SKILL.md) records each role as `provider:model@effort`, asks for one effort per model family, and probes each chosen model before it saves anything. A rerun that changes nothing keeps the sheet without asking again. - **Three-model panels.** `arena`, `architect` and `interrogate` default to Opus, Sol and Grok. Lauren's pstack defaults to Opus 5.5 and Grok; Open Pstack keeps three ([UPSTREAM.md](UPSTREAM.md)). - **Changes tested where they run.** Before a change merges, the exact candidate is installed in each app it affects and the changed behavior is tested there. The pull request records the result ([AGENTS.md](AGENTS.md)). diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 2071f47f..4f81a8e0 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -1,6 +1,6 @@ # Codex tool mapping for pstack -pstack skills retain Claude Code tool language (`Skill`, `Agent`, `AskUserQuestion`) in shared prose. On Codex the files are the same; only those tool names resolve differently. Model execution is not translated here. Read [`provider-dispatch.md`](provider-dispatch.md) for the parent-owned Claude/Codex/Grok route table and provider-qualified descriptors. +pstack skills retain Claude Code tool language (`Skill`, `Agent`, `AskUserQuestion`) in shared prose. On Codex the files are the same; only those tool names resolve differently. Model execution is not translated here. Read [`provider-dispatch.md`](provider-dispatch.md) for the parent-owned Claude/Codex/Grok route table and provider-qualified descriptors. In a T3 Code session, its [T3 host](provider-dispatch.md#t3-host) section replaces this file's subagent, background, and wait mappings. ## Harness config homes diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 8c69d3a0..7a1816a7 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -28,10 +28,24 @@ A kept predecessor in the catalog matches its family. Dispatch it with the sheet The top-level harness resolves the route once. A child receives an assigned provider, model, effort, access mode, prompt, working directory, and output path. A child never detects the harness, chooses a provider, or launches another model. Environment markers may corroborate the top-level harness before fan-out, but nested processes inherit parent markers and must not use them for routing. -The catalog's [route table](model-catalog.md#routes) gives each provider's route from each parent. A provider native to the parent uses the parent's subagent primitive. Every other provider uses the external runner. +The catalog's [route table](model-catalog.md#routes) gives each provider's route from each parent. Outside [T3](#t3-host), a provider native to the parent uses the parent's subagent primitive. Every other provider uses the external runner. `inherit-parent` and `auto` remain aliases. They use the parent's current model and effort through its native subagent primitive. In a panel they still consume one lane, but they reduce provider diversity; say so in the synthesis record. +## T3 host + +When the session's own instructions say it runs in T3 Code, T3 starts and tracks every subagent. This section replaces the native-lane, external-lane, background, and receipt rules below. Pstack still chooses each role's provider, model, and effort. T3's instructions govern capability lookup, waiting, cancellation, and re-review. + +- `inherit-parent` or `auto` on a lane that shares the checkout and finishes this turn uses the parent's native subagent tool with no model or effort override. Any other inheriting lane calls `delegate_task` without a `target`. +- A `claude` or `codex` descriptor, after [read-time normalization](#read-time-normalization), calls `delegate_task`. Set `target.providerInstanceId` to the advertised provider whose `driverKind` is `claudeAgent` or `codex`, and `target.model` to a concrete model ID. Put the effort in `target.options` as `{"effort": ""}` for Claude or `{"reasoningEffort": ""}` for Codex. Effort `default` passes `options: {}`, which clears inherited settings so the model uses its own defaults. Use `mode: "wait"` when this turn needs the result, and `mode: "async"` otherwise. +- A rolling alias such as `opus` or `fable` resolves to the `claude--` ID with the numerically highest revision that `orchestrator_capabilities` returns for the search `claude-`. The unfiltered list omits models, so search before treating a model as missing. A registered exact pin from the model matrix stays exact. +- A model or effort that T3 does not advertise is a dropout. Never lower the effort or pick another model. Every other provider is a dropout on T3. Do not start its CLI. +- A writer that needs its own checkout uses `isolation: "worktree"`. T3 creates it from the parent's last commit, so commit what the child needs first. Never create a child checkout in the shell. +- T3 does not enforce read-only access. Add "Do not edit files." to a read-only lane's task and record that lane as advisory read-only. +- `/setup-pstack` on T3 skips step 5's CLI access checks and route probes. It probes each assigned pair with a one-line `delegate_task` and runs step 9's smoke panel through `delegate_task`, verifying the T3 records below in place of transcripts and receipts. A pair T3 does not advertise fails its probe. + +T3 task results replace runner receipts. Keep the task ID, the provider, model, and options T3 reports, the status, and the summary. A lane completes when T3 reports `completed` with `result_available` and a nonempty summary. A failed, cancelled, or interrupted task, or a reported model or options that differ from the request, is a dropout. These records show what T3 reported, not which model served the request. + ## Native lanes Native dispatch avoids a second CLI startup and its base context. From ae3cc95e5b766ab1fc1bc08a6ac3fdcf6550bd4f Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Fri, 6 Nov 2026 09:45:21 -0500 Subject: [PATCH 2/7] feat(t3): route T3 lanes by what T3 offers, else the standalone route Corrections to the ported T3 host section, per the #253 design: - The parent records the orchestration harness and the model runtime once; T3 is detected from the session's instructions and working tools, with T3CODE_THREAD_ID only as corroboration. - A lane goes through delegate_task when T3 offers its provider, enabled, with the model and effort; otherwise it takes the standalone route of the same runtime (native or the external runner with --parent ); a named dropout only when neither can run it. - inherit-parent and auto go through delegate_task with no target. - Alias resolution stays inside the provider and the catalog's revision form, compares revisions numerically and ignores suffixes such as -high. - Worktree children need a committed base or fixture inputs; no commits of unrelated changes on their behalf. - Advisory read-only only where the caller allows it; enforced read-only takes the standalone route. - T3 results are T3 launch configuration evidence with explicit completion, timeout and dropout rules. Refs #253 Co-authored-by: Aiden Lee Co-Authored-By: Claude Opus 5.5 --- .../poteto-mode/references/codex-tools.md | 2 +- .../references/provider-dispatch.md | 33 ++++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/codex-tools.md b/plugins/pstack/skills/poteto-mode/references/codex-tools.md index 4f81a8e0..9ce86a95 100644 --- a/plugins/pstack/skills/poteto-mode/references/codex-tools.md +++ b/plugins/pstack/skills/poteto-mode/references/codex-tools.md @@ -1,6 +1,6 @@ # Codex tool mapping for pstack -pstack skills retain Claude Code tool language (`Skill`, `Agent`, `AskUserQuestion`) in shared prose. On Codex the files are the same; only those tool names resolve differently. Model execution is not translated here. Read [`provider-dispatch.md`](provider-dispatch.md) for the parent-owned Claude/Codex/Grok route table and provider-qualified descriptors. In a T3 Code session, its [T3 host](provider-dispatch.md#t3-host) section replaces this file's subagent, background, and wait mappings. +pstack skills retain Claude Code tool language (`Skill`, `Agent`, `AskUserQuestion`) in shared prose. On Codex the files are the same; only those tool names resolve differently. Model execution is not translated here. Read [`provider-dispatch.md`](provider-dispatch.md) for the parent-owned Claude/Codex/Grok route table and provider-qualified descriptors. In a T3 Code session under either app, its [T3 host](provider-dispatch.md#t3-host) section routes each lane. This file's subagent, background, and wait mappings apply only to a lane it gives the standalone route. ## Harness config homes diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 7a1816a7..0cefe81e 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -30,21 +30,36 @@ The top-level harness resolves the route once. A child receives an assigned prov The catalog's [route table](model-catalog.md#routes) gives each provider's route from each parent. Outside [T3](#t3-host), a provider native to the parent uses the parent's subagent primitive. Every other provider uses the external runner. -`inherit-parent` and `auto` remain aliases. They use the parent's current model and effort through its native subagent primitive. In a panel they still consume one lane, but they reduce provider diversity; say so in the synthesis record. +`inherit-parent` and `auto` remain aliases. Outside [T3](#t3-host), they use the parent's current model and effort through its native subagent primitive. In a panel they still consume one lane, but they reduce provider diversity; say so in the synthesis record. ## T3 host -When the session's own instructions say it runs in T3 Code, T3 starts and tracks every subagent. This section replaces the native-lane, external-lane, background, and receipt rules below. Pstack still chooses each role's provider, model, and effort. T3's instructions govern capability lookup, waiting, cancellation, and re-review. +A T3 Code parent records two facts once, before fan-out: the orchestration harness is T3, and the model runtime is the app running the session, Claude Code or Codex. The parent is on T3 when its own instructions name T3 Code and T3's orchestration tools respond. `T3CODE_THREAD_ID` only corroborates; a marker without working tools is a visible failure, not a T3 parent. T3 owns delegation, worktrees, and lifecycle. The runtime owns skill loading, the config home, and the model sheet. Pstack still chooses each role's provider, model, and effort, and T3's instructions govern waiting, cancellation, and re-review. -- `inherit-parent` or `auto` on a lane that shares the checkout and finishes this turn uses the parent's native subagent tool with no model or effort override. Any other inheriting lane calls `delegate_task` without a `target`. -- A `claude` or `codex` descriptor, after [read-time normalization](#read-time-normalization), calls `delegate_task`. Set `target.providerInstanceId` to the advertised provider whose `driverKind` is `claudeAgent` or `codex`, and `target.model` to a concrete model ID. Put the effort in `target.options` as `{"effort": ""}` for Claude or `{"reasoningEffort": ""}` for Codex. Effort `default` passes `options: {}`, which clears inherited settings so the model uses its own defaults. Use `mode: "wait"` when this turn needs the result, and `mode: "async"` otherwise. -- A rolling alias such as `opus` or `fable` resolves to the `claude--` ID with the numerically highest revision that `orchestrator_capabilities` returns for the search `claude-`. The unfiltered list omits models, so search before treating a model as missing. A registered exact pin from the model matrix stays exact. -- A model or effort that T3 does not advertise is a dropout. Never lower the effort or pick another model. Every other provider is a dropout on T3. Do not start its CLI. -- A writer that needs its own checkout uses `isolation: "worktree"`. T3 creates it from the parent's last commit, so commit what the child needs first. Never create a child checkout in the shell. -- T3 does not enforce read-only access. Add "Do not edit files." to a read-only lane's task and record that lane as advisory read-only. +Read `orchestrator_capabilities` once per parent run and keep the snapshot. Route each normalized descriptor in this order: + +1. T3 route: call `delegate_task` when T3 lists a provider for the descriptor's provider with `canRunChildTask`, no constraints, `canRunCrossProviderChildTask` when it is not the parent's provider, and the resolved model with the requested effort. That provider's `driverKind` is `claudeAgent` for `claude` and the descriptor's provider otherwise. +2. Standalone route: any other lane takes the route a standalone parent of the same runtime gives it in the [route table](model-catalog.md#routes): the runtime's native subagent for its own provider, and otherwise the external runner with `--parent` set to the runtime. +3. A lane neither route can run is a named dropout. Never lower the effort or pick another model. + +On the T3 route: + +- Set `target.providerInstanceId` to the provider's `providerInstanceId` and `target.model` to a concrete model ID. Put the effort in the model's effort option: `{"effort": ""}` on `claudeAgent`, `{"reasoningEffort": ""}` on `codex`. Effort `default` passes `options: {}`, which clears inherited settings. A family whose effort is in its CLI model passes that CLI model and no effort option. Use `mode: "wait"` when this turn needs the result, and `mode: "async"` otherwise. +- `inherit-parent` and `auto` call `delegate_task` with no `target`, so the child inherits the parent's provider, model, and options. Record the parent selection from the snapshot's `inheritedProviderInstanceId`, `inheritedModel`, and `inheritedOptions`. +- A rolling alias such as `opus` resolves only among that provider's models, from the `orchestrator_capabilities` search `claude-`; the unfiltered list omits models, so search before treating a model as missing. Keep only IDs that match the alias's versioned-predecessor form in the catalog's [rolling-alias table](model-catalog.md#kept-predecessors-and-normalization), and take the highest revision, compared number by number. A suffix that is not digits and hyphens, such as `-high`, is not a revision. A registered exact pin stays exact. +- A writer that needs its own checkout uses `isolation: "worktree"`. T3 creates it from the parent's commit and leaves uncommitted edits out, so give the child a committed base or explicit fixture inputs in its task. Never commit unrelated changes for it, and never create a child checkout in the shell. +- T3 does not enforce read-only access. A read-only lane on the T3 route is advisory read-only: add "Do not edit files." to its task and record it as advisory. When the calling skill requires enforced read-only access, the lane takes the standalone route. - `/setup-pstack` on T3 skips step 5's CLI access checks and route probes. It probes each assigned pair with a one-line `delegate_task` and runs step 9's smoke panel through `delegate_task`, verifying the T3 records below in place of transcripts and receipts. A pair T3 does not advertise fails its probe. -T3 task results replace runner receipts. Keep the task ID, the provider, model, and options T3 reports, the status, and the summary. A lane completes when T3 reports `completed` with `result_available` and a nonempty summary. A failed, cancelled, or interrupted task, or a reported model or options that differ from the request, is a dropout. These records show what T3 reported, not which model served the request. +Keep one T3 result record per lane: the capability snapshot, the normalized descriptor, the request, and every raw response. Its model evidence is T3 launch configuration, never `provider-report` or `pinned-argv`. It shows what T3 launched, not which model served the request or the reasoning depth it applied. + +A T3 lane completes only when all of these hold: + +1. The response's task, provider, model, and effort match the request, compared on the normalized required settings only. `inherit-parent` matches the recorded parent selection, and `default` matches empty options. +2. The response reports `status: "completed"`, `workState: "result_available"`, `hasPendingChildRuns: false`, `hasPendingQuestion: false`, and `pendingApproval: null`. +3. The summary is nonempty, and every artifact the calling skill requires exists. + +A `wait` timeout means the child is still running: follow T3's `next`, and never poll or replace it. A failed, cancelled, interrupted, or mismatched task is a named dropout. ## Native lanes From 52096fce045ff55addf1d28782887fb1787c7b65 Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Fri, 6 Nov 2026 09:45:43 -0500 Subject: [PATCH 3/7] feat(t3): run /setup-pstack from a T3 session at its existing steps Step 1 records the orchestration harness and the runtime, and the runtime selects the config home, sheet and integration. Step 5 reads T3's capabilities once and routes each family by the T3 host order: T3-routed families probe through delegate_task, standalone-routed families keep the runtime's checks and probes. Step 7 shows each family's route and step 9 runs the smoke panel through the same routes. The fork's blanket setup bypass in provider-dispatch.md is deleted. Refs #253 Co-Authored-By: Claude Opus 5.5 --- .../poteto-mode/references/provider-dispatch.md | 1 - plugins/pstack/skills/setup-pstack/SKILL.md | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 0cefe81e..4619944b 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -49,7 +49,6 @@ On the T3 route: - A rolling alias such as `opus` resolves only among that provider's models, from the `orchestrator_capabilities` search `claude-`; the unfiltered list omits models, so search before treating a model as missing. Keep only IDs that match the alias's versioned-predecessor form in the catalog's [rolling-alias table](model-catalog.md#kept-predecessors-and-normalization), and take the highest revision, compared number by number. A suffix that is not digits and hyphens, such as `-high`, is not a revision. A registered exact pin stays exact. - A writer that needs its own checkout uses `isolation: "worktree"`. T3 creates it from the parent's commit and leaves uncommitted edits out, so give the child a committed base or explicit fixture inputs in its task. Never commit unrelated changes for it, and never create a child checkout in the shell. - T3 does not enforce read-only access. A read-only lane on the T3 route is advisory read-only: add "Do not edit files." to its task and record it as advisory. When the calling skill requires enforced read-only access, the lane takes the standalone route. -- `/setup-pstack` on T3 skips step 5's CLI access checks and route probes. It probes each assigned pair with a one-line `delegate_task` and runs step 9's smoke panel through `delegate_task`, verifying the T3 records below in place of transcripts and receipts. A pair T3 does not advertise fails its probe. Keep one T3 result record per lane: the capability snapshot, the normalized descriptor, the request, and every raw response. Its model evidence is T3 launch configuration, never `provider-report` or `pinned-argv`. It shows what T3 launched, not which model served the request or the reasoning depth it applied. diff --git a/plugins/pstack/skills/setup-pstack/SKILL.md b/plugins/pstack/skills/setup-pstack/SKILL.md index 238285bf..29b18e97 100644 --- a/plugins/pstack/skills/setup-pstack/SKILL.md +++ b/plugins/pstack/skills/setup-pstack/SKILL.md @@ -29,7 +29,7 @@ Codex writes `/pstack-models.md`. Codex has no `@` include, so mirr ### 1. Establish the parent -Use the harness and tool surface running this skill: Claude Code or Codex. Environment markers may corroborate that top-level answer, but do not launch a child and ask it to detect where it came from. Record the parent because the same descriptor takes a different route in each harness. +Use the harness and tool surface running this skill: Claude Code, Codex, or T3 Code running one of them. Environment markers may corroborate that top-level answer, but do not launch a child and ask it to detect where it came from. Record the parent because the same descriptor takes a different route in each harness. On T3, record the orchestration harness and the model runtime as [T3 host](../poteto-mode/references/provider-dispatch.md#t3-host) says. The runtime, Claude Code or Codex, selects ``, the sheet, and the integration below. ### 2. Load current state @@ -61,7 +61,7 @@ Except on the qualifying rerun above, ask one effort question for each assigned ### 5. Probe the requested pairs -Before any external CLI availability/authentication check or model probe, resolve the selected families through this parent's route table. On a Codex parent, name the assigned external families and inspect the parent's effective permission instructions for network access and the state paths and host prerequisites in each provider's [notes](../poteto-mode/references/model-catalog.md#provider-notes), including any required outside-workspace writes. Do not inspect credential contents or attempt a denied write to establish permission. `CODEX_SANDBOX_NETWORK_DISABLED=1` in the parent's own environment is evidence that external CLI network access is disabled; inspect it before child-environment scrubbing. `CODEX_SANDBOX` identifies a sandbox backend, not a complete permission policy. Missing markers, a config-file preference, or the name `workspace-write` alone do not prove sufficient access. Use the effective session permissions and required paths, not a blanket ban on every path outside the workspace. +Before any external CLI availability/authentication check or model probe, resolve the selected families through this parent's route table. On T3, read `orchestrator_capabilities` once, search for each assigned model the list omits, and give each family its route by the [T3 host](../poteto-mode/references/provider-dispatch.md#t3-host) order. A family on the T3 route needs no CLI access check. A family on the standalone route takes the checks below for the runtime's route table. On a Codex parent, name the assigned external families and inspect the parent's effective permission instructions for network access and the state paths and host prerequisites in each provider's [notes](../poteto-mode/references/model-catalog.md#provider-notes), including any required outside-workspace writes. Do not inspect credential contents or attempt a denied write to establish permission. `CODEX_SANDBOX_NETWORK_DISABLED=1` in the parent's own environment is evidence that external CLI network access is disabled; inspect it before child-environment scrubbing. `CODEX_SANDBOX` identifies a sandbox backend, not a complete permission policy. Missing markers, a config-file preference, or the name `workspace-write` alone do not prove sufficient access. Use the effective session permissions and required paths, not a blanket ban on every path outside the workspace. If required access is blocked, explain the affected assigned families and the restriction before invoking their CLIs. If permission information is unavailable or incomplete, call it **unknown**, say which required access cannot be established, and resolve it before external checks or probes. Offer exactly two paths when access is blocked or remains unknown: @@ -74,11 +74,11 @@ Probe only the selected `provider:model@effort` pair of each assigned family. Ru If a Grok probe fails because the host cannot enforce its bounded sandbox, see [Host and parent prerequisites](../poteto-mode/references/provider-dispatch.md#host-and-parent-prerequisites); keep the active configuration unchanged. -Find the family's provider in the [route table](../poteto-mode/references/model-catalog.md#routes). A native route gets a native one-turn probe: on Claude Code, the family's `pstack--` agent; on Codex, `spawn_agent` with the descriptor's model and `reasoning_effort`. An external route gets one `pstack-runner` call with a tiny read-only probe prompt. The runner's preflight checks the CLI, the login and the model, and its receipt is the result. +Find the family's provider in the [route table](../poteto-mode/references/model-catalog.md#routes). A native route gets a native one-turn probe: on Claude Code, the family's `pstack--` agent; on Codex, `spawn_agent` with the descriptor's model and `reasoning_effort`. An external route gets one `pstack-runner` call with a tiny read-only probe prompt. The runner's preflight checks the CLI, the login and the model, and its receipt is the result. A T3 route gets one `delegate_task` probe, and its T3 result record must meet T3 host's completion rules. -Use a tiny read-only probe that returns a unique marker. For each family that an external writer role uses on a provider whose writer needs a `full-access` setting line, also run one `isolated-write` probe in a disposable linked worktree, with `--writer-sandbox full-access`, that creates a small program and runs it; it passes only when the program's output file holds the expected marker. A login-status command alone proves credentials, not that the requested model and effort flags run. Record native and external results separately. Never call the external launcher for the parent's own provider. +Use a tiny read-only probe that returns a unique marker. For each family that an external writer role uses on a provider whose writer needs a `full-access` setting line, also run one `isolated-write` probe in a disposable linked worktree, with `--writer-sandbox full-access`, that creates a small program and runs it; it passes only when the program's output file holds the expected marker. A login-status command alone proves credentials, not that the requested model and effort flags run. Record native, external, and T3 results separately. Never call the external launcher for the parent's own provider. -Receipts and native transcripts prove the requested effort and the route. They do not prove a provider's hidden applied reasoning depth. There is no implicit timeout, weaker-model fallback, same-provider external fallback, or second mutable configuration source. +Receipts and native transcripts prove the requested effort and the route. T3 result records prove only T3's launch configuration. They do not prove a provider's hidden applied reasoning depth. There is no implicit timeout, weaker-model fallback, same-provider external fallback, or second mutable configuration source. ### 6. Render, preserving role families @@ -93,7 +93,7 @@ Rewrite every matrix-family descriptor to `provider:model@/pstack-models.md`, the route table for this parent, every rendered role and descriptor, and the setting lines. Carry the loaded assignments into the new destination; only explicit role or effort changes and the documented normalization may change them. Ask for confirmation before writing, unless the qualifying rerun's probes all passed. +Show any rolling-alias migrations as original and normalized descriptors and any retired-role rows dropped in step 2. Then show the source file read in step 2, the new destination `/pstack-models.md`, the route table for this parent (on T3, each family's route from step 5), every rendered role and descriptor, and the setting lines. Carry the loaded assignments into the new destination; only explicit role or effort changes and the documented normalization may change them. Ask for confirmation before writing, unless the qualifying rerun's probes all passed. Why and Reflect require the parent's live MCP surface. Keep their investigator, reviewer, and synthesizer roles on `inherit-parent` or `auto`; the bounded external runner deliberately omits ambient MCPs. `inherit-parent` and `auto` always validate, but say when they reduce a panel's provider diversity. For panel roles, one lane runs per entry. The list length is the fan-out count. `arena cross-judge pool` is also a list, but Arena runs one judge. It tries only entries from a provider other than the parent's, in list order, so order it by preference. An `inherit-parent` or `auto` entry never judges. `swarm workers` is the default for every worker unless a race explicitly assigns another descriptor. @@ -117,7 +117,7 @@ Do not copy the model sheet between harnesses without rerunning the parent-speci ### 9. Behavioral smoke -Before declaring setup complete, run one small read-only mixed panel from this parent: every distinct chosen descriptor, distinct output/receipt paths, and an independent cross-judge. Launch Claude-native agents and every external process in the background with retained handles, then drain them. Verify the native transcript entries and every external receipt. A structural config check or unit test is not a substitute. +Before declaring setup complete, run one small read-only mixed panel from this parent: every distinct chosen descriptor, distinct output/receipt paths, and an independent cross-judge. Launch Claude-native agents and every external process in the background with retained handles, then drain them. On T3, run each lane and the judge through the route step 5 gave its family. Verify the native transcript entries, every external receipt, and every T3 result record. A structural config check or unit test is not a substitute. Report the sheet path, parent route table, requested-effort probe results, smoke results, and external elapsed/token/cost receipts. Re-running this skill re-probes and updates the same sheet. Do not claim the provider exposed hidden applied-effort observability. From e1682eeabe8d8b12ab5dbdc60349e88f5de81442 Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Fri, 6 Nov 2026 09:46:22 -0500 Subject: [PATCH 4/7] feat(t3): name T3 Code as the third harness in policy and provenance docs AGENTS.md, UPSTREAM.md, README.md and docs/reference.md name Claude Code, Codex and T3 Code; T3 owns orchestration while its runtime owns configuration and skill installation. The triage rule that routed a new tool only as an external provider now distinguishes model providers from parent harnesses. UPSTREAM.md records the T3 routing adaptation, and CHANGES.md carries the entry under Unreleased. Refs #253 Co-Authored-By: Claude Opus 5.5 --- .agents/triage-backlog.md | 2 +- AGENTS.md | 2 +- CHANGES.md | 4 ++-- README.md | 8 ++++---- UPSTREAM.md | 8 +++++--- docs/reference.md | 2 +- 6 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.agents/triage-backlog.md b/.agents/triage-backlog.md index 46bd1799..9f3fb75a 100644 --- a/.agents/triage-backlog.md +++ b/.agents/triage-backlog.md @@ -30,7 +30,7 @@ cd - && git worktree remove --force /port `upstream-merge.py` has no dry-run mode and writes files, so this second throwaway worktree is the dry run. Report its verbatim, clean-merge and needs-review counts. Exit 1 means review is needed. Exit 2 means the script refused its input. Report the two apart. -Tag each item port-only or Cursor-derived, by the paths it edits (a PR's files) or names, as `UPSTREAM.md` and `NOTICE.md` map them; an item that names none is tagged from its subject, and the tag says so. Before a local fix to a Cursor-derived file is scheduled, check the source that `UPSTREAM.md` or `NOTICE.md` names for that path for an existing fix, such as `git log cursor/main -- pstack/` for the pstack tree. An "upstream first" disposition is finished only when the authorized upstream action exists and its link is in the plan. For an issue already closed upstream-first, record whether the upstream PR exists, and act only when its reopen condition fired. Route a new tool as an external provider, not as a parent harness. Done when the counts, the exit code and a tag for each item are in the plan. +Tag each item port-only or Cursor-derived, by the paths it edits (a PR's files) or names, as `UPSTREAM.md` and `NOTICE.md` map them; an item that names none is tagged from its subject, and the tag says so. Before a local fix to a Cursor-derived file is scheduled, check the source that `UPSTREAM.md` or `NOTICE.md` names for that path for an existing fix, such as `git log cursor/main -- pstack/` for the pstack tree. An "upstream first" disposition is finished only when the authorized upstream action exists and its link is in the plan. For an issue already closed upstream-first, record whether the upstream PR exists, and act only when its reopen condition fired. Distinguish model providers from parent harnesses; T3 Code is a supported parent harness. Done when the counts, the exit code and a tag for each item are in the plan. ### Steps 2 (Collect) and 5 (Release lag): model snapshot, release procedure and model lag diff --git a/AGENTS.md b/AGENTS.md index 5903f99d..54bdf6b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Track all durable work in this repository's GitHub Issues. Do not create a parallel Linear queue. Build a ticket that comes from a waived review finding only after that failure has been seen in real use; until then the ticket records the finding and stays unscheduled. Read `UPSTREAM.md` before changing upstream-derived content. -Cursor's `cursor/plugins/pstack` tree is the content upstream. Keep one shared skill tree for Claude Code and Codex; adapt harness primitives at the existing mapping boundaries instead of forking skills or adding compatibility layers. The parent harness resolves provider routing once. Children do not detect or reroute themselves. +Cursor's `cursor/plugins/pstack` tree is the content upstream. Keep one shared skill tree for Claude Code, Codex and T3 Code. T3 Code owns orchestration, and the app it runs, Claude Code or Codex, owns configuration and skill installation. Adapt harness primitives at the existing mapping boundaries instead of forking skills or adding compatibility layers. The parent harness resolves provider routing once. Children do not detect or reroute themselves. Code this repository owns explains itself. `UPSTREAM.md`, `CHANGES.md` and `NOTICE.md` say which files are local adaptations or additions. Upstream-derived files keep upstream's comments, so later syncs stay clean. In owned code, a comment stays only if it is a legal or license header, an explanation of behaviour forced by a vendor, platform or protocol we cannot change, a formatter directive, a doc comment that defines a public API contract, or an issue or RFC link for a constraint that code cannot express. When owned code would surprise a reader, reshape it (rename, extract, add a type) instead of explaining it. Encode a "must not change" constraint as a type, test or lint, then delete its comment. Suppress a lint or type error only when its rule is wrong, pedantic or style-only; otherwise fix the cause. pstack's `no-comments` skill applies this rule. diff --git a/CHANGES.md b/CHANGES.md index 278d2d80..aa26052a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,9 +3,9 @@ Open Pstack lands on green: Mergify merges a ready PR to main once verify and exact-head live-gate pass and no review thread is unresolved, and a workflow resolves Unfret's own threads. A PR that touches `.github/` or the Mergify config also waits for Unfret, and workflow changes require operator queueing. 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. -## T3 host route (#253) +## Unreleased -Provider dispatch gains a T3 host section. In a T3 Code session, pstack still chooses each role's model and effort, and T3's `delegate_task` starts, isolates, and tracks the agents in place of the native, external-runner, and receipt rules. `codex-tools.md` points to it. No skill body changes. +**T3 Code is a third parent harness (#253).** Started from fikaventures' PR #254. In a T3 Code session, pstack still chooses each role's model and effort. A lane whose provider T3 offers and has enabled, with the model and effort, goes through T3's `delegate_task`. Any other lane takes the route a standalone Claude Code or Codex parent gives it, and a lane neither route can run is a named dropout. `inherit-parent` and `auto` go through `delegate_task` with no model override. A T3 result is T3 launch-configuration evidence, not a served-model report. A read-only lane on T3 is advisory, and a caller that needs enforced read-only access takes the standalone route. `/setup-pstack` runs from a T3 session: it probes T3-routed families through `delegate_task` and the rest as before, and runs its smoke panel through the same routes. ## 1.18.0 gives external providers writers and adds a Muse Code provider diff --git a/README.md b/README.md index 5e15c274..186ec52b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Latest release](https://img.shields.io/github/v/release/ericlitman/open-pstack)](https://github.com/ericlitman/open-pstack/releases/latest) [![MIT license](https://img.shields.io/github/license/ericlitman/open-pstack)](LICENSE) -**Open Pstack brings [Lauren Tan (@poteto)](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack) to Claude Code and Codex.** Its job is to stay as close to her original work as possible while translating the parts that depend on Cursor. +**Open Pstack brings [Lauren Tan (@poteto)](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack) to Claude Code and Codex, and to T3 Code running either one.** Its job is to stay as close to her original work as possible while translating the parts that depend on Cursor. Lauren built pstack from the skills she uses to ship code at Cursor. In a [55-minute interview with Denis Labelle](https://x.com/DenisLabelle/status/2091337807939706928), she says that she shipped 1,000 pull requests in one month after steadily improving how her agents work and verify their results. @@ -19,7 +19,7 @@ Compared with Lauren's pstack, Open Pstack adds: - **A model and provider registry.** Models, efforts and providers live in one typed [registry](plugins/pstack/skills/poteto-mode/scripts/runner/registry/). The [model catalog](plugins/pstack/skills/poteto-mode/references/model-catalog.md) and the Claude-native agent files are generated from it, and the skills point at the catalog's role defaults instead of naming models. A new model is one registry entry. A new command-line provider is one adapter module and one line in the provider list. See the [design](docs/plans/2026-10-11-model-provider-registry.md). - **One skill tree for Claude Code and Codex.** Both apps read the same skills, and one [reference](plugins/pstack/skills/poteto-mode/references/codex-tools.md) maps how each app starts them. Lauren's pstack does not run in Codex. See [Claude Code and Codex](#claude-code-and-codex) below. - **Models from other providers.** Claude Code and Codex hand work to other providers' models through each provider's own command-line tool, started by one [runner](plugins/pstack/skills/poteto-mode/references/provider-dispatch.md). The runner supports nine providers. Claude, Codex and Grok run lanes that read or write files. Devin, DeepSeek Harness, Antigravity, OpenCode, Amp and Cursor CLI run read-only lanes. The model catalog's [routes table](plugins/pstack/skills/poteto-mode/references/model-catalog.md#routes) lists how each one starts. Each run leaves a receipt with the exact model flags and, where the provider reports it, the model that answered. Every provider except Claude and Codex gets only an allowlisted environment. A Claude lane that writes files runs in Claude Code's Bash sandbox, with writes to the shared Git directory denied. A model that cannot run fails visibly; the runner never switches to a weaker model. -- **Runs inside T3 Code.** In [T3 Code](plugins/pstack/skills/poteto-mode/references/provider-dispatch.md#t3-host), pstack keeps each role's model and effort and lets T3 start and track the agents. +- **Runs inside T3 Code.** In [T3 Code](plugins/pstack/skills/poteto-mode/references/provider-dispatch.md#t3-host), pstack keeps each role's model and effort. T3 starts each lane whose provider it offers, and the runner starts the rest. - **A checked model sheet.** [`setup-pstack`](plugins/pstack/skills/setup-pstack/SKILL.md) records each role as `provider:model@effort`, asks for one effort per model family, and probes each chosen model before it saves anything. A rerun that changes nothing keeps the sheet without asking again. - **Three-model panels.** `arena`, `architect` and `interrogate` default to Opus, Sol and Grok. Lauren's pstack defaults to Opus 5.5 and Grok; Open Pstack keeps three ([UPSTREAM.md](UPSTREAM.md)). - **Changes tested where they run.** Before a change merges, the exact candidate is installed in each app it affects and the changed behavior is tested there. The pull request records the result ([AGENTS.md](AGENTS.md)). @@ -157,7 +157,7 @@ Both apps read the same pstack skills. Only the way they start those skills and | Other models | Codex, Grok and the other external providers run through their command-line tools ([routes](plugins/pstack/skills/poteto-mode/references/model-catalog.md#routes)). | Claude, Grok and the other external providers run through their command-line tools ([routes](plugins/pstack/skills/poteto-mode/references/model-catalog.md#routes)). | | Skills and workflows | Shared with Codex. | Shared with Claude Code. | -The other providers take part only through the runner. Open Pstack itself runs inside Claude Code or Codex. +The other providers take part only through the runner. Open Pstack itself runs inside Claude Code or Codex. In T3 Code, the app T3 runs loads the skills and the model sheet, and T3 starts each lane whose provider it offers. ## Learn from the original @@ -183,7 +183,7 @@ In this repository, “upstream” means Lauren's original pstack. Open Pstack d Fixes for Claude Code or Codex and help bringing over new pstack releases are welcome. Search [GitHub Issues](https://github.com/ericlitman/open-pstack/issues) before opening a new issue. For larger behavior changes, explain why the change belongs in Open Pstack instead of Lauren's original project. -Read [UPSTREAM.md](UPSTREAM.md) before changing content brought over from Lauren's pstack. Pull requests must keep one shared skill tree for Claude Code and Codex and pass the repository's tests, type checks, plugin validation, and static checks. +Read [UPSTREAM.md](UPSTREAM.md) before changing content brought over from Lauren's pstack. Pull requests must keep one shared skill tree for Claude Code, Codex and T3 Code and pass the repository's tests, type checks, plugin validation, and static checks. ## License diff --git a/UPSTREAM.md b/UPSTREAM.md index 6acb59ce..950f167f 100644 --- a/UPSTREAM.md +++ b/UPSTREAM.md @@ -1,6 +1,6 @@ # Upstream synchronization -open-pstack tracks [Cursor's pstack](https://github.com/cursor/plugins/tree/main/pstack) while adapting Cursor-specific primitives for Claude Code and Codex. +open-pstack tracks [Cursor's pstack](https://github.com/cursor/plugins/tree/main/pstack) while adapting Cursor-specific primitives for Claude Code, Codex and T3 Code. ## Current sync point @@ -69,6 +69,8 @@ The table above is the current Cursor sync point. Open Pstack 1.17.0 imported th - **#265 — Orchestrate verifier and sub-coordinator**: keep upstream's "on a different model family" for each PR's verifier; do not substitute provider. After upstream's sub-coordinator sentence, the port adds that where a subagent cannot start agents, such as a Claude Code native lane, the coordinator starts the sub-coordinator's workers from the kickoffs it writes and hands their results back to it. `codex-tools.md#subagent-policy` states that native lanes disallow `Agent` and `Task`. +- **#253 — T3 Code parent**: Cursor has no T3 Code route. The port adds the `T3 host` section to `provider-dispatch.md`: a lane whose provider T3 offers and has enabled goes through `delegate_task`, and any other lane takes the standalone route of the Claude Code or Codex runtime. `codex-tools.md` points to it, and `setup-pstack/SKILL.md` routes its probes and smoke panel the same way at its existing steps. No other skill body changes. Keep these lines during syncs. + ## Check for changes The repository already names Cursor's repository as the `cursor` remote in the maintainer checkout. A fresh clone can add it once: @@ -90,7 +92,7 @@ No output means the tracked pstack tree has not changed. This comparison does no ## Incorporate a change 1. Create or update a GitHub issue in `ericlitman/open-pstack` and branch from current `main`. Pin the upstream target commit in the issue. -2. Read each upstream pstack commit in order, and record a decision for each one in the issue as a table: adopt, adapt (name the adaptation), or exclude (give the reason). Bring over each adopted commit's intent and content, then apply only the Claude Code and Codex substitutions documented in `CHANGES.md` and the adaptations in this file. +2. Read each upstream pstack commit in order, and record a decision for each one in the issue as a table: adopt, adapt (name the adaptation), or exclude (give the reason). Bring over each adopted commit's intent and content, then apply only the Claude Code, Codex and T3 Code substitutions documented in `CHANGES.md` and the adaptations in this file. 3. Run the audit and the mechanical merge from a clean checkout: ```shell @@ -100,7 +102,7 @@ No output means the tracked pstack tree has not changed. This comparison does no The merge checks out files the port never changed, merges the rest three ways, and prints counts of verbatim, clean, review and unmapped paths. It exits 1 when any path needs review. 4. Resolve each review row by hand. A conflict hunk keeps the port's adaptation and takes upstream's new intent beside it. A change to an excluded path stays out and extends that exclusion's line in this file. Each unmapped path, such as a Cursor manifest or the guide, is excluded or mapped by a decision in step 2. Record every hand resolution and every new adaptation or exclusion in this file. -5. Keep one shared `plugins/pstack/skills/` tree. Put harness translation in the existing `codex-tools.md` and provider routing in `provider-dispatch.md`; do not fork a skill per harness. +5. Keep one shared `plugins/pstack/skills/` tree. Put harness translation in the existing `codex-tools.md` and provider routing, including T3 Code's, in `provider-dispatch.md`; do not fork a skill per harness. 6. Add every new skill to the `skills` list in `.claude/skills/verify-open-pstack/features/registry.json`. 7. Update the commit and version in this file, add a provenance row to `NOTICE.md`, and take `README-UPSTREAM.md` when upstream changes its README. 8. Run CI-equivalent checks locally, then run the installed Claude Code and Codex behavioral lanes required by the changed surface with `verify-open-pstack`. A sync that edits the verifier runs from the candidate head with `--self-test`. A run above the session limit needs `--wide ""`. Unit tests alone are not a release gate. diff --git a/docs/reference.md b/docs/reference.md index 8d6daf2f..9a830649 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -2,7 +2,7 @@ This page contains the full skill, dependency, runtime, and porting reference. For the plain-English introduction and quick start, see the [main README](../README.md). -[Poteto](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack), adapted to run in Claude Code and Codex without Cursor. One shared skill tree serves both harnesses; lanes on other providers' models, including Grok and seven more providers, run through the bundled runner. Version 1.17.0 is synced to Cursor pstack v0.15.36 at `2776ddd2355d6d6c8747d3964600f28daf62f049`. See [UPSTREAM.md](../UPSTREAM.md) for the exact sync contract. +[Poteto](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack), adapted to run in Claude Code, Codex and T3 Code without Cursor. One shared skill tree serves all three harnesses, and in T3 Code the Claude Code or Codex runtime loads it; lanes on other providers' models, including Grok and seven more providers, run through the bundled runner. Version 1.17.0 is synced to Cursor pstack v0.15.36 at `2776ddd2355d6d6c8747d3964600f28daf62f049`. See [UPSTREAM.md](../UPSTREAM.md) for the exact sync contract. Original by Lauren Tan. This distribution builds on Michael Denyer's [pstack-claude](https://github.com/michael-denyer/pstack-claude) port and retains its history and MIT attribution. It imports seven MIT-licensed skills from [cursor-team-kit](https://github.com/cursor/plugins/tree/main/cursor-team-kit): `deslop`, `thermo-nuclear-code-quality-review`, `make-pr-easy-to-review`, `fix-ci`, `fix-merge-conflicts`, `get-pr-comments`, `what-did-i-get-done`. From 9b4c6e43d1b29cd926e9b7ae9f8c36fbc32d511f Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Fri, 6 Nov 2026 09:55:48 -0500 Subject: [PATCH 5/7] feat(t3): map each pstack provider to its T3 driver name T3 names Claude `claudeAgent` and Antigravity `antigravity`, where pstack says `claude` and `agy`. One table in the T3 host section now maps each pstack provider to the `driverKind` a live `orchestrator_capabilities` call lists; a provider without a T3 driver takes the standalone route. Refs #253 Co-Authored-By: Claude Opus 5.5 --- .../poteto-mode/references/provider-dispatch.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 4619944b..af4b3d27 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -38,7 +38,18 @@ A T3 Code parent records two facts once, before fan-out: the orchestration harne Read `orchestrator_capabilities` once per parent run and keep the snapshot. Route each normalized descriptor in this order: -1. T3 route: call `delegate_task` when T3 lists a provider for the descriptor's provider with `canRunChildTask`, no constraints, `canRunCrossProviderChildTask` when it is not the parent's provider, and the resolved model with the requested effort. That provider's `driverKind` is `claudeAgent` for `claude` and the descriptor's provider otherwise. +1. T3 route: call `delegate_task` when T3 lists a provider for the descriptor's provider with `canRunChildTask`, no constraints, `canRunCrossProviderChildTask` when it is not the parent's provider, and the resolved model with the requested effort. Match the descriptor's provider to T3's `driverKind` with this table: + + | Pstack provider | T3 `driverKind` | + | --- | --- | + | `claude` | `claudeAgent` | + | `codex` | `codex` | + | `cursor` | `cursor` | + | `grok` | `grok` | + | `opencode` | `opencode` | + | `agy` | `antigravity` | + + A provider missing from the table has no T3 driver and takes the standalone route. 2. Standalone route: any other lane takes the route a standalone parent of the same runtime gives it in the [route table](model-catalog.md#routes): the runtime's native subagent for its own provider, and otherwise the external runner with `--parent` set to the runtime. 3. A lane neither route can run is a named dropout. Never lower the effort or pick another model. From 10cd1fdf3b09aaf45ba0f806c2b9723ac9552d19 Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Fri, 6 Nov 2026 10:04:28 -0500 Subject: [PATCH 6/7] feat(verify): add a T3 transport and the t3:dispatch live case The verifier can now run a live lane from a real T3 Code session. The `t3` transport starts a run-owned disposable T3 server from the operator's installed service binary, gives its Claude provider the same session-only candidate plugin flags as the Claude harness, runs each case with `t3 thread start --wait --json`, stops the server, and parses the parent's Claude transcript, located through the run-owned `statev2.sqlite`, with the Claude stream parser. `t3:dispatch` has the installed poteto-mode dispatch one fixture-sheet role (`claude:claude-haiku-5-5@low`) and asserts the candidate skill path, the exact `delegate_task` request, the settled response and a per-run marker the child read from a fixture file. Classification reaches `t3:dispatch` only from provider-dispatch.md, codex-tools.md, setup-pstack/SKILL.md and recipes.ts, never by default; self-test adds no T3 session, and the doctor checks the T3 binary only when a `t3:` feature is named. Refs #253 Co-Authored-By: Claude Opus 5.5 --- .claude/skills/verify-open-pstack/SKILL.md | 4 +- .../verify-open-pstack/features/README.md | 1 + .../verify-open-pstack/features/recipes.ts | 38 ++++++++-- .../skills/verify-open-pstack/features/t3.md | 21 ++++++ .../skills/verify-open-pstack/scripts/cli.ts | 2 +- .../verify-open-pstack/scripts/core.test.ts | 18 ++++- .../skills/verify-open-pstack/scripts/core.ts | 13 +++- .../verify-open-pstack/scripts/doctor.ts | 12 +++- .../verify-open-pstack/scripts/harness.ts | 70 +++++++++++++++---- .../verify-open-pstack/scripts/types.ts | 4 +- 10 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 .claude/skills/verify-open-pstack/features/t3.md diff --git a/.claude/skills/verify-open-pstack/SKILL.md b/.claude/skills/verify-open-pstack/SKILL.md index 4f9dc155..be3340dc 100644 --- a/.claude/skills/verify-open-pstack/SKILL.md +++ b/.claude/skills/verify-open-pstack/SKILL.md @@ -1,6 +1,6 @@ --- name: verify-open-pstack -description: Verify an Open Pstack PR's exact candidate head in native Claude Code and Codex sessions, judge them with machine-checked assertions, and publish the exact-SHA live gate. +description: Verify an Open Pstack PR's exact candidate head in native Claude Code, Codex and T3 Code sessions, judge them with machine-checked assertions, and publish the exact-SHA live gate. --- # Verify Open Pstack @@ -51,6 +51,8 @@ Create detached exact-head candidate checkouts and run-owned temporary/fixture s Claude runs with normal operator config/login and session-only `claude --plugin-dir /plugins/pstack --settings '{"enabledPlugins":{"pstack@open-pstack":false}}'`, plus `-p --input-format stream-json --replay-user-messages --output-format stream-json --verbose --no-session-persistence --permission-mode dontAsk --allowedTools `. The prompt arrives on stdin, so the replayed user message shows the slash expansion. A tool outside the pre-authorized list is denied without a prompt. Codex runs `codex exec --json` against the run-owned installation with approvals set to never, `--sandbox workspace-write`, `--add-dir` for a fixture outside its working directory, and `--dangerously-bypass-hook-trust` so the candidate's hooks run for that invocation only. Cases whose parent launches an external provider CLI use `--sandbox danger-full-access`, because those CLIs need network and their own state outside the fixture. Codex installs the exact-head marketplace/plugin in its run-owned home with `codex plugin marketplace add … --ref ` and `codex plugin add`. Verify candidate plugin and project-skill sources against immutable Git provenance before and after exercise; version strings alone are insufficient. +`t3:` features run through the T3 transport in `features/t3.md`: a run-owned disposable T3 server whose Claude provider gets the same session-only plugin flags, one `t3 thread start --wait --json` per case, and the parent's Claude transcript parsed like the Claude stream. Classification never adds `t3` on its own and self-test does not reach it; the doctor checks the T3 service binary only when a `t3:` feature is named. + Candidate commands and provider descendants use the recorded native-login arrangement, never GitHub publisher credentials. No extra login, implicit timeout, or weaker-model fallback is permitted. If Claude's plugin flags fail to exclude installed pstack or Codex replaces the auth symlink instead of writing through it, stop and reopen the design. For every named feature, exercise its native surface and retain real native tool calls and concrete fixture effects. Asset consumers come from the pinned plugin manifests; the current `plugins/pstack/assets/logo.png` is consumed by the Codex manifest only and therefore requires the Codex installed surface, not an invented Claude asset exercise. The session recorder streams each session's stdout and stderr to private files under `sessions///` in the run root and keeps them on any exit. For Codex it also copies the session's rollout from the run-owned home, because only the rollout records which `SKILL.md` each skill mention resolved to. A nonzero session exit fails the case. The trusted parent then parses the native stream and evaluates the case's assertions on the native skill load, the commands that ran, and files the session wrote in its fixture, and copies those files next to the stream. A model's claim that something passed never counts, and neither do direct CLI tests. diff --git a/.claude/skills/verify-open-pstack/features/README.md b/.claude/skills/verify-open-pstack/features/README.md index 664638b0..414099a7 100644 --- a/.claude/skills/verify-open-pstack/features/README.md +++ b/.claude/skills/verify-open-pstack/features/README.md @@ -35,6 +35,7 @@ The feature documents below define sub-features, user entry points, driving inst - `catalog.md`: the generated model catalog and native agents, read by a parent resolving roles from a model sheet. - `shipped-tools.md`: watch, plan checks, audit, statechart, and evidence logging. - `assets.md`: manifest-declared installed assets on actual consuming harnesses. +- `t3.md`: a T3 Code parent dispatching a role through `delegate_task` (`t3:dispatch`), on a run-owned disposable T3 server. - `hooks.md`: the SessionStart hook's context reaching a new session (`hook:startup`), and the Windows runner beside it. - `recipes.ts`: the headless cases and machine-checked assertions for every selectable feature. A named feature without one fails the run as `missing-recipe` before any session starts. A recipe proves a lookup or a read with a fixture value the model cannot derive from the prompt, such as a per-run `crypto.randomUUID()` marker, compared in the structured result. A command's text can show which tool the parent called, but it never proves what that tool read or produced, so do not add command-text or tool-output pattern checks for that. - `agent-preload:poteto-agent` (Claude only): the parent dispatches `pstack:poteto-agent` through the Agent tool and asks it for the first Core principle of the preloaded poteto-mode skill without invoking `Skill` or reading files. The check needs that Agent call, `Laziness Protocol` in the reply the parent writes to `result.md`, and no `Skill` call or `Read` of poteto-mode's `SKILL.md` among the child's own tool calls. diff --git a/.claude/skills/verify-open-pstack/features/recipes.ts b/.claude/skills/verify-open-pstack/features/recipes.ts index 16ba16a8..87655e09 100644 --- a/.claude/skills/verify-open-pstack/features/recipes.ts +++ b/.claude/skills/verify-open-pstack/features/recipes.ts @@ -5,7 +5,7 @@ import { homedir } from 'node:os'; import { basename, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { sha256, type Command } from '../scripts/io.ts'; -import type { Harness, SessionRecord } from '../scripts/types.ts'; +import type { CoreHarness, Harness, SessionRecord } from '../scripts/types.ts'; export interface Fixture { harness: Harness; @@ -29,7 +29,7 @@ export interface Case { } export const CLAUDE_TOOLS = 'Skill,Read,Glob,Grep,Write,Edit,Bash,Agent,Task,TodoWrite'; -export const DEFAULT_ROLES: Record = { claude: 'bug-fix', codex: 'judgment and prose' }; +export const DEFAULT_ROLES: Record = { claude: 'bug-fix', codex: 'judgment and prose' }; export const ROUTE = /^([a-z][a-z0-9-]*):([A-Za-z0-9._/-]+)@([a-z]+)$/; const SETUP_DESCRIPTOR = 'claude:opus@high'; const DESCRIPTOR = /\b[a-z][a-z0-9-]*:[A-Za-z0-9._/-]+@[a-z]+/g; @@ -54,7 +54,7 @@ export async function readCatalog(location: string): Promise { const kept = new Map(rows(section('Kept predecessors and normalization').split('\nRolling aliases:')[0]!).slice(1) .map(([provider, model, , current]): [string, string] => [`${provider}:${model}`, `${provider}:${current}`])); const [header = [], ...routes] = rows(section('Routes')); - const column = { claude: header.indexOf('Claude Code parent'), codex: header.indexOf('Codex parent') }; + const column = { claude: header.indexOf('Claude Code parent'), codex: header.indexOf('Codex parent'), t3: -1 }; if (!defaults.size || column.claude < 0 || column.codex < 0) throw new Error('catalog-unreadable'); return { defaults, kept, route(harness, descriptor) { if (!descriptor.includes(':')) return 'native'; @@ -440,7 +440,7 @@ const museReader: Case = { }, }; -function runner(harness: Harness, routes: string[]): Case[] { +function runner(harness: CoreHarness, routes: string[]): Case[] { const chosen = routes.filter(route => !route.startsWith(`${harness}:`)); const cases = (chosen.length ? chosen : ['']).map((fixed, index): Case => { let route = fixed, provider = '', model = '', effort = ''; @@ -775,7 +775,37 @@ const startupHook: Case = { }, }; +const T3_ROLE = 'bug-fix', T3_LANE = { provider: 'claudeAgent', model: 'claude-haiku-5-5', effort: 'low' }; +const t3Dispatch: Case = { + id: 'dispatch', + async prepare(f) { + await files(f.dir, { 'marker.txt': `${crypto.randomUUID()}\n`, 'pstack-models.md': sheetText([[T3_ROLE, [`claude:${T3_LANE.model}@${T3_LANE.effort}`]]]) }); + }, + prompt: f => `${invoke(f.harness, 'poteto-mode', `Dispatch exactly one lane for the ${T3_ROLE} role and nothing else. For this task the pstack model sheet is ${join(f.dir, 'pstack-models.md')}; use it in place of any configured sheet. The lane's whole task: "Read ${join(f.dir, 'marker.txt')} and reply with its exact contents. Do not edit files." Wait for the lane's result, then reply with it.`)} ${rules(f.dir)}`, + async assert(record, f) { + if (!record.skills.includes(join(f.location, 'skills/poteto-mode'))) throw new Error('skill-not-loaded-from-candidate:pstack:poteto-mode'); + const calls = record.tools.filter(t => t.name === 'mcp__t3-code__delegate_task' && !t.failed); + if (calls.length !== 1) throw new Error(`delegate-task-calls:${calls.length}`); + const { input, result } = calls[0]!, target = input.target ?? {}; + if (target.providerInstanceId !== T3_LANE.provider || target.model !== T3_LANE.model || !Bun.deepEquals(target.options, { effort: T3_LANE.effort })) throw new Error('delegate-request-mismatch'); + const response = parseJson(result) as Record | undefined; + if (response?.providerInstanceId !== T3_LANE.provider || response.model !== T3_LANE.model || + !Bun.deepEquals(response.options, [{ id: 'effort', value: T3_LANE.effort }])) throw new Error('delegate-response-mismatch'); + if (response.status !== 'completed' || response.workState !== 'result_available' || response.hasPendingChildRuns !== false || + response.hasPendingQuestion !== false || response.pendingApproval !== null) throw new Error(`delegate-not-settled:${response.status}`); + const marker = (await readFile(join(f.dir, 'marker.txt'), 'utf8')).trim(); + if (typeof response.summary !== 'string' || !response.summary.includes(marker)) throw new Error('marker-missing'); + const exchange = join(f.dir, 'delegate-task.json'); + await writeFile(exchange, JSON.stringify({ request: input, response }, null, 2)); + return { assertions: ['skill-loaded-from-candidate', 'delegate-request', 'delegate-settled', 'marker-returned'], files: [exchange, join(f.dir, 'pstack-models.md')] }; + }, +}; + export function recipe(feature: string, harness: Harness, routes: string[] = []): Case[] { + if (harness === 't3') { + if (feature === 'dispatch') return [t3Dispatch]; + throw new Error(`missing-recipe:${harness}/${feature}`); + } if (feature === 'assets:codex') throw new Error('unsupported-native-consumer:assets:codex'); if (feature === 'project-skill') return [projectSkill]; if (feature === 'hook:startup') return [startupHook]; diff --git a/.claude/skills/verify-open-pstack/features/t3.md b/.claude/skills/verify-open-pstack/features/t3.md new file mode 100644 index 00000000..0d7e0333 --- /dev/null +++ b/.claude/skills/verify-open-pstack/features/t3.md @@ -0,0 +1,21 @@ +# T3 parent dispatch + +## Sub-features + +`t3:dispatch`: a T3 Code parent running the installed `poteto-mode` routes a role whose provider T3 offers through T3's `delegate_task`, with the model and effort from the model sheet, and judges the result by the T3 host completion rules in `provider-dispatch.md`. Select it for a change to `provider-dispatch.md`, `codex-tools.md`, `setup-pstack/SKILL.md` or `recipes.ts`. A path that reaches it never adds it to every run; name it with `--feature t3:dispatch`. + +## How to get to it (user POV) + +Start a T3 Code thread on the Claude provider with the candidate plugin loaded, and ask `poteto-mode` to dispatch one role from a model sheet. + +## Driving it with verify-open-pstack + +The `t3` transport runs a disposable T3 server from the operator's installed service binary, `~/.t3/runtime/versions//t3`, which `~/.t3/runtime/service-state.json` names. The server's base directory and project live in the run root. Its `userdata/settings.json` sets the `claudeAgent` provider's `launchArgs` to the same session-only `--plugin-dir /plugins/pstack --settings '{"enabledPlugins":{"pstack@open-pstack":false}}'` the Claude harness uses, so the parent and every Claude child it delegates to load the candidate. Each case adds the fixture as a project, runs its prompt with `t3 thread start --provider claudeAgent --model claude-sonnet-5-5 --wait --json`, and stops the server. + +## Gotchas + +`t3 thread show --json` carries neither tool calls nor options, so the transport reads the parent's Claude transcript. The run-owned `statev2.sqlite` maps the T3 thread to its Claude session id (`nativeThreadRef.nativeId`), and the transcript is `/projects//.jsonl`. That transcript and the delegated child's stay in the operator's Claude projects folder; the verifier copies the parent's into the run root. The operator's T3 service, settings and data are never touched. + +## Headless + +The `dispatch` case writes a fixture sheet whose `bug-fix` role is `claude:claude-haiku-5-5@low` and a `marker.txt` holding a per-run `crypto.randomUUID()` that the prompt never contains. It asserts the transcript's `Base directory for this skill` line for `poteto-mode` is the candidate plugin path; exactly one `delegate_task` call has `target.providerInstanceId` `claudeAgent`, `target.model` `claude-haiku-5-5` and `target.options` `{"effort":"low"}`; its result names the same provider, model and effort and is settled (`completed`, `result_available`, no pending child runs, question or approval); and its summary holds the marker. The request and response are retained as `delegate-task.json`. The model's prose is never read. diff --git a/.claude/skills/verify-open-pstack/scripts/cli.ts b/.claude/skills/verify-open-pstack/scripts/cli.ts index 699cee15..3d875b48 100644 --- a/.claude/skills/verify-open-pstack/scripts/cli.ts +++ b/.claude/skills/verify-open-pstack/scripts/cli.ts @@ -26,7 +26,7 @@ export function parse(args: string[]): Options { if (!ROUTE.test(value) || routes.includes(value)) throw new Error('Runner route must be a distinct PROVIDER:MODEL@EFFORT'); routes.push(value); } else if (arg === '--feature') { - if (!/^(claude|codex):[a-z][a-z0-9:-]+$/.test(value) || features.includes(value)) throw new Error('Feature must be a distinct HARNESS:FEATURE, such as claude:agent-preload:poteto-agent'); + if (!/^(claude|codex|t3):[a-z][a-z0-9:-]+$/.test(value) || features.includes(value)) throw new Error('Feature must be a distinct HARNESS:FEATURE, such as claude:agent-preload:poteto-agent'); features.push(value); } else if (arg === '--wide') { if (!value.trim() || !publishable(value)) throw new Error('Wide reason must be nonblank printable ASCII, at most 160 characters, without backticks or angle brackets'); diff --git a/.claude/skills/verify-open-pstack/scripts/core.test.ts b/.claude/skills/verify-open-pstack/scripts/core.test.ts index 2360deab..c01b5a54 100644 --- a/.claude/skills/verify-open-pstack/scripts/core.test.ts +++ b/.claude/skills/verify-open-pstack/scripts/core.test.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path'; import registryData from '../features/registry.json'; import { recipe } from '../features/recipes.ts'; import { classify, completeEvidence, matches, newReceipt, requiredFeatures, requiredHarnesses, validateRegistry } from './core.ts'; -import { HARNESSES, type Receipt } from './types.ts'; +import { HARNESSES, LIVE_HARNESSES, type Receipt } from './types.ts'; import codexManifest from '../../../../plugins/pstack/.codex-plugin/plugin.json'; import claudeManifest from '../../../../plugins/pstack/.claude-plugin/plugin.json'; @@ -63,6 +63,18 @@ describe('registry and ownership', () => { expect(s.features).toContain('claude:agent-preload:poteto-agent'); expect(s.features).not.toContain('codex:agent-preload:poteto-agent'); }); + test('only the T3 dispatch surfaces reach t3:dispatch, and self-test never adds a T3 session', () => { + const reach = (filename: string) => classify([{ filename }], registry).features.includes('t3:dispatch'); + for (const filename of ['plugins/pstack/skills/poteto-mode/references/provider-dispatch.md', 'plugins/pstack/skills/poteto-mode/references/codex-tools.md', + 'plugins/pstack/skills/setup-pstack/SKILL.md', '.claude/skills/verify-open-pstack/features/recipes.ts']) expect(reach(filename)).toBe(true); + for (const filename of ['plugins/pstack/skills/poteto-mode/references/model-catalog.md', 'plugins/pstack/skills/poteto-mode/SKILL.md', + 'plugins/pstack/hooks/hooks.json', '.claude/skills/verify-open-pstack/scripts/harness.ts']) expect(reach(filename)).toBe(false); + const r = receipt(true); + expect(requiredHarnesses(r)).toEqual(['claude', 'codex']); + r.runFeatures = ['t3:dispatch']; + expect(requiredHarnesses(r)).toEqual(['claude', 'codex', 't3']); + expect(requiredFeatures(r, 't3')).toEqual(['dispatch']); + }); test('poteto-mode SKILL.md also reaches the Claude agent that preloads it', () => { const s = classify([{ filename: 'plugins/pstack/skills/poteto-mode/SKILL.md' }], registry); expect(s.features).toEqual([...both('skill-invocation:poteto-mode'), 'claude:agent-preload:poteto-agent'].sort()); @@ -187,13 +199,13 @@ describe('registry and ownership', () => { test('the shared recipe implementation reaches every implemented recipe and preserves project proof', () => { const path = '.claude/skills/verify-open-pstack/features/recipes.ts'; const expected = [...both('catalog', 'setup', 'runner', 'shipped-tools', 'hook:startup', 'project-skill', 'writers', - ...registry.skills.map(skill => `skill-invocation:${skill}`)), 'claude:agent-preload:poteto-agent'].sort(); + ...registry.skills.map(skill => `skill-invocation:${skill}`)), 'claude:agent-preload:poteto-agent', 't3:dispatch'].sort(); for (const file of [{ filename: path }, { filename: 'docs/moved.md', previous_filename: path }]) { const s = classify([file], registry); expect(s.features).toEqual(expected); expect(s.skills).toEqual([...registry.skills].sort()); expect(s.noRuntime).toBe(false); - for (const harness of HARNESSES) { + for (const harness of LIVE_HARNESSES) { for (const feature of s.features.filter(f => f.startsWith(`${harness}:`))) { expect(recipe(feature.slice(harness.length + 1), harness).length).toBeGreaterThan(0); } diff --git a/.claude/skills/verify-open-pstack/scripts/core.ts b/.claude/skills/verify-open-pstack/scripts/core.ts index 92bb9ffc..82ff7f40 100644 --- a/.claude/skills/verify-open-pstack/scripts/core.ts +++ b/.claude/skills/verify-open-pstack/scripts/core.ts @@ -1,4 +1,4 @@ -import { HARNESSES, REPO, type ChangedFile, type Harness, type Receipt, type Registry, type Selection } from './types.ts'; +import { HARNESSES, LIVE_HARNESSES, REPO, type ChangedFile, type Harness, type Receipt, type Registry, type Selection } from './types.ts'; export function validateRegistry(value: unknown): Registry { if (!value || typeof value !== 'object') throw new Error('Invalid feature registry'); @@ -17,6 +17,11 @@ export function matches(path: string, pattern: string): boolean { return pattern.endsWith('/**') ? path.startsWith(pattern.slice(0, -2)) : path === pattern; } +const T3_DISPATCH_PATHS = [ + 'plugins/pstack/skills/poteto-mode/references/provider-dispatch.md', 'plugins/pstack/skills/poteto-mode/references/codex-tools.md', + 'plugins/pstack/skills/setup-pstack/SKILL.md', '.claude/skills/verify-open-pstack/features/recipes.ts', +]; + export const VERSION_MANIFESTS: Readonly> = { 'plugins/pstack/.claude-plugin/plugin.json': 'claude', '.claude-plugin/marketplace.json': 'claude', 'plugins/pstack/.codex-plugin/plugin.json': 'codex', }; @@ -34,6 +39,7 @@ export function classify(files: ChangedFile[], registry: Registry): Selection { if (path !== 'plugins/pstack/assets/logo.png') throw new Error(`Unmapped asset consumer: ${path}`); features.add('codex:assets:codex'); covered = true; } + if (T3_DISPATCH_PATHS.includes(path)) features.add('t3:dispatch'); const sharedRecipe = path === '.claude/skills/verify-open-pstack/features/recipes.ts'; if (sharedRecipe) both('catalog', 'writers'); const catalog = registry.catalog.some(p => matches(path, p)); @@ -73,11 +79,12 @@ export function newReceipt(pr: number, sha: string, base: string, selfTest: bool export function requiredFeatures(receipt: Receipt, harness: Harness): string[] { const named = receipt.runFeatures.filter(f => f.startsWith(`${harness}:`)).map(f => f.slice(harness.length + 1)); - return [...new Set([...named, ...(receipt.selfTest ? ['project-skill'] : [])])]; + const selfTest = receipt.selfTest && (HARNESSES as readonly Harness[]).includes(harness); + return [...new Set([...named, ...(selfTest ? ['project-skill'] : [])])]; } export function requiredHarnesses(receipt: Receipt): Harness[] { - return HARNESSES.filter(harness => requiredFeatures(receipt, harness).length > 0); + return LIVE_HARNESSES.filter(harness => requiredFeatures(receipt, harness).length > 0); } export function completeEvidence(receipt: Receipt): void { diff --git a/.claude/skills/verify-open-pstack/scripts/doctor.ts b/.claude/skills/verify-open-pstack/scripts/doctor.ts index 1553935c..91fadcda 100644 --- a/.claude/skills/verify-open-pstack/scripts/doctor.ts +++ b/.claude/skills/verify-open-pstack/scripts/doctor.ts @@ -1,4 +1,4 @@ -import { mkdir, realpath } from 'node:fs/promises'; +import { mkdir, readFile, realpath } from 'node:fs/promises'; import { join } from 'node:path'; import { command, isolatedEnv, save, type Command } from './io.ts'; import { linkCodexAuth, removeCodexHome } from './isolation.ts'; @@ -7,6 +7,13 @@ import { HARNESSES, type Harness } from './types.ts'; export class LoginRequired extends Error {} const CLAUDE_FIX = 'run `claude auth login` from a shell on this Mac, not over SSH', CODEX_FIX = 'run `codex login` on this Mac'; +export async function t3Binary(): Promise { + const runtime = join(process.env.HOME ?? '', '.t3/runtime'); + const { activeVersion } = JSON.parse(await readFile(join(runtime, 'service-state.json'), 'utf8')); + if (typeof activeVersion !== 'string' || !/^\d[\w.-]*$/.test(activeVersion)) throw new Error('T3 service state names no active version'); + return join(runtime, 'versions', activeVersion, 't3'); +} + export async function doctor(root: string, run: Command = command, platform = process.platform, candidate = false, harnesses: readonly Harness[] = HARNESSES): Promise { const report: Record = { platform, candidate, date: new Date().toISOString(), skill: await realpath(join(import.meta.dir, '..')), checks: {} }; const checks = report.checks as Record; @@ -16,6 +23,7 @@ export async function doctor(root: string, run: Command = command, platform = pr for (const dir of ['tmp', '.codex', '.config/gh', '.cache']) await mkdir(join(home, dir), { recursive: true, mode: 0o700 }); const env = isolatedEnv(home); for (const binary of candidate ? ['bun', 'git', 'claude', 'codex'] : ['bun', 'git', 'claude', 'codex', 'gh']) checks[binary] = (await run([binary, '--version'], { env })).trim(); + if (harnesses.includes('t3')) checks.t3 = (await run([await t3Binary(), '--version'], { env })).trim(); const claude = await run(['claude', '--help'], { env }); for (const flag of ['--plugin-dir', '--settings']) if (!claude.includes(flag)) throw new Error(`Claude isolation missing ${flag}`); checks.claudeHelp = claude; @@ -25,7 +33,7 @@ export async function doctor(root: string, run: Command = command, platform = pr if (args.includes('marketplace') && !/local path|local or Git|local marketplace/i.test(help)) throw new Error('Codex local marketplace interface unavailable'); checks[args.join(' ')] = help; } - if (!candidate && harnesses.includes('claude')) { + if (!candidate && (harnesses.includes('claude') || harnesses.includes('t3'))) { let loggedIn: unknown; try { loggedIn = JSON.parse(await run(['claude', 'auth', 'status'], { env })).loggedIn; } catch {} if (loggedIn !== true) throw new LoginRequired(`Claude login check failed: \`claude auth status\` did not report "loggedIn": true; ${CLAUDE_FIX}`); diff --git a/.claude/skills/verify-open-pstack/scripts/harness.ts b/.claude/skills/verify-open-pstack/scripts/harness.ts index d5d59d5b..da0feb87 100644 --- a/.claude/skills/verify-open-pstack/scripts/harness.ts +++ b/.claude/skills/verify-open-pstack/scripts/harness.ts @@ -1,16 +1,18 @@ -import { copyFile, mkdir, readdir, readFile, realpath, stat } from 'node:fs/promises'; +import { Database } from 'bun:sqlite'; +import { copyFile, mkdir, open, readdir, readFile, realpath, stat, writeFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; import { CLAUDE_TOOLS, recipe, type Fixture } from '../features/recipes.ts'; import { requiredFeatures, requiredHarnesses } from './core.ts'; import { command, isolatedEnv, retainedFile, save, session, treeHash, type Command, type Session } from './io.ts'; -import { doctor } from './doctor.ts'; +import { doctor, t3Binary } from './doctor.ts'; import { assertCodexAuth, linkCodexAuth, linkMuseConfig, removeCodexHome, restoreSetup, snapshotSetup, type SetupSnapshot } from './isolation.ts'; import { sourceDigest, sourceHash } from './provenance.ts'; import { publishable } from './verify.ts'; import { REPO, type Driver, type Harness, type Installation, type Observation, type Receipt, type SessionRecord } from './types.ts'; +const T3_PARENT_MODEL = 'claude-sonnet-5-5'; const setupFeature = (feature: string): boolean => feature === 'setup' || feature === 'skill-invocation:setup-pstack'; -export function launch(harness: Harness, workspace: string, options: { cwd: string; addDirs: string[]; codexSandbox?: 'danger-full-access'; claudePermission?: 'bypassPermissions' }): string[] { +export function launch(harness: Exclude, workspace: string, options: { cwd: string; addDirs: string[]; codexSandbox?: 'danger-full-access'; claudePermission?: 'bypassPermissions' }): string[] { const addDirs = options.addDirs.flatMap(dir => ['--add-dir', dir]); return harness === 'claude' ? ['claude', '-p', '--input-format', 'stream-json', '--replay-user-messages', '--output-format', 'stream-json', '--verbose', '--no-session-persistence', @@ -31,16 +33,18 @@ function resultText(blocks: Event[], id: string): string | undefined { export function parseSession(harness: Harness, text: string, exitCode: number, started: number, rollout = ''): SessionRecord { if (exitCode !== 0) throw new Error(`exit:${exitCode}`); const stream: Event[] = text.split('\n').filter(line => line.trim()).map(line => JSON.parse(line)); - if (harness === 'claude') { + if (harness !== 'codex') { const blocks = stream.filter(e => e.type === 'assistant' || e.type === 'user').flatMap(e => Array.isArray(e.message?.content) ? e.message.content as Event[] : []); const failed = new Set(blocks.filter(b => b.type === 'tool_result' && b.is_error).map(b => b.tool_use_id)); const uses = blocks.filter(b => b.type === 'tool_use' && !failed.has(b.id)); const commands = stream.filter(e => e.type === 'user' && e.isReplay && typeof e.message?.content === 'string') .flatMap(e => [...(e.message.content as string).matchAll(/\/([^<\s]+)<\/command-name>/g)].map(m => m[1]!)); + const skillDirs = blocks.filter(b => b.type === 'text' && typeof b.text === 'string') + .flatMap(b => [...(b.text as string).matchAll(/^Base directory for this skill: (.+)$/gm)].map(m => m[1]!)); const context = stream.filter(e => e.type === 'system' && e.subtype === 'hook_response' && e.hook_event === 'SessionStart' && e.exit_code === 0 && e.outcome === 'success' && typeof e.stdout === 'string').map(e => e.stdout as string); return { harness, exitCode, started, context, - skills: [...commands, ...uses.filter(u => u.name === 'Skill' && typeof u.input?.skill === 'string').map(u => u.input.skill as string)], + skills: harness === 't3' ? skillDirs : [...commands, ...uses.filter(u => u.name === 'Skill' && typeof u.input?.skill === 'string').map(u => u.input.skill as string)], commands: uses.filter(u => u.name === 'Bash' && typeof u.input?.command === 'string').map(u => u.input.command as string), tools: stream.filter(e => e.type === 'assistant' && Array.isArray(e.message?.content)).flatMap(e => (e.message.content as Event[]).filter(b => b.type === 'tool_use') .map(b => ({ name: String(b.name), id: String(b.id), input: b.input ?? {}, parent: e.parent_tool_use_id ?? null, failed: failed.has(b.id), result: resultText(blocks, b.id) }))) }; @@ -107,7 +111,7 @@ export class MacDriver implements Driver { const home = join(root, 'state', harness), workspace = join(home, 'workspace'); await mkdir(join(home, 'tmp'), { recursive: true, mode: 0o700 }); for (const dir of ['.codex', '.config/gh', '.cache']) await mkdir(join(home, dir), { recursive: true, mode: 0o700 }); - const env = isolatedEnv(home, harness); + const env = isolatedEnv(home); const candidateRun = this.candidate(home); await candidateRun(['git', 'clone', '--no-checkout', '--', `https://github.com/${REPO}.git`, workspace], { env }); await candidateRun(['git', 'fetch', 'origin', receipt.sha], { cwd: workspace, env }); @@ -129,10 +133,10 @@ export class MacDriver implements Driver { verifyCodexEnabled(listed); await save(join(root, 'codex-plugins.json'), JSON.parse(listed)); } - const manifest = JSON.parse(await readFile(join(location, harness === 'claude' ? '.claude-plugin/plugin.json' : '.codex-plugin/plugin.json'), 'utf8')); + const manifest = JSON.parse(await readFile(join(location, harness === 'codex' ? '.codex-plugin/plugin.json' : '.claude-plugin/plugin.json'), 'utf8')); if (await sourceDigest(workspace) !== pinnedSource) throw new Error('Pinned source changed during installation'); installs.push({ harness, sha: receipt.sha, home, location, treeHash: expected, sourceHash: pinnedSource, pluginVersion: manifest.version, - cliVersion: (await candidateRun([harness, '--version'], { env })).trim() }); + cliVersion: (await candidateRun([harness === 't3' ? await t3Binary() : harness, '--version'], { env })).trim() }); } return installs; } @@ -145,6 +149,41 @@ export class MacDriver implements Driver { await copyFile(join(sessions, name), copy); return readFile(copy, 'utf8'); } + private async t3Thread(location: string, project: string, base: string, env: Record, prompt: string, out: { thread: string; stderr: string; transcript: string }): Promise { + const bin = await t3Binary(); + await mkdir(join(base, 'userdata'), { recursive: true, mode: 0o700 }); + await writeFile(join(base, 'userdata/settings.json'), JSON.stringify({ providerInstances: { claudeAgent: { driver: 'claudeAgent', + config: { launchArgs: `--plugin-dir '${location}' --settings '{"enabledPlugins":{"pstack@open-pstack":false}}'` } } } })); + const listener = Bun.listen({ hostname: '127.0.0.1', port: 0, socket: { data() {} } }), port = listener.port; + listener.stop(true); + const log = await open(join(base, 'serve.log'), 'wx', 0o600); + const server = Bun.spawn([bin, 'serve', '--base-dir', base, '--port', String(port), '--host', '127.0.0.1', project], + { cwd: project, env, detached: true, stdin: 'ignore', stdout: log.fd, stderr: log.fd }); + try { + while (!await Bun.file(join(base, 'userdata/server-runtime.json')).exists()) { + if (server.exitCode !== null) throw new Error(`t3-serve-exit:${server.exitCode}`); + await Bun.sleep(250); + } + await this.run([bin, 'project', 'add', '--base-dir', base, project], { env }); + const code = await this.record([bin, 'thread', 'start', '--base-dir', base, '--provider', 'claudeAgent', '--model', T3_PARENT_MODEL, '--wait', '--json', project, '-'], + { cwd: project, env, input: prompt, stdout: out.thread, stderr: out.stderr }); + if (code !== 0) throw new Error(`t3-thread-exit:${code}`); + } finally { + try { process.kill(-server.pid, 'SIGTERM'); } catch (error) { if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error; } + await server.exited; + await log.close(); + } + const thread = JSON.parse(await readFile(out.thread, 'utf8')); + if (thread.status !== 'completed') throw new Error(`t3-thread-status:${thread.status}`); + const db = new Database(join(base, 'userdata/statev2.sqlite'), { readwrite: true }); + let rows: { payload_json: string }[]; + try { rows = db.query("select payload_json from orchestration_v2_projection_provider_threads where thread_id = ? and provider = 'claudeAgent'").all(thread.threadId) as typeof rows; } + finally { db.close(); } + const session = rows.length === 1 ? JSON.parse(rows[0]!.payload_json).nativeThreadRef?.nativeId : undefined; + if (typeof session !== 'string' || !/^[0-9a-f-]{36}$/.test(session)) throw new Error('t3-native-session-missing'); + const projects = join(env.CLAUDE_CONFIG_DIR ?? join(env.HOME!, '.claude'), 'projects'); + await copyFile(join(projects, (await realpath(project)).replace(/[^A-Za-z0-9]/g, '-'), `${session}.jsonl`), out.transcript); + } async exercise(receipt: Receipt): Promise { const plans = receipt.installations.map(installation => ({ installation, features: requiredFeatures(receipt, installation.harness).sort((a, b) => Number(setupFeature(a)) - Number(setupFeature(b))) @@ -153,22 +192,24 @@ export class MacDriver implements Driver { for (const { installation, features } of plans) { const { harness, home } = installation, workspace = join(home, 'workspace'); if (await sourceDigest(workspace) !== installation.sourceHash) throw new Error('Pinned source changed before exercise'); - const env = isolatedEnv(home, harness); + const env = isolatedEnv(home); for (const { feature, cases } of features) { const slug = feature.replace(/[^a-z0-9-]/g, '-'), dir = join(home, 'fixtures', slug), records = join(receipt.artifactRoot, 'sessions', harness, slug); for (const path of [dir, records]) await mkdir(path, { recursive: true, mode: 0o700 }); const fixture: Fixture = { harness, dir, workspace, location: installation.location, run: this.candidate(home), - configHome: harness === 'claude' ? env.CLAUDE_CONFIG_DIR ?? join(env.HOME!, '.claude') : env.CODEX_HOME! }; + configHome: harness === 'codex' ? env.CODEX_HOME! : env.CLAUDE_CONFIG_DIR ?? join(env.HOME!, '.claude') }; const assertions: string[] = [], evidence: string[] = []; if (setupFeature(feature)) this.setup = await snapshotSetup(); try { for (const c of cases) { await c.prepare?.(fixture); const stdout = join(records, `${c.id}.jsonl`), stderr = join(records, `${c.id}.stderr`), cwd = c.cwd === 'workspace' ? workspace : dir; - const started = Date.now(); - const exitCode = await this.record(launch(harness, workspace, { cwd, addDirs: cwd === dir ? [] : [dir], codexSandbox: c.codexSandbox, claudePermission: c.claudePermission }), + const started = Date.now(), thread = join(records, `${c.id}.thread.json`); + let exitCode = 0; + if (harness === 't3') await this.t3Thread(installation.location, cwd, join(home, 'server', `${slug}.${c.id}`), env, c.prompt(fixture), { thread, stderr, transcript: stdout }); + else exitCode = await this.record(launch(harness, workspace, { cwd, addDirs: cwd === dir ? [] : [dir], codexSandbox: c.codexSandbox, claudePermission: c.claudePermission }), { cwd, env, input: sessionInput(harness, c.prompt(fixture)), stdout, stderr }); - evidence.push(stdout, ...(await stat(stderr)).size ? [stderr] : []); + evidence.push(stdout, ...harness === 't3' ? [thread] : [], ...(await stat(stderr)).size ? [stderr] : []); let record: SessionRecord, result; try { const text = await readFile(stdout, 'utf8'); @@ -192,7 +233,8 @@ export class MacDriver implements Driver { const [transcript, ...artifacts] = await Promise.all(evidence.map(path => retainedFile(receipt.artifactRoot, path))); const passed = `passed: ${assertions.join(' ')}`; observations.push({ harness, feature, reviewer: 'recipe', assertions, - surface: harness === 'claude' ? 'claude -p stream-json with the candidate plugin-dir' : 'codex exec --json with the run-owned candidate installation', + surface: { claude: 'claude -p stream-json with the candidate plugin-dir', codex: 'codex exec --json with the run-owned candidate installation', + t3: 't3 thread start on a run-owned T3 server whose Claude provider loads the candidate plugin-dir' }[harness], action: `recipe ${feature}: ${cases.map(c => c.id).join(', ')}`, observed: publishable(passed) ? passed : `passed ${assertions.length} assertions; ids retained privately`, transcript: transcript!.path, transcriptHash: transcript!.sha256, artifacts }); diff --git a/.claude/skills/verify-open-pstack/scripts/types.ts b/.claude/skills/verify-open-pstack/scripts/types.ts index 28278857..3aa6cb0e 100644 --- a/.claude/skills/verify-open-pstack/scripts/types.ts +++ b/.claude/skills/verify-open-pstack/scripts/types.ts @@ -1,6 +1,8 @@ export const REPO = 'ericlitman/open-pstack'; export const HARNESSES = ['claude', 'codex'] as const; -export type Harness = typeof HARNESSES[number]; +export type CoreHarness = typeof HARNESSES[number]; +export const LIVE_HARNESSES = [...HARNESSES, 't3'] as const; +export type Harness = typeof LIVE_HARNESSES[number]; export type Phase = 'resolve' | 'classify' | 'prepare' | 'exercise' | 'publish' | 'failed' | 'head-moved'; export interface Pull { number: number; head: { sha: string }; base: { sha: string }; From 30ba22997b0f6c36e1a763117be97da3191ae189 Mon Sep 17 00:00:00 2001 From: Eric Litman Date: Fri, 6 Nov 2026 11:35:53 -0500 Subject: [PATCH 7/7] chore: release T3 Code as a third harness as 1.19.0 Refs #253 Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 2 +- CHANGES.md | 4 +++- README.md | 2 +- UPSTREAM.md | 4 ++-- docs/reference.md | 2 +- plugins/pstack/.claude-plugin/plugin.json | 2 +- plugins/pstack/.codex-plugin/plugin.json | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index f5af0092..1dc9b976 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.18.0", + "version": "1.19.0", "author": { "name": "Lauren Tan (original)" }, diff --git a/CHANGES.md b/CHANGES.md index aa26052a..ad376c00 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,9 @@ Open Pstack lands on green: Mergify merges a ready PR to main once verify and exact-head live-gate pass and no review thread is unresolved, and a workflow resolves Unfret's own threads. A PR that touches `.github/` or the Mergify config also waits for Unfret, and workflow changes require operator queueing. This port applies the Cursor → Claude Code substitutions in skill bodies. Earlier drafts left them flagged; this revision resolves them. A later pass added a Codex build that shares the same skills; see [Codex port](#codex-port) below. -## Unreleased +## 1.19.0 adds T3 Code as a third harness + +Open Pstack 1.19.0 keeps the Cursor pstack 0.15.36 sync. Default roles and panels are unchanged. **T3 Code is a third parent harness (#253).** Started from fikaventures' PR #254. In a T3 Code session, pstack still chooses each role's model and effort. A lane whose provider T3 offers and has enabled, with the model and effort, goes through T3's `delegate_task`. Any other lane takes the route a standalone Claude Code or Codex parent gives it, and a lane neither route can run is a named dropout. `inherit-parent` and `auto` go through `delegate_task` with no model override. A T3 result is T3 launch-configuration evidence, not a served-model report. A read-only lane on T3 is advisory, and a caller that needs enforced read-only access takes the standalone route. `/setup-pstack` runs from a T3 session: it probes T3-routed families through `delegate_task` and the rest as before, and runs its smoke panel through the same routes. diff --git a/README.md b/README.md index 186ec52b..032d9e92 100644 --- a/README.md +++ b/README.md @@ -173,7 +173,7 @@ This repository also keeps: ## Staying close to Lauren's pstack -Open Pstack 1.18.0 tracks pstack 0.15.36 at Cursor commit [`2776ddd2355d6d6c8747d3964600f28daf62f049`](https://github.com/cursor/plugins/commit/2776ddd2355d6d6c8747d3964600f28daf62f049). +Open Pstack 1.19.0 tracks pstack 0.15.36 at Cursor commit [`2776ddd2355d6d6c8747d3964600f28daf62f049`](https://github.com/cursor/plugins/commit/2776ddd2355d6d6c8747d3964600f28daf62f049). The two projects have separate version numbers. The pstack version identifies Lauren's upstream content. The Open Pstack version identifies the Claude Code and Codex package built from it. diff --git a/UPSTREAM.md b/UPSTREAM.md index 950f167f..a330694d 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 | `2776ddd2355d6d6c8747d3964600f28daf62f049` | | Upstream version | `0.15.36` | -| open-pstack version | `1.18.0` | +| open-pstack version | `1.19.0` | -The table above is the current Cursor sync point. Open Pstack 1.17.0 imported this 0.15.36 sync, and 1.18.0 keeps it. `README-UPSTREAM.md` preserves the upstream 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.17.0 imported this 0.15.36 sync, and 1.18.0 and 1.19.0 keep it. `README-UPSTREAM.md` preserves the upstream 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 9a830649..8c7e20d7 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -2,7 +2,7 @@ This page contains the full skill, dependency, runtime, and porting reference. For the plain-English introduction and quick start, see the [main README](../README.md). -[Poteto](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack), adapted to run in Claude Code, Codex and T3 Code without Cursor. One shared skill tree serves all three harnesses, and in T3 Code the Claude Code or Codex runtime loads it; lanes on other providers' models, including Grok and seven more providers, run through the bundled runner. Version 1.17.0 is synced to Cursor pstack v0.15.36 at `2776ddd2355d6d6c8747d3964600f28daf62f049`. See [UPSTREAM.md](../UPSTREAM.md) for the exact sync contract. +[Poteto](https://x.com/poteto)'s [pstack](https://github.com/cursor/plugins/tree/main/pstack), adapted to run in Claude Code, Codex and T3 Code without Cursor. One shared skill tree serves all three harnesses, and in T3 Code the Claude Code or Codex runtime loads it; lanes on other providers' models, including Grok and seven more providers, run through the bundled runner. Version 1.19.0 is synced to Cursor pstack v0.15.36 at `2776ddd2355d6d6c8747d3964600f28daf62f049`. See [UPSTREAM.md](../UPSTREAM.md) for the exact sync contract. Original by Lauren Tan. This distribution builds on Michael Denyer's [pstack-claude](https://github.com/michael-denyer/pstack-claude) port and retains its history and MIT attribution. It imports seven MIT-licensed skills from [cursor-team-kit](https://github.com/cursor/plugins/tree/main/cursor-team-kit): `deslop`, `thermo-nuclear-code-quality-review`, `make-pr-easy-to-review`, `fix-ci`, `fix-merge-conflicts`, `get-pr-comments`, `what-did-i-get-done`. diff --git a/plugins/pstack/.claude-plugin/plugin.json b/plugins/pstack/.claude-plugin/plugin.json index ac501fa1..a6dbe8ce 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.18.0", + "version": "1.19.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 dfdca662..b65a3136 100644 --- a/plugins/pstack/.codex-plugin/plugin.json +++ b/plugins/pstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "pstack", - "version": "1.18.0", + "version": "1.19.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"