diff --git a/capy/skill/gstack/SKILL.md b/capy/skill/gstack/SKILL.md index 167d2475a..cf242b8d1 100644 --- a/capy/skill/gstack/SKILL.md +++ b/capy/skill/gstack/SKILL.md @@ -46,7 +46,7 @@ Capy checklist for `/autoplan`: drive `gstack-autoplan`, never a hand adaptation Token caps on /ship: pass a brief's cap as `--token-cap `. /ship reads `cost.tokens` and `cost.credits` from `tools.thread.get({ thread_id: "" })` (its own jam, subagents included; `"this"` names the root thread, so only a root run may pass it) inside the same execute program as the `gstack-ship-budget` command that takes them as `--reading --credits `, never in a call of its own. `ship/DRIVER.md` starts `--mode observe`: each program ends with `note`, and only a note past the cap stops (`docs/ship-budget.md`). -/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base ` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start ` once per row the step logs (admission mode: `--reservation` from `RESERVATION_ID:`); (3) `BEFORE` (tokens and credits); (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it; audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll: one `tools.subagent.list` per ~20 s tick (a `tools.subagent.get` with `transcript: "none"` only on a status change), returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); start readers listed under `rejected` with `tools.subagent.start` as slots free; report, never drop, one stuck 20 minutes; (6) `gstack-ship-budget note --record --step --reading --credits --dispatched-at --dispatched-credits --readers `; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls (a later call costs about 40x). While readers run, never end the turn on `wait` (each finished subagent wakes a turn); rerun the poll program. Otherwise re-issue `wait` with `duration_seconds: 200` and the operation ids. At Step 22 run `gstack-ship-budget calls --events ` on your jam's `tools.thread.get` transcript since /ship started; report its `CALLS:` line. A small waiting child saves nothing. +/ship on Capy is about a dozen calls, each one execute program (`ship/DRIVER.md`). A review pass, Step 11 and the Step 14.5 audit each run inside one program, in order: (1) `gstack-review-plan --base ` with the pass's flags (its `DISPATCH:` lines); (2) `gstack-review-log --start ` once per row the step logs (admission mode: `--reservation` from `RESERVATION_ID:`); (3) `BEFORE`; (4) `tools.subagent.create`, one reader each, `machine: "shared"`, its brief copied verbatim from the section that dispatches it (a rendered file as bash `cat` prints it: a file read cuts long lines); audit readers (Steps 7, 8, 9.2.1, 14.5) cannot block, so they add `model: { model_id: "anthropic/claude-sonnet-5-5", reasoning_mode: "low" }` (`anthropic/claude-haiku-5-5` also fits); (5) a poll: one `tools.subagent.list` per ~20 s tick (a `tools.subagent.get` with `transcript: "none"` only on a status change), returning 200 s after the program's first line, where a reader is settled only at status `done` or `failed` or with a `summary` (a just-created `idle` reader has not run); start readers listed under `rejected` with `tools.subagent.start` as slots free; report, never drop, one stuck 20 minutes; (6) `gstack-ship-budget note --record --step --reading --credits --dispatched-at --dispatched-credits --readers `; (7) return only each reader's final output, or what still runs. The parent never walks the dispatch steps call by call. Waits: never let 5 minutes pass between calls. While readers run, never end the turn on `wait`; rerun the poll program. Otherwise re-issue `wait` with `duration_seconds: 200` and the operation ids. At Step 22 run `gstack-ship-budget calls --events ` on your jam's `tools.thread.get` transcript since /ship started; report its `CALLS:` line. A small waiting child saves nothing. ## Plan, approve, ship diff --git a/changelog.d/gsta-167-calls-1.md b/changelog.d/gsta-167-calls-1.md index 3aa6cedb4..b3dd62a54 100644 --- a/changelog.d/gsta-167-calls-1.md +++ b/changelog.d/gsta-167-calls-1.md @@ -12,3 +12,5 @@ ### Fixed - **`gstack-ship-budget note --step 9.1`** records under step 9 with the sub-step as its label. Before, it was refused, and replays lost two notes to that error. - **`gstack-review-scope render`** accepts `{"deferred": [...]}` as well as the bare array. +- **`gstack-review-dispatch judge`** treats a judged line with no severity, or `"severity": null`, as invalid and prints `JUDGMENT_REJECTED`. Before, it crashed with a TypeError, and a replay spent three calls recovering. +- **The Capy skill passes a rendered brief as bash `cat` prints it.** A file read cut the long lines of a 231,589-byte outside-voice brief, so `sent` reported `BRIEF_MISMATCH` and the reader had to be sent again. diff --git a/lib/review-ledger.ts b/lib/review-ledger.ts index defc5208a..d529e1b81 100644 --- a/lib/review-ledger.ts +++ b/lib/review-ledger.ts @@ -41,7 +41,8 @@ export function recordSent(ledger: string, reader: string, sentText: string): { /** Severity ranks across the voices' vocabularies; a label with no severity (FIXABLE, INVESTIGATE) ranks 0. */ const RANKS: Record = { critical: 4, p0: 4, high: 3, p1: 3, medium: 2, p2: 2, informational: 2, low: 1, p3: 1, fixable: 0, investigate: 0 }; -export function rank(label: string): number | null { +export function rank(label: unknown): number | null { + if (typeof label !== 'string') return null; const r = RANKS[label.trim().replace(/^\[|\]$/g, '').toLowerCase()]; return r === undefined ? null : r; } diff --git a/test/review-dispatch.test.ts b/test/review-dispatch.test.ts index 44c7ef8c6..7cf6d45c8 100644 --- a/test/review-dispatch.test.ts +++ b/test/review-dispatch.test.ts @@ -211,6 +211,8 @@ describe('gstack-review-dispatch sent, collect, judge and verify: nothing change expect(parseFindings('r', 'FIXABLE: race in a.ts:3\nINVESTIGATE: odd lock\nRecommendation: Fix the race because x\n').map(f => f.label)).toEqual(['FIXABLE', 'INVESTIGATE']); const m: MergedLine[] = [{ id: 'r#1', reader: 'r', label: 'FIXABLE', raw: 'x', output_sha256: 'h' }]; expect(judge(m, [{ id: 'r#1', severity: 'severe' }]).invalid).toEqual(['r#1']); + // Replay C1-34 (GSTA-193): a judged line with "severity": null crashed judge with a TypeError; it is an invalid line. + expect(judge(m, [{ id: 'r#1', severity: null as unknown as string }, { id: 'lead#1' } as unknown as { id: string; severity: string }]).invalid).toEqual(['r#1', 'lead#1']); expect(judge(m, [{ id: 'r#1', severity: 'INFORMATIONAL', reason: 'a real defect' }]).ok).toBe(true); });