Skip to content
Draft
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
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

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

## Unreleased adds Agy as an external pstack-runner provider

`pstack-runner` accepts `--provider agy` from a Claude Code or Codex parent. The lane preflights `agy models`, invokes print mode once, pins `--sandbox`, maps read-only to `--mode plan` and isolated-write to `--mode accept-edits`, and never passes `--dangerously-skip-permissions`. Agy's effort flag is `low|medium|high`; `xhigh` and `max` pin to `high`. Receipts use Codex-style `pinned-argv` proof because Agy JSON does not report a served model id. Agy is not a parent harness and is not a fifth first-run matrix family.

## 1.2.0 adds verified multi-PR plans, earlier runtime diagnostics, and shared review-bot triage

Plans with several stages now use one checklist instead of an overview and separate files for each stage. It has one ordered section for every pull request and keeps all ten ways of testing the real product, unit tests, live and performance proof, checks for how changes work together, merge rules, and supporting details in one place. A Node-based checker with no extra dependencies rejects missing or out-of-order sections, fake screenshots, empty definitions of success, incomplete performance proof, incorrectly written review checks, unsupported punctuation, and incorrect command use. Claude Code and Codex use the same installed skill and checker through their existing parent-controlled setup. If a provider fails, it is identified by name and treated as a dropout. No backup provider or hidden time limit was added.
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ pstack does not ask you to trust an agent on day one. It helps the agent leave e

## Install

