diff --git a/docs/trajectories/typescript-bun-migration/RESUME.md b/docs/trajectories/typescript-bun-migration/RESUME.md index 77150f143..8a6f59182 100644 --- a/docs/trajectories/typescript-bun-migration/RESUME.md +++ b/docs/trajectories/typescript-bun-migration/RESUME.md @@ -1,9 +1,9 @@ # Trajectory — TypeScript / Bun migration -**Status**: Active (Lane B slice 13 merged — [#892](https://github.com/Lucent-Financial-Group/Zeta/pull/892), commit `e9dc894`) -**Milestone**: 34 ported + 1 in-flight = 35 total (2 from #849 + 3 from #866 + 3 from #868 + 3 from #870 + 2 from #872 + 3 from #874 + 3 from #876 + 3 from #878 + 3 from #880 + 3 from #882 + 2 from #883 + 2 from #884 + 1 from #885 + 1 from #892 = 34 merged; +1 in-flight in slice-14). Slice-14 opens **budget-cluster** (budget/snapshot-burn). 8 Bucket B files remain. +**Status**: Active (Lane B slice 14 merged — [#894](https://github.com/Lucent-Financial-Group/Zeta/pull/894), commit `9cb21a7`) +**Milestone**: 35 ported + 1 in-flight = 36 total (2 from #849 + 3 from #866 + 3 from #868 + 3 from #870 + 2 from #872 + 3 from #874 + 3 from #876 + 3 from #878 + 3 from #880 + 3 from #882 + 2 from #883 + 2 from #884 + 1 from #885 + 1 from #892 + 1 from #894 = 35 merged; +1 in-flight in slice-15). Slice-15 opens **peer-call cluster** (peer-call/grok). 7 Bucket B files remain. **Current blocker**: None. -**Next concrete action**: Pick a coherent next slice from Bucket B (8 files remaining). Per Gate B: read-only scope first, then re-verify the layered baseline currency before first mutating action. +**Next concrete action**: Pick a coherent next slice from Bucket B (7 files remaining). Per Gate B: read-only scope first, then re-verify the layered baseline currency before first mutating action. **Last updated**: 2026-04-30 ## Why this trajectory exists @@ -64,18 +64,17 @@ tools/profile.sh Rationale: TS/Bun is itself one of the things `install.sh` installs. These scripts cannot depend on Bun. -### Bucket B — Should become TypeScript (8 files remaining) +### Bucket B — Should become TypeScript (7 files remaining) -Post-install scripts that operate on the repo (lints, audits, hygiene checks, peer-call wrappers, budget reports, git ops). Same shape as the scripts ported in #849, #866, #868, #870, #872, #874, #876, #878, #880, #882, #883, #884, #885, #892. The originally-listed audit/lint scripts have progressively ported (1 in slice-14 in flight — budget/snapshot-burn); the bash originals remain in-tree as the equivalence reference and will retire once the TS ports have soaked. +Post-install scripts that operate on the repo (lints, audits, hygiene checks, peer-call wrappers, budget reports, git ops). Same shape as the scripts ported in #849, #866, #868, #870, #872, #874, #876, #878, #880, #882, #883, #884, #885, #892, #894. The originally-listed audit/lint scripts have progressively ported (1 in slice-15 in flight — peer-call/grok); the bash originals remain in-tree as the equivalence reference and will retire once the TS ports have soaked. ```text tools/budget/daily-cost-report.sh tools/budget/project-runway.sh -tools/budget/snapshot-burn.sh # in flight (slice 14) tools/git/batch-resolve-pr-threads.sh tools/peer-call/codex.sh tools/peer-call/gemini.sh -tools/peer-call/grok.sh +tools/peer-call/grok.sh # in flight (slice 15) tools/pr-preservation/archive-pr.sh ``` @@ -133,6 +132,7 @@ tools/skill-catalog/backfill_dv2_frontmatter.sh # ported in #884 tools/audit-packages.sh # ported in #884 tools/backlog/generate-index.sh # ported in #885 tools/git/push-with-retry.sh # ported in #892 +tools/budget/snapshot-burn.sh # ported in #894 ``` ## Recommended next slice diff --git a/docs/trajectories/typescript-bun-migration/slice-audits.md b/docs/trajectories/typescript-bun-migration/slice-audits.md index 9b9e7649c..91ff030ac 100644 --- a/docs/trajectories/typescript-bun-migration/slice-audits.md +++ b/docs/trajectories/typescript-bun-migration/slice-audits.md @@ -411,7 +411,32 @@ Per-port pattern checklist: Slice 6 passes audit. No new patterns recorded — all reused from prior slices. -## Slice 14 — 1 port (budget/snapshot-burn — budget-cluster opens) (PR pending — `lane-b/ts-bun-slice-14-snapshot-burn-2026-04-30`) +## Slice 15 — 1 port (peer-call/grok — peer-call cluster opens) (PR pending — `lane-b/ts-bun-slice-15-peer-call-grok-2026-04-30`) + +**Slice files**: + +- `tools/peer-call/grok.{sh→ts}` (Otto's harness-side caller for invoking Grok via cursor-agent as a peer reviewer) + +**Comparison points**: identical to slice 14. Within Gate B 30-day window. tsc gate active per #890. + +### Code-pattern audit (per-port) + +- **`grok.ts`** (157 → 289 lines): bash arg-parse loop → `classifyFlag` helper + `MutableArgState` so the main `parseArgs` stays under cognitive-complexity 15. Bash `eval "$context_cmd" 2>&1 | head -c 20000` → `spawnSync("/bin/sh", ["-c", contextCmd])` capturing stdout+stderr + slice to `CTX_HEAD_BYTES`; same security posture as bash original (user explicitly supplies the shell command via --context-cmd contract). Bash `head -c 20000 < "$file"` → `readFileSync` + `Buffer.subarray(0, FILE_HEAD_BYTES)`. Bash file-existence check → `isRegularFile` helper using `statSync().isFile()` with try/catch. PREAMBLE preserved verbatim (Otto's contribution to the four-ferry consensus protocol convention). cursor-agent invocation via `spawnSync` with `stdio: "inherit"` so cursor-agent's stdout/stderr stream live to the user (key UX for an LLM-CLI peer-call); exit-code semantics preserved (0 success, 2 if cursor-agent non-zero). eslint-disable for `no-os-command-from-path` placed on the literal next line (the `"cursor-agent"` argument) per the directive-placement pattern from slice-13 (#892 review). + +### Equivalence audit + +- **`grok`**: byte-equivalent on argument-validation paths (`--file` without value → exit 1 with same message; `--context-cmd` without value → exit 1; `--bogus` → exit 1; no prompt → exit 1). LLM-response equivalence is non-deterministic (cursor-agent + Grok produce different completions per invocation); only structural/UX equivalence verified — same flag set, same model selection, same preamble construction, same file/context-cmd injection. + +### Behavioural note vs bash original + +- The bash version uses `cursor-agent --print --output-format ...` directly with stdio inherited; the TS version invokes `cursor-agent` via spawnSync with `stdio: "inherit"` for parity. No buffering differences expected. +- The eval/security boundary is preserved verbatim: user-supplied `--context-cmd` runs through `/bin/sh -c` (TS) which is structurally equivalent to `eval` (bash). Same trust assumption: caller controls the script invocation, so caller controls the context command. + +### Outcome + +Slice 15 passes audit. **Peer-call cluster opens** (first of 3 LLM-CLI wrappers). Bucket B 8 → 7. Sibling ports (gemini.sh + codex.sh) follow the same shape per the cross-script structural similarity (`diff` showed only header-comment + flag-set + model-string variations). + +## Slice 14 — 1 port (budget/snapshot-burn — budget-cluster opens) (PR #894, merged 2026-04-30, commit `9cb21a7`) **Slice files**: diff --git a/tools/peer-call/grok.ts b/tools/peer-call/grok.ts new file mode 100644 index 000000000..7db914c68 --- /dev/null +++ b/tools/peer-call/grok.ts @@ -0,0 +1,289 @@ +#!/usr/bin/env bun +// grok.ts — Claude-Code-side caller for invoking Grok as a peer +// reviewer via cursor-agent. +// +// TypeScript+Bun port of grok.sh, slice 15 of the TS+Bun migration. +// See docs/best-practices/repo-scripting.md. +// +// Usage: +// bun tools/peer-call/grok.ts "prompt text" +// bun tools/peer-call/grok.ts --thinking "prompt text" +// bun tools/peer-call/grok.ts --file path/to/file.fs "prompt text" +// bun tools/peer-call/grok.ts --context-cmd "git diff HEAD~3..HEAD" "prompt text" +// bun tools/peer-call/grok.ts --json "prompt text" +// +// Routing: wraps `cursor-agent --print --model grok-4-20-thinking` +// (default) or `grok-4-20` (with --fast flag). The --print flag +// makes cursor-agent non-interactive (script-friendly). +// +// Per the four-ferry consensus (PR #24): Otto's role is "tests" not +// "owns the peer protocol." This script is Otto's harness-side +// contribution; the protocol convention is what we converge on +// through use, as peers. +// +// Exit codes: +// 0 — Grok responded successfully +// 1 — invocation error (bad arguments, cursor-agent missing, etc.) +// 2 — Grok returned a non-zero exit (response captured to stderr) + +import { readFileSync, statSync } from "node:fs"; +import { spawnSync } from "node:child_process"; + +const SPAWN_MAX_BUFFER = 64 * 1024 * 1024; +const FILE_HEAD_BYTES = 20000; +const CTX_HEAD_BYTES = 20000; + +type Mode = "thinking" | "fast"; +type OutputFormat = "text" | "json" | "stream-json"; + +interface Args { + readonly mode: Mode; + readonly outputFormat: OutputFormat; + readonly file: string; + readonly contextCmd: string; + readonly prompt: string; +} + +interface ArgError { + readonly error: string; + readonly exitCode: 1; +} + +interface ArgHelp { + readonly help: true; +} + +interface MutableArgState { + mode: Mode; + outputFormat: OutputFormat; + file: string; + contextCmd: string; + prompt: string; +} + +type StepResult = + | { readonly kind: "advance"; readonly skip: 1 | 2 } + | { readonly kind: "stop" } + | { readonly kind: "help" } + | { readonly kind: "error"; readonly message: string }; + +function classifyFlag( + a: string, + next: string | undefined, + state: MutableArgState, +): StepResult { + if (a === "--thinking") { state.mode = "thinking"; return { kind: "advance", skip: 1 }; } + if (a === "--fast") { state.mode = "fast"; return { kind: "advance", skip: 1 }; } + if (a === "--json") { state.outputFormat = "json"; return { kind: "advance", skip: 1 }; } + if (a === "--stream") { state.outputFormat = "stream-json"; return { kind: "advance", skip: 1 }; } + if (a === "--file") { + if (next === undefined) return { kind: "error", message: "error: --file requires PATH" }; + state.file = next; + return { kind: "advance", skip: 2 }; + } + if (a === "--context-cmd") { + if (next === undefined) return { kind: "error", message: "error: --context-cmd requires COMMAND" }; + state.contextCmd = next; + return { kind: "advance", skip: 2 }; + } + if (a === "-h" || a === "--help") return { kind: "help" }; + if (a === "--") return { kind: "stop" }; + if (a.startsWith("-")) return { kind: "error", message: `error: unknown flag: ${a}` }; + // Positional: concat into prompt. + state.prompt = state.prompt.length === 0 ? a : `${state.prompt} ${a}`; + return { kind: "advance", skip: 1 }; +} + +function parseArgs(argv: readonly string[]): Args | ArgError | ArgHelp { + const state: MutableArgState = { + mode: "thinking", + outputFormat: "text", + file: "", + contextCmd: "", + prompt: "", + }; + let i = 0; + while (i < argv.length) { + const a = argv[i] ?? ""; + const step = classifyFlag(a, argv[i + 1], state); + if (step.kind === "help") return { help: true }; + if (step.kind === "error") return { error: step.message, exitCode: 1 }; + if (step.kind === "stop") { + // Everything after `--` is the prompt verbatim. + state.prompt = argv.slice(i + 1).join(" "); + break; + } + i += step.skip; + } + return { + mode: state.mode, + outputFormat: state.outputFormat, + file: state.file, + contextCmd: state.contextCmd, + prompt: state.prompt, + }; +} + +function emitHelp(): void { + process.stdout.write( + `grok.ts — Claude-Code-side caller for invoking Grok as a peer\n` + + `reviewer via cursor-agent.\n` + + `\n` + + `Usage:\n` + + ` bun tools/peer-call/grok.ts "prompt text"\n` + + ` bun tools/peer-call/grok.ts --thinking "prompt text"\n` + + ` bun tools/peer-call/grok.ts --fast "prompt text"\n` + + ` bun tools/peer-call/grok.ts --file PATH "prompt text"\n` + + ` bun tools/peer-call/grok.ts --context-cmd "CMD" "prompt text"\n` + + ` bun tools/peer-call/grok.ts --json "prompt text"\n` + + ` bun tools/peer-call/grok.ts --stream "prompt text"\n` + + `\n` + + `Routing: wraps cursor-agent --print --model grok-4-20-thinking\n` + + `(default) or grok-4-20 (with --fast).\n`, + ); +} + +function commandAvailable(cmd: string): boolean { + const result = spawnSync(cmd, ["--version"], { stdio: "ignore" }); + return result.status === 0; +} + +function isRegularFile(path: string): boolean { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +function readHead(path: string, bytes: number): string { + if (!isRegularFile(path)) return ""; + const buf = readFileSync(path); + return buf.subarray(0, bytes).toString("utf8"); +} + +function runContextCmd(contextCmd: string): string { + // Bash uses `eval "$context_cmd" 2>&1 | head -c 20000`. Match that + // shape: shell -c so user-supplied compound commands work, capture + // both stdout + stderr, truncate to CTX_HEAD_BYTES. User intentionally + // supplies the shell command (per --context-cmd contract); same + // security posture as the bash original's `eval`. + const result = spawnSync("/bin/sh", ["-c", contextCmd], { + encoding: "utf8", + maxBuffer: SPAWN_MAX_BUFFER, + }); + const combined = `${result.stdout}${result.stderr}`; + return combined.slice(0, CTX_HEAD_BYTES); +} + +const PREAMBLE = `You are Grok, invoked as a peer reviewer by Otto (Claude Opus 4.7 +running in Claude Code) on the Zeta / Superfluid AI factory. Per the +four-ferry consensus (Amara/Grok/Gemini/Otto) the role distribution +is: Gemini proposes, Grok critiques, Amara sharpens, Otto tests, Git +decides. This call is Otto invoking your critique role. + +Per Aaron's 'agents-not-bots' discipline: you are a peer, not a +subordinate. Push back on Otto's framing if you see it differently. +Don't copy-paste anyone else's work; write from your own +understanding. Make it ours, not anyone-alone-imposed.`; + +interface PromptResult { + readonly ok: boolean; + readonly value: string; +} + +function buildFullPrompt(args: Args): PromptResult { + let full = `${PREAMBLE}\n\n---\n\n${args.prompt}`; + + if (args.file.length > 0) { + if (!isRegularFile(args.file)) { + return { + ok: false, + value: `error: --file path does not exist: ${args.file}`, + }; + } + const head = readHead(args.file, FILE_HEAD_BYTES); + full += `\n\n---\n\nFile context: ${args.file}\n\`\`\`\n${head}\n\`\`\``; + } + + if (args.contextCmd.length > 0) { + const ctxOutput = runContextCmd(args.contextCmd); + full += `\n\n---\n\nContext command: ${args.contextCmd}\nOutput:\n\`\`\`\n${ctxOutput}\n\`\`\``; + } + + return { ok: true, value: full }; +} + +function pickModel(mode: Mode): string { + return mode === "thinking" ? "grok-4-20-thinking" : "grok-4-20"; +} + +export function main(argv: readonly string[]): number { + const parsed = parseArgs(argv); + if ("help" in parsed) { + emitHelp(); + return 0; + } + if ("error" in parsed) { + process.stderr.write(`${parsed.error}\n`); + return parsed.exitCode; + } + if (parsed.prompt.length === 0) { + process.stderr.write("error: prompt required\n"); + process.stderr.write(`see: bun tools/peer-call/grok.ts --help\n`); + return 1; + } + + if (!commandAvailable("cursor-agent")) { + process.stderr.write("error: cursor-agent not on PATH\n"); + process.stderr.write( + "install via Cursor desktop app + ensure ~/.local/bin is on PATH\n", + ); + return 1; + } + + const fullPromptResult = buildFullPrompt(parsed); + if (!fullPromptResult.ok) { + process.stderr.write(`${fullPromptResult.value}\n`); + return 1; + } + + const model = pickModel(parsed.mode); + + // cursor-agent invocation: no shell interpolation (args passed as + // separate array elements). The user's prompt is one fixed argument + // after `--`; cursor-agent does its own argument parsing. Same + // security posture as the bash original. + const result = spawnSync( + // eslint-disable-next-line sonarjs/no-os-command-from-path + "cursor-agent", + [ + "--print", + "--model", + model, + "--output-format", + parsed.outputFormat, + "--mode", + "ask", + "--force", + "--", + fullPromptResult.value, + ], + { + stdio: "inherit", + maxBuffer: SPAWN_MAX_BUFFER, + }, + ); + + const exitCode = result.status ?? 1; + if (exitCode !== 0) { + process.stderr.write("\n"); + process.stderr.write(`cursor-agent exited with code ${String(exitCode)}\n`); + return 2; + } + return 0; +} + +if (import.meta.main) { + process.exit(main(process.argv.slice(2))); +}