Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"name": "pstack",
"source": "./plugins/pstack",
"description": "if you want to go fast, go deep first. pstack helps you write less, but higher quality code. rigorous agent workflows you can parallelize with confidence.",
"version": "1.3.0",
"version": "1.3.0-codex.2",
"author": {
"name": "Lauren Tan (original)"
},
Expand Down
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This port applies the Cursor → Claude Code substitutions in skill bodies. Earlier drafts left them flagged; this revision resolves them. A later pass added a Codex build that shares the same skills; see [Codex port](#codex-port) below.

## 1.3.0-codex.2 local candidate

Codex setup accepts optional model families and explicitly loaded provider extensions while retaining the default Claude panel. Native Codex dispatch carries the selected model and effort with a compatible history setting. Claude output parsing supports terminal results inside event arrays, and Grok preflight recognizes API-key authentication while verifying exact model availability.

This candidate is installed from the maintained fork for live Codex verification. It does not change the Cursor sync point or the existing Claude model sheet.

## 1.3.0 syncs to Cursor pstack 0.14.7

Open Pstack now tracks Cursor pstack 0.14.7 at `efa2a531985e0a8084d36ff3cf87233be8a9f34b`.
Expand Down
4 changes: 2 additions & 2 deletions UPSTREAM.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ open-pstack tracks [Cursor's pstack](https://github.com/cursor/plugins/tree/main
| Path | `pstack/` |
| Commit | `efa2a531985e0a8084d36ff3cf87233be8a9f34b` |
| Upstream version | `0.14.7` |
| open-pstack version | `1.3.0` |
| open-pstack version | `1.3.0-codex.2` |

The table above is the current Cursor sync point. Open Pstack 1.3.0 keeps this 0.14.7 sync. `README-UPSTREAM.md` preserves its pstack README verbatim. `CHANGES.md` and `NOTICE.md` describe the adaptations and provenance.
The table above is the current Cursor sync point. Open Pstack 1.3.0-codex.2 keeps this 0.14.7 sync. `README-UPSTREAM.md` preserves its pstack README verbatim. `CHANGES.md` and `NOTICE.md` describe the adaptations and provenance.

## Upstream-only exclusions

Expand Down
2 changes: 1 addition & 1 deletion plugins/pstack/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "pstack",
"displayName": "pstack",
"version": "1.3.0",
"version": "1.3.0-codex.2",
"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"
Expand Down
2 changes: 1 addition & 1 deletion plugins/pstack/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pstack",
"version": "1.3.0",
"version": "1.3.0-codex.2",
"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"
Expand Down
5 changes: 3 additions & 2 deletions plugins/pstack/skills/poteto-mode/references/codex-tools.md
Original file line number Diff line number Diff line change
@@ -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 built-in and optional extension routes and provider-qualified descriptors.

## Tool actions

Expand Down Expand Up @@ -35,14 +35,15 @@ Without it, the native Codex lane is a named dropout. Independent external lanes
poteto-mode's Subagents section sets Claude-specific defaults (`subagent_type: "poteto-agent"`, `run_in_background: true`). On Codex:

- There is no `poteto-agent` subagent type. Route an ad-hoc subagent through poteto-mode's style by dispatching a `spawn_agent` whose instructions tell it to read the `poteto-mode` skill in full first.
- Model or effort overrides use `fork_turns: "none"` and a complete task with grounding paths; full-history forks inherit the parent and cannot carry these overrides.
- `spawn_agent` calls already run concurrently with your turn, so `run_in_background: true` has no separate flag. Issue the dispatch and continue.
- There is no `comment-sicko` subagent type either. The **no-comments** skill spawns it on Claude Code; on Codex dispatch a `spawn_agent` whose instructions tell it to read `agents/comment-sicko.md` in full first.
- Claude Code runs every subagent on this machine, so the **swarm** skill's workers and the fan-out playbooks (`orchestrate`, `autopilot-full`, `autopilot-stack`) isolate writers with worktrees. The same holds on Codex.
- Keep the rest of the policy unchanged. Pass file pointers not inlined context, give each worker its own worktree or branch when they write, review every subagent's diff yourself.

## Models and providers

Do not replace every configured entry with a Codex model. `/setup-pstack` writes portable descriptors such as `claude:fable@max`, `codex:gpt-5.6-sol@max`, and `grok:grok-4.6@xhigh`. In a Codex parent, only `codex:*` is native. Route Claude and Grok descriptors through the external launcher exactly as `provider-dispatch.md` specifies. The current default panel intentionally keeps four-provider frontier diversity and contains no older GPT or Claude substitute.
Do not replace every configured entry with a Codex model. `/setup-pstack` writes portable descriptors such as `claude:fable@max`, `codex:gpt-5.6-sol@max`, and `grok:grok-4.6@xhigh`. In a Codex parent, only `codex:*` is native. Route Claude and Grok descriptors through the external launcher exactly as `provider-dispatch.md` specifies. The default panel has four model families across three providers. Optional Astra is also native. If the sheet contains `Dispatch extension: <path>`, read it before validating or dispatching extension descriptors and invoke its declared launcher directly. The sheet remains the only role configuration; the extension supplies routes and evidence. User choices and effort caps override examples.

## Claude built-in skills pstack references

Expand Down
25 changes: 17 additions & 8 deletions plugins/pstack/skills/poteto-mode/references/provider-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,26 @@ pstack model choices are provider-qualified descriptors:

## Model matrix

| Family | Upstream pstack choice | Provider | Model | Default effort | Selectable efforts | Claude-native agent stem |
|---|---|---|---|---|---|---|
| fable | fable | claude | fable | max | low medium high xhigh max | fable |
| sol | gpt-5.6-sol-max | codex | gpt-5.6-sol | max | low medium high xhigh max | - |
| grok | grok-4.6-fast-xhigh | grok | grok-4.6 | xhigh | low medium high xhigh max | - |
| opus | opus | claude | opus | xhigh | low medium high xhigh max | opus |
| Family | Upstream pstack choice | Provider | Model | Default effort | Selectable efforts | Claude-native agent stem | First-run active |
|---|---|---|---|---|---|---|---|
| fable | fable | claude | fable | max | low medium high xhigh max | fable | yes |
| sol | gpt-5.6-sol-max | codex | gpt-5.6-sol | max | low medium high xhigh max | - | yes |
| grok | grok-4.6-fast-xhigh | grok | grok-4.6 | xhigh | low medium high xhigh max | - | yes |
| opus | opus | claude | opus | xhigh | low medium high xhigh max | opus | yes |
| astra | - | codex | gpt-6-astra | medium | low medium high xhigh max | - | no |

The allowed effort universe is exactly `low`, `medium`, `high`, `xhigh`, `max`. First-run requested efforts are the Default effort cell of each row. A Claude-native agent stem of `-` means the family has no Claude-native agent. Otherwise the shipped agent name is `pstack-<stem>-<effort>`.
The allowed effort universe is exactly `low`, `medium`, `high`, `xhigh`, `max`. The default panel selects Fable, Sol, Grok, and Opus in that order. Rows with First-run active `yes` seed a missing sheet; otherwise derive selected families from the loaded role map and explicit user changes. Optional families are selected only by a loaded role descriptor or an explicit user choice. Defaults propose effort only for selected families without a supplied or current value. Explicit user model/effort choices override examples; user effort caps constrain every selection and probe, including loaded values. Never probe above a cap. Reuse choices already supplied in the conversation. A Claude-native agent stem of `-` means the family has no Claude-native agent. Otherwise the shipped agent name is `pstack-<stem>-<effort>`.

`fable` and `opus` are Claude Code's rolling aliases. Claude resolves each alias to the latest available family revision. A runner receipt keeps the requested alias in `model` and the concrete provider-reported revision in `reportedModel`; verification accepts only a numeric `claude-fable-*` or `claude-opus-*` revision from the matching family.

## Optional dispatch extensions

When the current model sheet contains a line `Dispatch extension: <path>`, read that reference before validating or dispatching any role. Setup may also load a path explicitly supplied by the user and persist this pointer in the sheet. Expand `~` for the current user; resolve relative paths against the model sheet directory. A missing reference is inconsistent state.

An extension declares additional provider/model families, selectable efforts, a proposed effort, parent-specific launcher argv, and authentication/model/completion evidence. It adds routing capabilities, never role assignments or a second mutable model configuration. Require unique families and provider/model pairs; extensions cannot override built-in families, routes, aliases, or the user's effort caps. Only families selected by a role descriptor or the user participate in setup. Persist all selected families in the role map.

The parent routes an extension descriptor directly to its declared launcher, with the same unique paths, retained background handle, access boundary, and receipt checks as built-in external lanes. Read the launcher's help before first use. Never send an extension provider to the built-in runner or reinterpret it as a native model. Shared skills consume this contract through the model sheet; no runtime resolver or machine-specific dependency is required.
Comment on lines +27 to +29

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Extension Format Is Undefined

The new dispatch-extension contract names the required data but does not define concrete syntax, placeholders, parsing rules, or a canonical example for family rows, launcher arguments, and evidence. Independently authored extensions therefore cannot be validated or invoked consistently, which can cause valid metadata to be rejected or paths and arguments to be substituted incorrectly. Please specify and test an unambiguous extension format before setup executes declared launchers.


## Read-time normalization

Normalize configured descriptors before matching them to the matrix or choosing a route. If a provider-qualified Claude model starts with `claude-fable-` or `claude-opus-` and its remaining revision contains only digits and hyphens, replace that model component in memory with `fable` or `opus`. Preserve provider, effort, role, and lane order. Use only the normalized descriptor for native dispatch or runner argv. Never pass the versioned predecessor to Claude.
Expand All @@ -43,7 +52,7 @@ The top-level harness resolves the route once. A child receives an assigned prov
Native dispatch avoids a second CLI startup and its base context.

- Claude Code: match the descriptor's `(provider, model)` to one model-matrix row, then dispatch it through `pstack-<stem>-<effort>` using that row's Claude-native agent stem and the descriptor's effort. Those definitions select the rolling model alias, requested effort, and `background: true`. `pstack-fable-max` and `pstack-opus-xhigh` remain in that set. Pass the complete task, grounding paths, access mode, and unique output location in the `Agent` prompt. Retain the task handle and drain it only after fan-out.
- Codex: call `spawn_agent` with the descriptor's model and `reasoning_effort`, the complete task, grounding paths, access mode, and unique output location. Use an isolated worktree for a writer. Codex subagents already run concurrently.
- Codex: call `spawn_agent` with the descriptor's model, `reasoning_effort`, and `fork_turns: "none"` (full-history forks cannot override model or effort), the complete task, grounding paths, access mode, and unique output location. Use an isolated worktree for a writer. Codex subagents already run concurrently.

Do not send a same-provider descriptor to the external runner. It rejects that call because the native route is cheaper and already available.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ const MATRIX_HEADER = [
"Default effort",
"Selectable efforts",
"Claude-native agent stem",
"First-run active",
] as const;

const FAMILY_ORDER = ["fable", "sol", "grok", "opus"] as const;
const DEFAULT_FAMILY_ORDER = ["fable", "sol", "grok", "opus"] as const;
const PROVIDERS = ["claude", "codex", "grok"] as const;
const DESCRIPTOR_RE =
/(claude|codex|grok):[a-z0-9.-]+@(low|medium|high|xhigh|max)/g;
Expand Down Expand Up @@ -54,7 +55,7 @@ const SETUP_SECTION_ORDER = [
"### 2. Load current state",
"### 3. Parse per-family efforts",
"### 4. Collect one requested effort per family",
"### 5. Probe the four requested pairs",
"### 5. Probe the selected pairs",
"### 6. Render, preserving role families",
"### 7. Confirm and commit",
] as const;
Expand All @@ -67,6 +68,7 @@ interface MatrixRow {
defaultEffort: Effort;
selectableEfforts: Effort[];
claudeNativeAgentStem: string | null;
firstRunActive: boolean;
}

function splitRow(line: string): string[] {
Expand All @@ -91,6 +93,12 @@ function asEffort(value: string): Effort {
throw new Error(`not an effort: ${value}`);
}

function asFirstRunActive(value: string): boolean {
if (value === "yes") return true;
if (value === "no") return false;
throw new Error(`invalid First-run active value: ${value}`);
}

function parseModelMatrix(markdown: string): MatrixRow[] {
const lines = markdown.split(/\r?\n/);
const start = lines.findIndex((line) => line.trim() === "## Model matrix");
Expand All @@ -108,9 +116,10 @@ function parseModelMatrix(markdown: string): MatrixRow[] {
.slice(start + 1, end)
.map((line) => line.trim())
.filter((line) => line.startsWith("|"));
if (table.length !== 6) {
const minimumRows = DEFAULT_FAMILY_ORDER.length + 2;
if (table.length < minimumRows) {
throw new Error(
`model matrix must be header, separator, and 4 data rows, got ${table.length}`
`model matrix must contain the default families, got ${table.length - 2} data rows`
);
}
const header = splitRow(table[0]);
Expand All @@ -120,7 +129,7 @@ function parseModelMatrix(markdown: string): MatrixRow[] {
if (!isSeparator(splitRow(table[1]))) {
throw new Error("matrix header separator missing");
}
return table.slice(2).map((line) => {
const rows = table.slice(2).map((line) => {
const cells = splitRow(line);
if (cells.length !== MATRIX_HEADER.length) {
throw new Error(`matrix row has ${cells.length} cells: ${line}`);
Expand All @@ -133,10 +142,14 @@ function parseModelMatrix(markdown: string): MatrixRow[] {
defaultEffortRaw,
selectableRaw,
stemRaw,
firstRunActiveRaw,
] = cells;
if (!(PROVIDERS as readonly string[]).includes(provider)) {
throw new Error(`invalid provider: ${provider}`);
}
if (!/^[a-z0-9.-]+$/.test(model)) {
throw new Error(`invalid model for ${family}: ${model}`);
}
const selectableEfforts = selectableRaw.split(/\s+/).map(asEffort);
const claudeNativeAgentStem = stemRaw === "-" ? null : stemRaw;
if (claudeNativeAgentStem !== null && !/^[a-z0-9-]+$/.test(claudeNativeAgentStem)) {
Expand All @@ -146,6 +159,7 @@ function parseModelMatrix(markdown: string): MatrixRow[] {
throw new Error(`${family} stem must be present iff provider is claude`);
}
const defaultEffort = asEffort(defaultEffortRaw);
const firstRunActive = asFirstRunActive(firstRunActiveRaw);
if (!selectableEfforts.includes(defaultEffort)) {
throw new Error(`${family} default effort is not selectable`);
}
Expand All @@ -157,8 +171,35 @@ function parseModelMatrix(markdown: string): MatrixRow[] {
defaultEffort,
selectableEfforts,
claudeNativeAgentStem,
firstRunActive,
};
});

const families = new Set<string>();
const providerModels = new Set<string>();
for (const row of rows) {
if (families.has(row.family)) {
throw new Error(`duplicate family: ${row.family}`);
}
families.add(row.family);

const providerModel = `${row.provider}:${row.model}`;
if (providerModels.has(providerModel)) {
throw new Error(`duplicate provider/model: ${providerModel}`);
}
providerModels.add(providerModel);
}

const firstRunFamilies = rows
.filter((row) => row.firstRunActive)
.map((row) => row.family);
if (firstRunFamilies.join("|") !== DEFAULT_FAMILY_ORDER.join("|")) {
throw new Error(
`unexpected first-run families: ${firstRunFamilies.join(", ")}`
);
}

return rows;
}

function defaultDescriptors(rows: MatrixRow[]): string[] {
Expand Down Expand Up @@ -200,13 +241,17 @@ function firstRunSheet(setup: string): string {
}

describe("model matrix", () => {
const rows = parseModelMatrix(readFileSync(DISPATCH_PATH, "utf8"));
const dispatch = readFileSync(DISPATCH_PATH, "utf8");
const rows = parseModelMatrix(dispatch);
const defaultRows = rows.filter((row) => row.firstRunActive);
const setup = readFileSync(SETUP_PATH, "utf8");
const quad = defaultDescriptors(rows);
const quad = defaultDescriptors(defaultRows);

it("owns the effort universe and first-run defaults", () => {
expect([...EFFORTS]).toEqual(["low", "medium", "high", "xhigh", "max"]);
expect(rows.map((row) => row.family)).toEqual([...FAMILY_ORDER]);
expect(defaultRows.map((row) => row.family)).toEqual([
...DEFAULT_FAMILY_ORDER,
]);
for (const row of rows) {
expect(row.upstreamChoice.length).toBeGreaterThan(0);
expect(row.model.length).toBeGreaterThan(0);
Expand All @@ -216,7 +261,7 @@ describe("model matrix", () => {
);
}
expect(
rows.map((row) => [row.family, row.defaultEffort])
defaultRows.map((row) => [row.family, row.defaultEffort])
).toEqual([
["fable", "max"],
["sol", "max"],
Expand All @@ -231,6 +276,31 @@ describe("model matrix", () => {
["fable", "fable"],
["opus", "opus"],
]);
expect(
rows
.filter((row) => !row.firstRunActive)
.map((row) => [row.family, row.provider, row.model, row.defaultEffort])
).toContainEqual(["astra", "codex", "gpt-6-astra", "medium"]);
});

it("rejects duplicate families and provider/model pairs", () => {
expect(() =>
parseModelMatrix(
dispatch.replace(
"| astra | - | codex | gpt-6-astra |",
"| sol | - | codex | gpt-6-astra |"
)
)
).toThrow("duplicate family: sol");

expect(() =>
parseModelMatrix(
dispatch.replace(
"| astra | - | codex | gpt-6-astra |",
"| astra | - | codex | gpt-5.6-sol |"
)
)
).toThrow("duplicate provider/model: codex:gpt-5.6-sol");
});

it("ships exactly the declared Claude-native frontier agents", () => {
Expand Down Expand Up @@ -319,8 +389,8 @@ describe("model matrix", () => {
expect(setup).toContain("Do not invent a precedence rule.");
expect(setup).toContain("Do not probe or write while any inconsistency is unresolved.");
expect(setup).toContain("A failed probe writes nothing:");
expect(setup).toContain("Run one probe per family");
expect(setup).toContain("normalized complete role map from step 2");
expect(setup).toContain("Run one probe per selected family");
expect(setup).toContain("final in-memory assignments from step 2");
expect(setup).toContain("starts with `claude-fable-` or `claude-opus-`");
expect(setup).toContain("preserving the provider, effort, role, and lane order");
expect(setup).toContain("Show any rolling-alias migrations");
Expand Down
Loading