You need a current Claude Code or Codex installation. For the full four-model review, install and sign in to the Claude Code, Codex, and Grok command-line tools. [Bun](https://bun.sh) runs the small local tool that starts models outside the app you are using. You can still use the core workflows with fewer models.
You need a current Claude Code or Codex installation. For the full four-model review, install and sign in to the Claude Code, Codex, and Grok command-line tools. [Bun](https://bun.sh) runs the small local tool that starts models outside the app you are using. You can still use the core workflows with fewer models. `pstack-runner` can also launch the Agy CLI as an extra external provider. Agy is not a parent harness and is not part of the default four-model panel.

### Claude Code

Expand Down Expand Up @@ -132,10 +132,10 @@ Both apps read the same pstack skills. Only the way they start those skills and
| --- | --- | --- |
| Start poteto-mode | Claude loads a small startup instruction that can route non-trivial work into it. You can also run `/pstack:poteto-mode` yourself. | Ask for `pstack:poteto-mode` by name. Codex does not load the Claude startup instruction. |
| Runs inside the app | Claude models stay inside Claude Code. | The Sol model stays inside Codex. |
| Other models | Codex and Grok run through their signed-in command-line tools. | Claude and Grok run through their signed-in command-line tools. |
| Other models | Codex, Grok, and Agy run through their signed-in command-line tools. | Claude, Grok, and Agy run through their signed-in command-line tools. |
| Skills and workflows | Shared with Codex. | Shared with Claude Code. |

Grok can take part in a multi-model review. You cannot use Grok as the main app running pstack.
Grok can take part in a multi-model review. You cannot use Grok as the main app running pstack. Agy is the same kind of extra command-line provider. It is never the app that runs pstack.

## Learn from the original

Expand Down
18 changes: 11 additions & 7 deletions plugins/pstack/skills/poteto-mode/references/provider-dispatch.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ The allowed effort universe is exactly `low`, `medium`, `high`, `xhigh`, `max`.

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.

| Parent | `claude:*` | `codex:*` | `grok:*` |
|---|---|---|---|
| Claude Code | native `Agent` | external runner | external runner |
| Codex | external runner | native `spawn_agent` | external runner |
| Parent | `claude:*` | `codex:*` | `grok:*` | `agy:*` |
|---|---|---|---|---|
| Claude Code | native `Agent` | external runner | external runner | external runner |
| Codex | external runner | native `spawn_agent` | external runner | 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.

Expand All @@ -46,7 +46,7 @@ The launcher lives at `skills/poteto-mode/scripts/runner/pstack-runner` under th
```text
pstack-runner \
--parent <claude|codex> \
--provider <claude|codex|grok> \
--provider <claude|codex|grok|agy> \
--model <real CLI model> \
--effort <low|medium|high|xhigh|max> \
--mode <read-only|isolated-write> \
Expand All @@ -61,6 +61,10 @@ Pass arguments as an argv array or quote every path. Never interpolate prompt te

Grok authentication preflight has one bounded retry. If the first `grok models` result would be classified as unauthenticated, the runner waits five seconds and tries the same preflight once more. A second failure is terminal. The delay and second attempt share the runner's absolute deadline and cancellation latch, and the receipt keeps evidence from both attempts. Model execution is never retried.

Agy is an external provider only. It is not a parent harness and is not a first-run matrix family. The portable slug is `agy:gemini-3.1-pro-high@high`. Preflight is `agy models`. That command can exit 0 while still asking the operator to sign in, so the runner treats `Please sign in` as `unauthenticated` and requires the requested slug as a whole token in the listing. There is no Grok-style contradictory-auth retry.

Agy print mode takes the prompt as the `--print` value, not stdin and not a prompt file. The runner's process cwd is the assigned worktree. `--sandbox` is boolean. Read-only maps to `--mode plan` plus `--sandbox`. Isolated-write maps to `--mode accept-edits`, `--sandbox`, and `--disable-slash-commands`. Do not combine `--mode plan` with `--disable-slash-commands`; the CLI then ignores plan mode. There is no `--no-subagents` and no tool allow or deny list. Never pass `--dangerously-skip-permissions`. Agy `--effort` is `low`, `medium`, or `high`; the runner pins `xhigh` and `max` to `high`. The CLI's default `--print-timeout` is five minutes, so the runner pins `8760h` and still lets the wrapper `--timeout` own the deadline.

The parent tool sandbox still governs whether a subscribed child CLI can reach its credentials and network. Run setup's live probe from the actual parent profile. A blocked external CLI is a loud dropout, not a reason to elevate permissions or substitute a model silently.

The parent invocation must itself be resumable background work:
Expand All @@ -72,7 +76,7 @@ Start the background process, continue launching the other lanes, then drain the

The runner and its preflight have no implicit timeout. Do not invent a duration from role, mode, or a convenient round number; real implementation lanes can run for 90 minutes or much longer. Pass `--timeout` only when the user, an external service deadline, or a measured task contract supplies a real bound. That value starts at wrapper entry, before module loading and argument parsing, and remains one absolute deadline across setup, preflight, model execution, and output capture. It is never a fresh allowance per child, and long waits are armed in runtime-safe chunks without shortening the supplied deadline. Otherwise supervise liveness through the retained background task/session handle and cancel manually only on evidence that the run is dead. Cancel through that retained handle so the runner receives SIGINT or SIGTERM, sends it to an active child when one remains, stops waiting on inherited output pipes, removes the empty output reservation, and writes a `cancelled` receipt. Preserve that receipt; a retry is a new attempt with new unique output and receipt paths. Unchanged running state is not a dropout, and Claude's ten-minute foreground ceiling is never a reason to terminate a healthy lane.

Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok plan mode plus its `read-only` sandbox and read-oriented tool list. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, and Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout.
Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, Grok plan mode plus its `read-only` sandbox and read-oriented tool list, and Agy `--mode plan` plus `--sandbox`. Grok's built-in read-only profile deliberately keeps its own state and system temporary directories writable, so point a read-only Grok lane at the actual checkout rather than a worktree under `/tmp`, `/var/tmp`, or the host's temporary directory. `isolated-write` maps to Claude `acceptEdits` with project-only settings, Codex `workspace-write`, Grok `acceptEdits` plus its `workspace` sandbox and write-capable tool list, and Agy `--mode accept-edits` plus `--sandbox` and `--disable-slash-commands`. Give every writer only a dedicated worktree or output directory. Never route a writer into the primary checkout.

Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher reserves output and receipt paths exclusively and refuses to overwrite them.

Expand All @@ -82,7 +86,7 @@ Success requires all of these:

1. Exit status `0`.
2. Receipt status `complete`.
3. Either `modelVerified: true` with `modelEvidence: "provider-report"`, or a Codex receipt with `reportedModel: null`, `modelVerified: false`, and `modelEvidence: "pinned-argv"`. Codex 0.149.0 accepts the exact `--model` argument but does not report the served model in its JSONL stream.
3. Either `modelVerified: true` with `modelEvidence: "provider-report"`, or a Codex or Agy receipt with `reportedModel: null`, `modelVerified: false`, and `modelEvidence: "pinned-argv"`. Codex 0.149.0 accepts the exact `--model` argument but does not report the served model in its JSONL stream. Agy `--output-format json` reports text, session, and usage, but not the served model id.
4. A non-empty output file.

The receipt also carries elapsed time, token usage when the CLI exposes it, and cost when available. Keep it with the arena or review artifacts so parent-harness comparisons are evidence-based.
Expand Down
6 changes: 6 additions & 0 deletions plugins/pstack/skills/poteto-mode/scripts/runner/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,10 @@ describe("runner CLI parsing", () => {
"greater than zero"
);
});

it("accepts agy as an external provider", () => {
const args = argv();
args[args.indexOf("--provider") + 1] = "agy";
expect(parseArgs(args)?.provider).toBe("agy");
});
});
2 changes: 1 addition & 1 deletion plugins/pstack/skills/poteto-mode/scripts/runner/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
UsageError,
} from "./types.ts";

const HELP = `Usage: pstack-runner --parent <claude|codex> --provider <claude|codex|grok> \\
const HELP = `Usage: pstack-runner --parent <claude|codex> --provider <claude|codex|grok|agy> \\
--model <slug> --effort <level> --mode <read-only|isolated-write> \\
--prompt <file> --cwd <dir> --output <file> --receipt <file> [--timeout <seconds>]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { describe, expect, it } from "bun:test";
import { writeFileSync } from "node:fs";
import { invocationCommand } from "./commands.ts";
import type { RunnerOptions } from "./types.ts";

const AGY_PROMPT = "Return the marker.\n";

function writeAgyPrompt(path: string): void {
writeFileSync(path, AGY_PROMPT);
}

function options(overrides: Partial<RunnerOptions> = {}): RunnerOptions {
return {
parent: "claude",
Expand Down Expand Up @@ -133,6 +140,23 @@ describe("invocationCommand", () => {
);
expect(grok.args).not.toContain("--always-approve");

const agyWrite = options({
provider: "agy",
model: "gemini-3.1-pro-high",
mode: "isolated-write",
});
writeAgyPrompt(agyWrite.promptPath);
const agy = invocationCommand(agyWrite);
expect(agy.args).toEqual(
expect.arrayContaining([
"--mode",
"accept-edits",
"--sandbox",
"--disable-slash-commands",
])
);
expect(agy.args).not.toContain("--dangerously-skip-permissions");

const claude = invocationCommand(
options({ provider: "claude", model: "claude-fable-5", mode: "isolated-write" })
);
Expand All @@ -146,6 +170,68 @@ describe("invocationCommand", () => {
);
});

it("pins Agy model, mapped effort, sandbox, and print prompt", () => {
const input = options({
provider: "agy",
model: "gemini-3.1-pro-high",
effort: "high",
});
writeAgyPrompt(input.promptPath);
const spec = invocationCommand(input);
expect(spec.command).toBe("agy");
expect(spec.stdin).toBe("none");
expect(spec.args).toEqual([
"--model",
"gemini-3.1-pro-high",
"--effort",
"high",
"--mode",
"plan",
"--sandbox",
"--output-format",
"json",
"--print-timeout",
"8760h",
"--print",
AGY_PROMPT,
]);
expect(spec.args).not.toContain("--disable-slash-commands");
expect(spec.args).not.toContain("--dangerously-skip-permissions");
});

it("maps Agy xhigh and max to high and disables slash commands on write", () => {
const input = options({
provider: "agy",
model: "gemini-3.1-pro-high",
effort: "xhigh",
mode: "isolated-write",
});
writeAgyPrompt(input.promptPath);
const spec = invocationCommand(input);
expect(spec.args).toEqual(
expect.arrayContaining([
"--effort",
"high",
"--mode",
"accept-edits",
"--sandbox",
"--disable-slash-commands",
"--print",
AGY_PROMPT,
])
);
expect(spec.args).not.toContain("max");
expect(spec.args).not.toContain("xhigh");
const maxSpec = invocationCommand(
options({
provider: "agy",
model: "gemini-3.1-pro-high",
effort: "max",
})
);
expect(maxSpec.args).toEqual(expect.arrayContaining(["--effort", "high"]));
});

it("covers low, medium, and high for every external provider", () => {
const cases = [
{
Expand All @@ -169,10 +255,17 @@ describe("invocationCommand", () => {
effort,
],
},
{
provider: "agy" as const,
model: "gemini-3.1-pro-high",
flag: (effort: "low" | "medium" | "high") => ["--effort", effort],
},
];
for (const { provider, model, flag } of cases) {
for (const effort of ["low", "medium", "high"] as const) {
const spec = invocationCommand(options({ provider, model, effort }));
const input = options({ provider, model, effort });
if (provider === "agy") writeAgyPrompt(input.promptPath);
const spec = invocationCommand(input);
expect(spec.args).toEqual(expect.arrayContaining(flag(effort)));
}
}
Expand Down
38 changes: 38 additions & 0 deletions plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { readFileSync } from "node:fs";
import type {
AccessMode,
Effort,
Provider,
RunnerOptions,
} from "./types.ts";

const AGY_PRINT_TIMEOUT = "8760h";

export interface CommandSpec {
readonly command: string;
readonly args: readonly string[];
Expand All @@ -27,6 +30,8 @@ export function preflightCommand(provider: Provider): CommandSpec {
};
case "grok":
return { command: "grok", args: ["models"], stdin: "none" };
case "agy":
return { command: "agy", args: ["models"], stdin: "none" };
}
}

Expand Down Expand Up @@ -59,6 +64,15 @@ function permissionMode(mode: AccessMode): string {
return mode === "read-only" ? "plan" : "acceptEdits";
}

function agyMode(mode: AccessMode): string {
return mode === "read-only" ? "plan" : "accept-edits";
}

function agyEffort(effort: Effort): "low" | "medium" | "high" {
if (effort === "low" || effort === "medium") return effort;
return "high";
}

function effortOverride(effort: Effort): string {
return `model_reasoning_effort=${JSON.stringify(effort)}`;
}
Expand Down Expand Up @@ -146,5 +160,29 @@ export function invocationCommand(options: RunnerOptions): CommandSpec {
],
stdin: "none",
};
case "agy": {
const args: string[] = [
"--model",
options.model,
"--effort",
agyEffort(options.effort),
"--mode",
agyMode(options.mode),
"--sandbox",
"--output-format",
"json",
"--print-timeout",
AGY_PRINT_TIMEOUT,
];
if (options.mode === "isolated-write") {
args.push("--disable-slash-commands");
}
args.push("--print", readFileSync(options.promptPath, "utf8"));
return {
command: "agy",
args,
stdin: "none",
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,39 @@ describe("parseProviderOutput", () => {
expect(parsed.reportedModel).toBe("claude-fable-5");
});

it("extracts Agy JSON without inventing a provider-reported model", () => {
const parsed = parseProviderOutput(
"agy",
JSON.stringify({
conversation_id: "agy-session",
status: "SUCCESS",
response: "AGY_OK\n",
usage: {
input_tokens: 11,
output_tokens: 2,
thinking_tokens: 4,
cache_read_tokens: 3,
total_tokens: 13,
},
}),
"",
"gemini-3.1-pro-high"
);
expect(parsed).toMatchObject({
text: "AGY_OK\n",
reportedModel: null,
sessionId: "agy-session",
usage: {
inputTokens: 11,
outputTokens: 2,
reasoningTokens: 4,
cachedInputTokens: 3,
totalTokens: 13,
},
costUsd: null,
});
});

it("rejects malformed or textless responses", () => {
expect(() =>
parseProviderOutput("claude", "not-json", "", "claude-fable-5")
Expand All @@ -122,5 +155,17 @@ describe("parseProviderOutput", () => {
"gpt-5.6-sol"
)
).toThrow("final agent message");
expect(() =>
parseProviderOutput(
"agy",
JSON.stringify({
status: "ERROR",
response: "",
error: "invalid model selection",
}),
"",
"gemini-3.1-pro-high"
)
).toThrow("invalid model selection");
});
});
Loading