From f88382a206a0fdc68913159a69ebed636e727742 Mon Sep 17 00:00:00 2001 From: Real Time Date: Sat, 29 Aug 2026 18:15:56 -0500 Subject: [PATCH 1/2] feat(pstack): add agy as an external pstack-runner provider pstack-runner can now launch one Agy print-mode child with the same preflight, receipt, and no-fallback contract as Grok. Agy stays provider-only. xhigh and max pin to high. JSON receipts use pinned-argv because the CLI does not report a served model id. --- .../poteto-mode/scripts/runner/cli.test.ts | 6 ++ .../skills/poteto-mode/scripts/runner/cli.ts | 2 +- .../scripts/runner/commands.test.ts | 95 ++++++++++++++++++- .../poteto-mode/scripts/runner/commands.ts | 38 ++++++++ .../scripts/runner/parse-output.test.ts | 45 +++++++++ .../scripts/runner/parse-output.ts | 35 +++++++ .../poteto-mode/scripts/runner/run.test.ts | 75 +++++++++++++-- .../skills/poteto-mode/scripts/runner/run.ts | 14 ++- .../poteto-mode/scripts/runner/types.ts | 2 +- 9 files changed, 300 insertions(+), 12 deletions(-) diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/cli.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/cli.test.ts index 05f79b5..9cd1ede 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/cli.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/cli.test.ts @@ -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"); + }); }); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/cli.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/cli.ts index 4fcce24..be1ecb1 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/cli.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/cli.ts @@ -13,7 +13,7 @@ import { UsageError, } from "./types.ts"; -const HELP = `Usage: pstack-runner --parent --provider \\ +const HELP = `Usage: pstack-runner --parent --provider \\ --model --effort --mode \\ --prompt --cwd --output --receipt [--timeout ] diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts index dbb597f..c18ca55 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.test.ts @@ -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 { return { parent: "claude", @@ -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" }) ); @@ -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 = [ { @@ -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))); } } diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts index 5f2b10c..f11fddb 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/commands.ts @@ -1,3 +1,4 @@ +import { readFileSync } from "node:fs"; import type { AccessMode, Effort, @@ -5,6 +6,8 @@ import type { RunnerOptions, } from "./types.ts"; +const AGY_PRINT_TIMEOUT = "8760h"; + export interface CommandSpec { readonly command: string; readonly args: readonly string[]; @@ -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" }; } } @@ -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)}`; } @@ -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", + }; + } } } diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts index 924a255..c63a967 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.test.ts @@ -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") @@ -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"); }); }); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts index 9990a82..51b8c2d 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts @@ -104,6 +104,39 @@ function parseGrok(stdout: string, requestedModel: string): ParsedOutput { }; } +function parseAgy(stdout: string): ParsedOutput { + let raw: unknown; + try { + raw = JSON.parse(stdout); + } catch { + throw new Error("agy did not emit valid JSON"); + } + const value = object(raw); + if (value === null) throw new Error("agy emitted a non-object result"); + if (nullableString(value.status) !== "SUCCESS") { + throw new Error(nullableString(value.error) ?? "agy reported an error result"); + } + const text = nullableString(value.response); + if (text === null) throw new Error("agy result did not contain final text"); + const usage = object(value.usage); + return { + text, + reportedModel: null, + sessionId: nullableString(value.conversation_id), + usage: usage === null + ? null + : normalizedUsage({ + input_tokens: usage.input_tokens, + output_tokens: usage.output_tokens, + total_tokens: usage.total_tokens, + cache_read_input_tokens: usage.cache_read_tokens + ?? usage.cache_read_input_tokens, + reasoning_tokens: usage.thinking_tokens ?? usage.reasoning_tokens, + }), + costUsd: null, + }; +} + function parseCodex(stdout: string): ParsedOutput { let text: string | null = null; let usage: NormalizedUsage | null = null; @@ -160,6 +193,8 @@ export function parseProviderOutput( return parseCodex(stdout); case "grok": return parseGrok(stdout, requestedModel); + case "agy": + return parseAgy(stdout); } } diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts index deac398..d20134a 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.test.ts @@ -27,7 +27,8 @@ const name = process.argv[1].split("/").at(-1); const isPreflight = (name === "claude" && args[0] === "auth") || (name === "codex" && args[0] === "login") || - (name === "grok" && args[0] === "models"); + (name === "grok" && args[0] === "models") || + (name === "agy" && args[0] === "models"); const stage = isPreflight ? "preflight" : "model"; const startedPath = isPreflight ? process.env.FAKE_PREFLIGHT_STARTED_PATH @@ -87,6 +88,19 @@ if (name === "grok" && args[0] === "models") { console.log("You are logged in with grok.com.\\nAvailable models:\\n * grok-4.6 (default)"); process.exit(0); } +if (name === "agy" && args[0] === "models") { + if (process.env.FAKE_AGY_UNAUTH === "1") { + console.log("Fetching available models..."); + console.error("Error: Please sign in to view available models. Launch the CLI without arguments to sign in."); + process.exit(0); + } + if (process.env.FAKE_AGY_MISSING_MODEL === "1") { + console.log("Fetching available models...\\ngemini-3.7-flash-lowGemini 3.7 Flash (Low)"); + process.exit(0); + } + console.log("Fetching available models...\\ngemini-3.1-pro-highGemini 3.1 Pro (High)"); + process.exit(0); +} const modelIndex = args.findIndex((value) => value === "--model"); const model = modelIndex >= 0 ? args[modelIndex + 1] : "unknown"; if (process.env.FAKE_INVALID_MODEL === "1") { @@ -115,6 +129,8 @@ if (name === "claude") { console.log(JSON.stringify({type:"thread.started",thread_id:"o1"})); console.log(JSON.stringify({type:"item.completed",item:{type:"agent_message",text:"CODEX_OK"}})); console.log(JSON.stringify({type:"turn.completed",usage:{input_tokens:20,cached_input_tokens:5,output_tokens:3,reasoning_output_tokens:1}})); +} else if (name === "agy") { + console.log(JSON.stringify({conversation_id:"a1",status:"SUCCESS",response:"AGY_OK\\n",usage:{input_tokens:11,output_tokens:2,thinking_tokens:0,cache_read_tokens:3,total_tokens:13}})); } else { console.log(JSON.stringify({type:"assistant",message:{content:[{type:"text",text:"progress"}]}})); console.log(JSON.stringify({type:"result",subtype:"success",is_error:false,result:"GROK_OK",session_id:"g1",usage:{input_tokens:30,output_tokens:4,total_tokens:34},total_cost_usd:0.02,modelUsage:{[model + "-build"]:{}}})); @@ -137,12 +153,14 @@ function options(provider: Provider, suffix: string = provider): RunnerOptions { ? "claude-fable-5" : provider === "codex" ? "gpt-5.6-sol" - : "grok-4.6"; + : provider === "agy" + ? "gemini-3.1-pro-high" + : "grok-4.6"; return { parent, provider, model, - effort: provider === "grok" ? "xhigh" : "max", + effort: provider === "grok" ? "xhigh" : provider === "agy" ? "high" : "max", mode: "read-only", promptPath: join(scratch, "prompt.md"), cwd: scratch, @@ -216,7 +234,7 @@ beforeEach(() => { bin = join(scratch, "bin"); mkdirSync(bin); writeFileSync(join(scratch, "prompt.md"), "Return the marker."); - for (const name of ["claude", "codex", "grok"]) makeExecutable(name); + for (const name of ["claude", "codex", "grok", "agy"]) makeExecutable(name); previousPath = process.env.PATH; process.env.PATH = `${bin}:${dirname(process.execPath)}:${previousPath ?? ""}`; delete process.env.FAKE_TIMEOUT; @@ -236,6 +254,8 @@ beforeEach(() => { delete process.env.FAKE_GROK_TRANSIENT_UNAUTH_PATH; delete process.env.FAKE_GROK_PREFLIGHT_LOG_PATH; delete process.env.FAKE_GROK_MISSING_MODEL; + delete process.env.FAKE_AGY_UNAUTH; + delete process.env.FAKE_AGY_MISSING_MODEL; delete process.env.FAKE_DESCENDANT_HOLDS_PIPES_MS; delete process.env.FAKE_DESCENDANT_PID_PATH; delete process.env.FAKE_SELF_SIGNAL; @@ -260,6 +280,8 @@ afterEach(() => { delete process.env.FAKE_GROK_TRANSIENT_UNAUTH_PATH; delete process.env.FAKE_GROK_PREFLIGHT_LOG_PATH; delete process.env.FAKE_GROK_MISSING_MODEL; + delete process.env.FAKE_AGY_UNAUTH; + delete process.env.FAKE_AGY_MISSING_MODEL; delete process.env.FAKE_DESCENDANT_HOLDS_PIPES_MS; delete process.env.FAKE_DESCENDANT_PID_PATH; delete process.env.FAKE_SELF_SIGNAL; @@ -267,7 +289,7 @@ afterEach(() => { }); describe("runLane", () => { - for (const provider of ["claude", "codex", "grok"] as const) { + for (const provider of ["claude", "codex", "grok", "agy"] as const) { it(`executes and receipts the ${provider} external lane`, async () => { const input = options(provider); const result = await runLane(input); @@ -279,8 +301,10 @@ describe("runLane", () => { status: "complete", provider, model: input.model, - modelVerified: provider !== "codex", - modelEvidence: provider === "codex" ? "pinned-argv" : "provider-report", + modelVerified: provider !== "codex" && provider !== "agy", + modelEvidence: provider === "codex" || provider === "agy" + ? "pinned-argv" + : "provider-report", preflight: { status: "passed" }, }); }); @@ -418,6 +442,39 @@ describe("runLane", () => { }); }); + it("classifies Agy sign-in failure without retrying preflight", async () => { + process.env.FAKE_AGY_UNAUTH = "1"; + const modelStarted = join(scratch, "agy-model.started"); + process.env.FAKE_MODEL_STARTED_PATH = modelStarted; + const input = options("agy", "agy-unauthenticated"); + const result = await runLane(input); + + expect(result.exitCode).toBe(77); + expect(existsSync(modelStarted)).toBe(false); + expect(receipt(input.receiptPath)).toMatchObject({ + status: "unauthenticated", + preflight: { status: "failed" }, + }); + expect(receipt(input.receiptPath).preflight.evidence).toContain( + "Please sign in" + ); + }); + + it("classifies a missing Agy model without retrying preflight", async () => { + process.env.FAKE_AGY_MISSING_MODEL = "1"; + const modelStarted = join(scratch, "agy-missing-model.started"); + process.env.FAKE_MODEL_STARTED_PATH = modelStarted; + const input = options("agy", "agy-missing-model"); + const result = await runLane(input); + + expect(result.exitCode).toBe(69); + expect(existsSync(modelStarted)).toBe(false); + expect(receipt(input.receiptPath)).toMatchObject({ + status: "unavailable-model", + preflight: { status: "failed" }, + }); + }); + it("does not retry a Grok preflight with a missing model", async () => { process.env.FAKE_GROK_MISSING_MODEL = "1"; const preflightLog = join(scratch, "grok-missing-model.log"); @@ -915,5 +972,9 @@ describe("childEnvironment", () => { PATH: "/bin", KEEP_ME: "yes", }); + expect(childEnvironment("agy", source)).toEqual({ + PATH: "/bin", + KEEP_ME: "yes", + }); }); }); diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts index d1f72d8..c5aec34 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/run.ts @@ -369,11 +369,18 @@ function preflightPassed(provider: Provider, model: string, result: ProcessResul return /logged in/i.test(combined); case "grok": return /logged in/i.test(combined) && combined.includes(model); + case "agy": + return !/please sign in/i.test(combined) && agyModelListed(combined, model); } } +function agyModelListed(output: string, model: string): boolean { + const escaped = model.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`(?:^|\\n)${escaped}(?![a-z0-9._-])`).test(output); +} + function successfulPreflightEvidence(provider: Provider, model: string): string { - return provider === "grok" + return provider === "grok" || provider === "agy" ? `authenticated; model ${model} available` : "authenticated"; } @@ -395,6 +402,9 @@ function preflightFailureStatus( ): ReceiptStatus { const status = unavailableStatus(value); if (status !== "child-failed") return status; + if (provider === "agy") { + return agyModelListed(value, model) ? "unauthenticated" : "unavailable-model"; + } return provider === "grok" && !value.includes(model) ? "unavailable-model" : "unauthenticated"; @@ -449,7 +459,7 @@ function modelProof( modelEvidence: "provider-report", }; } - if (provider === "codex" && reported === null) { + if ((provider === "codex" || provider === "agy") && reported === null) { return { reportedModel: null, modelVerified: false, diff --git a/plugins/pstack/skills/poteto-mode/scripts/runner/types.ts b/plugins/pstack/skills/poteto-mode/scripts/runner/types.ts index 11c6dfb..6837b74 100644 --- a/plugins/pstack/skills/poteto-mode/scripts/runner/types.ts +++ b/plugins/pstack/skills/poteto-mode/scripts/runner/types.ts @@ -1,5 +1,5 @@ export const PARENTS = ["claude", "codex"] as const; -export const PROVIDERS = ["claude", "codex", "grok"] as const; +export const PROVIDERS = ["claude", "codex", "grok", "agy"] as const; export const EFFORTS = ["low", "medium", "high", "xhigh", "max"] as const; export const ACCESS_MODES = ["read-only", "isolated-write"] as const; From f9e58113647392f2b667705d9917ca27dc40d8d8 Mon Sep 17 00:00:00 2001 From: Real Time Date: Sat, 29 Aug 2026 18:15:56 -0500 Subject: [PATCH 2/2] docs(pstack): document the agy external runner lane Record Agy on the parent route table and in External lanes without adding a fifth first-run matrix family. Setup and the four-model panel stay Fable, Sol, Grok, and Opus. --- CHANGES.md | 4 ++++ README.md | 6 +++--- .../references/provider-dispatch.md | 18 +++++++++++------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8953a58..c5460a7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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. diff --git a/README.md b/README.md index 315d51b..bd68692 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md index 9b24d38..731ce25 100644 --- a/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md +++ b/plugins/pstack/skills/poteto-mode/references/provider-dispatch.md @@ -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. @@ -46,7 +46,7 @@ The launcher lives at `skills/poteto-mode/scripts/runner/pstack-runner` under th ```text pstack-runner \ --parent \ - --provider \ + --provider \ --model \ --effort \ --mode \ @@ -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: @@ -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. @@ -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.