Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The runner and its preflight have no implicit timeout. Do not invent a duration

Read-only mode maps to Claude plan mode with project-only settings and an explicit tool list, Codex's read-only sandbox, and Grok auto mode plus its `read-only` sandbox and read-oriented tool list. Headless Grok cancels the whole turn on a permission prompt, which its `plan` mode raises for any shell command outside its built-in read-only list; auto mode reports a blocked call to the model instead. 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 auto mode plus its `workspace` sandbox and write-capable tool list. Grok uses auto mode in both access modes because `acceptEdits` raises the same turn-cancelling prompt for a shell command outside Grok's built-in list. 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.
Every concurrent external lane needs distinct prompt, output, and receipt paths. The launcher exclusively reserves the output, receipt, and `<receipt>.stdout` / `<receipt>.stderr` sidecars with private (`0600`) permissions and refuses to overwrite existing files. All these paths must be distinct from each other and the prompt; a failed reservation rolls back only files created by that attempt. Schema-version-1 receipts add nullable `stdoutPath` and `stderrPath` fields identifying reserved artifacts. Sidecars retain the model process's raw stdout/stderr bytes, including on dropouts; they do not include authentication-preflight output. Cancellation and timeout retain every byte captured before the drain stops. Keep these private local files with the receipt; quote only operator-selected excerpts in public evidence.

## Completion and dropouts

Expand All @@ -98,6 +98,6 @@ Success requires all of these:

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.

Any missing CLI, failed login, unavailable model, explicit timeout, cancellation, catchable post-reservation launcher failure, non-zero child exit, malformed result, or model mismatch is a receipt-bearing dropout. Record it and apply the calling skill's existing dropout policy. A `cancelled` receipt proves that the runner received the signal; its `signal` field is non-null only when the runner sent that signal to a still-active direct CLI child, and remains null when cancellation only stopped a post-exit pipe drain. The provider CLI owns any processes it starts beneath that direct child; the receipt does not claim a process-tree kill. Do not delete or overwrite the receipt. Never substitute the parent model, retry another provider, or reinterpret an external descriptor as a native model slug.
Any missing CLI, failed login, unavailable model, explicit timeout, cancellation, catchable post-reservation launcher failure, non-zero child exit, malformed result, or model mismatch is a receipt-bearing dropout. Record it and apply the calling skill's existing dropout policy. A `cancelled` receipt can represent either a launcher signal or a well-formed Grok terminal cancellation. Provider cancellations return wrapper exit 130; other valid Grok terminal failures return `child-failed`/70, while invalid or incomplete terminal data remains `malformed-output`/65. These provider failures preserve the exact reason in `error.message`, put it first in bounded evidence, and retain reported model/session/usage/cost and the actual child exit code, even after an ordinary nonzero child exit. Missing provider metadata stays null. Launcher cancellation and timeout take precedence. The `signal` field is non-null only when the runner sent that signal to a still-active direct CLI child, and remains null for provider-only cancellation or when launcher cancellation only stopped a post-exit pipe drain. The provider CLI owns any processes it starts beneath that direct child; the receipt does not claim a process-tree kill. Do not delete or overwrite the receipt. Never substitute the parent model, retry another provider, or reinterpret an external descriptor as a native model slug.

Start native and external lanes in the same fan-out phase, then wait for all of them before judging. A judge must not read candidate paths while their owners are still writing.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,47 @@ describe("parseProviderOutput", () => {
expect(reportedModelMatches("grok", "fable", "claude-fable-9-9")).toBe(false);
});

for (const [stopReason, expectedStatus] of [
["cancelled", "cancelled"], ["canceled", "cancelled"], ["end_turn", "child-failed"],
]) {
it(`issue78 retains terminal metadata for ${stopReason}`, () => {
let failure: unknown;
try {
parseProviderOutput("grok", JSON.stringify({
type: "result", subtype: "error_during_execution", is_error: true,
stop_reason: stopReason, errors: ["Exact provider reason"],
session_id: "terminal-session", modelUsage: { "grok-4.6-build": {} },
usage: { input_tokens: 30, output_tokens: 4 }, total_cost_usd: 0.02,
}), "", "grok-4.6");
} catch (error) {
failure = error;
}
expect(failure).toMatchObject({
message: "Exact provider reason", status: expectedStatus,
metadata: {
reportedModel: "grok-4.6-build", sessionId: "terminal-session",
usage: { inputTokens: 30, outputTokens: 4 }, costUsd: 0.02,
},
});
});
}

it("issue78 rejects incomplete terminal status", () => {
for (const terminal of [
{ type: "result", result: "text" },
{ type: "result", subtype: "success", result: "text" },
{ type: "result", subtype: "", is_error: true },
{ type: "result", subtype: "api_error", is_error: "true" },
]) {
expect(() => parseProviderOutput("grok", JSON.stringify(terminal), "", "grok-4.6"))
.toThrow("valid terminal status");
}
expect(() => parseProviderOutput("grok", "not-json", "", "grok-4.6"))
.toThrow("non-JSON event");
expect(() => parseProviderOutput("grok", '{"type":"assistant"}', "", "grok-4.6"))
.toThrow("terminal event");
});

it("rejects malformed or textless responses", () => {
expect(() =>
parseProviderOutput("claude", "not-json", "", "fable")
Expand Down
37 changes: 30 additions & 7 deletions plugins/pstack/skills/poteto-mode/scripts/runner/parse-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ import {
isRollingClaudeAlias,
} from "./model-aliases.ts";

export class ProviderResultError extends Error {
constructor(
message: string,
readonly status: "cancelled" | "child-failed",
readonly metadata: Omit<ParsedOutput, "text">
) {
super(message);
this.name = "ProviderResultError";
}
}

type JsonObject = Record<string, unknown>;

function object(value: unknown): JsonObject | null {
Expand Down Expand Up @@ -99,19 +110,31 @@ function parseGrok(stdout: string, requestedModel: string): ParsedOutput {
}

if (result === null) throw new Error("grok result did not contain a terminal event");
if (result.is_error === true || result.subtype !== "success") {
throw new Error("grok reported an error result");
if (nullableString(result.subtype) === null || typeof result.is_error !== "boolean") {
throw new Error("grok result did not contain a valid terminal status");
}
const text = nullableString(result.result);
if (text === null) throw new Error("grok result did not contain final text");

return {
text,
const metadata = {
reportedModel: modelFromUsage(result.modelUsage, "grok", requestedModel),
sessionId: nullableString(result.session_id),
usage: normalizedUsage(result.usage),
costUsd: finiteNumber(result.total_cost_usd) ?? null,
};
if (result.is_error || result.subtype !== "success") {
const errors = Array.isArray(result.errors)
? result.errors.filter((entry): entry is string => typeof entry === "string" && entry.length > 0)
: [];
const message = errors.join("\n") || nullableString(result.error)
|| nullableString(object(result.error)?.message) || nullableString(result.result)
|| `grok reported ${result.subtype}`;
const cancelled = result.stop_reason === "cancelled" || result.stop_reason === "canceled"
|| result.subtype === "cancelled" || result.subtype === "canceled"
|| /^(?:User cancel(?:led|ed) the execution|PermissionCancelled)/i.test(message);
throw new ProviderResultError(message, cancelled ? "cancelled" : "child-failed", metadata);
}
const text = nullableString(result.result);
if (text === null) throw new Error("grok result did not contain final text");

return { text, ...metadata };
}

function parseCodex(stdout: string): ParsedOutput {
Expand Down
Loading
Loading