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
14 changes: 7 additions & 7 deletions docs/trajectories/typescript-bun-migration/RESUME.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
```

Expand Down Expand Up @@ -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
Expand Down
27 changes: 26 additions & 1 deletion docs/trajectories/typescript-bun-migration/slice-audits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Comment on lines +414 to +423
- **`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**:

Expand Down
289 changes: 289 additions & 0 deletions tools/peer-call/grok.ts
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +19 to +22
//
// 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");
Comment on lines +161 to +162
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit file attachment reads to the configured head size

The TS port now loads the entire --file input into memory before truncating, while the bash version streamed only the first 20,000 bytes via head -c. On large files this can cause unnecessary memory spikes or OOM failures and makes the script much slower for the common “attach a big log/diff file” workflow, even though only a small prefix is ever used in the prompt.

Useful? React with 👍 / 👎.

}
Comment on lines +159 to +163

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,
});

Check warning

Code scanning / CodeQL

Indirect uncontrolled command line Medium

This command depends on an unsanitized
command-line argument
.
Comment thread
AceHack marked this conversation as resolved.
Dismissed
const combined = `${result.stdout}${result.stderr}`;
return combined.slice(0, CTX_HEAD_BYTES);
Comment on lines +175 to +176
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Truncate context output by bytes, not UTF-16 characters

This truncation uses String.slice(0, CTX_HEAD_BYTES), which counts UTF-16 code units, not bytes. The original bash behavior (head -c 20000) was byte-based, so non-ASCII context output can now exceed the intended 20,000-byte cap by a large margin, inflating prompt size and potentially causing token/cost regressions when --context-cmd emits Unicode-heavy output.

Useful? React with 👍 / 👎.

}

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.
Comment on lines +179 to +186
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;
}
Comment on lines +278 to +283
return 0;
}

if (import.meta.main) {
process.exit(main(process.argv.slice(2)));
}
Loading