-
Notifications
You must be signed in to change notification settings - Fork 1
ts(B-0086): port 1 peer-call sibling (.sh→.ts) — slice 17 of TS/Bun migration #900
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
AceHack
merged 1 commit into
main
from
lane-b/ts-bun-slice-17-peer-call-codex-2026-04-30
Apr 30, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,357 @@ | ||
| #!/usr/bin/env bun | ||
| // codex.ts — Claude-Code-side caller for invoking Codex (OpenAI) as | ||
| // a peer reviewer via the codex CLI. | ||
| // | ||
| // TypeScript+Bun port of codex.sh, slice 17 of the TS+Bun migration. | ||
| // Sibling to tools/peer-call/grok.ts (slice 15) and gemini.ts | ||
| // (slice 16). Codex's role: implementation peer / code-grounded | ||
| // second opinion (not in the original four-ferry consensus list, | ||
| // but a recurring PR-review participant). | ||
| // | ||
| // Usage: | ||
| // bun tools/peer-call/codex.ts "prompt text" | ||
| // bun tools/peer-call/codex.ts --model gpt-5.3-codex "prompt text" | ||
| // bun tools/peer-call/codex.ts --file path/to/file.fs "prompt text" | ||
| // bun tools/peer-call/codex.ts --context-cmd "git diff HEAD~3..HEAD" "prompt" | ||
| // bun tools/peer-call/codex.ts --review "review the diff for correctness" | ||
| // | ||
| // Routing: wraps `codex exec -s read-only --skip-git-repo-check` | ||
| // (default; non-interactive, sandboxed). The --review flag routes | ||
| // through `codex review` instead, which is Codex's first-class | ||
| // code-review path. | ||
| // | ||
| // Exit codes (uniform across peer-call siblings per | ||
| // tools/peer-call/README.md): | ||
| // 0 — Codex responded successfully | ||
| // 1 — invocation error (bad arguments, codex missing, etc.) | ||
| // 2 — Codex returned a non-zero exit (diagnostic on stderr) | ||
|
|
||
| import { closeSync, openSync, readSync, 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; | ||
|
|
||
| interface Args { | ||
| readonly model: string; | ||
| readonly reviewMode: boolean; | ||
| readonly file: string; | ||
| readonly contextCmd: string; | ||
| readonly prompt: string; | ||
| } | ||
|
|
||
| interface ArgError { | ||
| readonly error: string; | ||
| // Exit code 1 = invocation/usage error per tools/peer-call/README.md | ||
| // (uniform 0/1/2 across all three peer-call wrappers). | ||
| readonly exitCode: 1; | ||
| } | ||
|
|
||
| interface ArgHelp { | ||
| readonly help: true; | ||
| } | ||
|
|
||
| interface MutableArgState { | ||
| model: string; | ||
| reviewMode: boolean; | ||
| 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 === "--model") { | ||
| if (next === undefined) return { kind: "error", message: "error: --model requires NAME" }; | ||
| state.model = next; | ||
| return { kind: "advance", skip: 2 }; | ||
| } | ||
| if (a === "--review") { state.reviewMode = true; 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}` }; | ||
| 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 = { | ||
| model: "", | ||
| reviewMode: false, | ||
| 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") { | ||
| state.prompt = argv.slice(i + 1).join(" "); | ||
| break; | ||
| } | ||
| i += step.skip; | ||
| } | ||
| return { | ||
| model: state.model, | ||
| reviewMode: state.reviewMode, | ||
| file: state.file, | ||
| contextCmd: state.contextCmd, | ||
| prompt: state.prompt, | ||
| }; | ||
| } | ||
|
|
||
| function emitHelp(): void { | ||
| process.stdout.write( | ||
| `codex.ts — Claude-Code-side caller for invoking Codex as a peer\n` + | ||
| `reviewer via the codex CLI.\n` + | ||
| `\n` + | ||
| `Usage:\n` + | ||
| ` bun tools/peer-call/codex.ts "prompt text"\n` + | ||
| ` bun tools/peer-call/codex.ts --model NAME "prompt text"\n` + | ||
| ` bun tools/peer-call/codex.ts --file PATH "prompt text"\n` + | ||
| ` bun tools/peer-call/codex.ts --context-cmd "CMD" "prompt text"\n` + | ||
| ` bun tools/peer-call/codex.ts --review "prompt text"\n` + | ||
| `\n` + | ||
| `Routing: wraps codex exec -s read-only --skip-git-repo-check (default)\n` + | ||
| `or codex review (with --review). --model is ignored in --review mode.\n`, | ||
| ); | ||
| } | ||
|
|
||
| function commandAvailable(cmd: string): boolean { | ||
| // Match bash `command -v <cmd>` semantics (PATH existence) per the | ||
| // round-2 fix on slice-16 #898. | ||
| const result = spawnSync("/bin/sh", ["-c", `command -v "${cmd}"`], { | ||
| stdio: "ignore", | ||
| }); | ||
| return result.status === 0; | ||
| } | ||
|
|
||
| function isRegularFile(path: string): boolean { | ||
| try { | ||
| return statSync(path).isFile(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| interface ReadHeadResult { | ||
| readonly ok: boolean; | ||
| readonly content: string; | ||
| readonly error: string; | ||
| } | ||
|
|
||
| function readHead(path: string, bytes: number): ReadHeadResult { | ||
| // Read only the first `bytes` bytes (matches bash `head -c`); surfaces | ||
| // read failures via ReadHeadResult per Codex P2 on #898. | ||
| if (!isRegularFile(path)) { | ||
| return { ok: false, content: "", error: "not a regular file" }; | ||
| } | ||
| const buf = Buffer.alloc(bytes); | ||
| let fd: number | undefined; | ||
| try { | ||
| fd = openSync(path, "r"); | ||
| const n = readSync(fd, buf, 0, bytes, 0); | ||
| return { ok: true, content: buf.subarray(0, n).toString("utf8"), error: "" }; | ||
| } catch (err) { | ||
| const message = err instanceof Error ? err.message : String(err); | ||
| return { ok: false, content: "", error: message }; | ||
| } finally { | ||
| if (fd !== undefined) closeSync(fd); | ||
| } | ||
| } | ||
|
|
||
| function runContextCmd(contextCmd: string): string { | ||
| // Bash uses `eval "$context_cmd" 2>&1 | head -c 20000`. Match that | ||
| // shape end-to-end via /bin/bash -c (preserves bash-only features | ||
| // like `[[ ]]`, brace expansion, process substitution per Codex P2 | ||
| // on #898). Concat stdout + stderr to preserve shell parse errors | ||
| // that fall outside the inner `( ) 2>&1` redirect. | ||
| const wrapped = `(${contextCmd}) 2>&1 | head -c ${String(CTX_HEAD_BYTES)}`; | ||
| const result = spawnSync("/bin/bash", ["-c", wrapped], { | ||
| encoding: "utf8", | ||
| maxBuffer: SPAWN_MAX_BUFFER, | ||
| }); | ||
Check warningCode scanning / CodeQL Indirect uncontrolled command line Medium
This command depends on an unsanitized
command-line argument Error loading related location Loading |
||
|
AceHack marked this conversation as resolved.
Dismissed
|
||
| return `${result.stdout}${result.stderr}`.slice(0, CTX_HEAD_BYTES); | ||
| } | ||
|
|
||
| interface SpawnError { | ||
| readonly code?: string; | ||
| } | ||
|
|
||
| interface ChildOutcome { | ||
| readonly status: number; | ||
| readonly note: string; | ||
| } | ||
|
|
||
| function classifySpawnFailure( | ||
| status: number | null, | ||
| signal: string | null, | ||
| error: SpawnError | undefined, | ||
| ): ChildOutcome { | ||
| // 4-case helper (status set / ENOENT / signal / other) reused from | ||
| // PR #887 + slice-16 #898. Matches bash exit-code semantics where | ||
| // practical (127 for command-not-found). | ||
| if (status !== null) return { status, note: "" }; | ||
| if (error?.code === "ENOENT") { | ||
| return { status: 127, note: "command not found on PATH (ENOENT)" }; | ||
| } | ||
| if (error?.code !== undefined) { | ||
| return { status: 1, note: `spawn failed (${error.code})` }; | ||
| } | ||
| if (signal !== null) { | ||
| return { status: 1, note: `terminated by signal ${signal}` }; | ||
| } | ||
| return { status: 1, note: "terminated without exit code" }; | ||
| } | ||
|
|
||
| const PREAMBLE = `You are Codex, 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 canonical role distribution is: Gemini proposes, Grok | ||
| critiques, Amara sharpens, Otto tests, Git decides. You aren't | ||
| in the four-ferry list explicitly, but you've been a recurring | ||
| PR-review peer this session — your role is implementation peer | ||
| / code-grounded second opinion. | ||
|
|
||
| Per Aaron's 'agents-not-bots' discipline: you are a peer, not | ||
| a subordinate. Push back on Otto's framing if the code says | ||
| otherwise. Don't copy-paste anyone else's review; reason from | ||
| the artifact in front of you. Make it ours, not | ||
|
AceHack marked this conversation as resolved.
|
||
| 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 headResult = readHead(args.file, FILE_HEAD_BYTES); | ||
| if (!headResult.ok) { | ||
| return { | ||
| ok: false, | ||
| value: `error: --file read failed for ${args.file}: ${headResult.error}`, | ||
| }; | ||
| } | ||
| full += `\n\n---\n\nFile context: ${args.file}\n\`\`\`\n${headResult.content}\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 buildCodexArgs(args: Args, fullPrompt: string): readonly string[] { | ||
| if (args.reviewMode) { | ||
| if (args.model.length > 0) { | ||
| // `codex review` uses its own model selection; the bash original | ||
| // emits a warning if --model is also given. Preserved verbatim. | ||
| process.stderr.write( | ||
| "warning: --model is ignored in --review mode (codex review uses its own model selection)\n", | ||
| ); | ||
| } | ||
| return ["review", fullPrompt]; | ||
| } | ||
| const out: string[] = ["exec", "-s", "read-only", "--skip-git-repo-check"]; | ||
| if (args.model.length > 0) { | ||
| out.push("-m", args.model); | ||
| } | ||
| out.push(fullPrompt); | ||
| return out; | ||
| } | ||
|
|
||
| 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/codex.ts --help\n"); | ||
| return 1; | ||
| } | ||
|
|
||
| if (!commandAvailable("codex")) { | ||
| process.stderr.write("error: codex not on PATH\n"); | ||
| process.stderr.write( | ||
| "install via: npm i -g @openai/codex (or per the maintainer's setup)\n", | ||
| ); | ||
| return 1; | ||
| } | ||
|
|
||
| const promptResult = buildFullPrompt(parsed); | ||
| if (!promptResult.ok) { | ||
| process.stderr.write(`${promptResult.value}\n`); | ||
| return 1; | ||
| } | ||
|
|
||
| const codexArgs = buildCodexArgs(parsed, promptResult.value); | ||
|
|
||
| const result = spawnSync( | ||
| // eslint-disable-next-line sonarjs/no-os-command-from-path | ||
| "codex", | ||
| codexArgs, | ||
| { | ||
| stdio: "inherit", | ||
| maxBuffer: SPAWN_MAX_BUFFER, | ||
| }, | ||
| ); | ||
|
|
||
| const classified = classifySpawnFailure( | ||
| result.status, | ||
| result.signal, | ||
| result.error as SpawnError | undefined, | ||
| ); | ||
| if (classified.note.length > 0) { | ||
| process.stderr.write(`codex: ${classified.note}\n`); | ||
| } | ||
| if (classified.status !== 0) { | ||
| process.stderr.write("\n"); | ||
| process.stderr.write(`codex exited with code ${String(classified.status)}\n`); | ||
| return 2; | ||
| } | ||
| return 0; | ||
| } | ||
|
|
||
| if (import.meta.main) { | ||
| process.exit(main(process.argv.slice(2))); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.