diff --git a/docs/runbook/opencode-codex-bridge.md b/docs/runbook/opencode-codex-bridge.md new file mode 100644 index 0000000..1f43258 --- /dev/null +++ b/docs/runbook/opencode-codex-bridge.md @@ -0,0 +1,33 @@ +# OpenCode + Codex Bridge Runbook + +This runbook documents the orchestration contract for patch-first execution. + +## Contract + +- OpenCode writes a Task Spec for each execution batch. +- OpenCode invokes `run_codex_cli --spec --json`. +- The bridge runs Codex as a child process and returns JSON. +- OpenCode alone decides accept, reject, or retry. + +## Required Flow + +1. OpenCode creates a Task Spec with scope, constraints, and acceptance commands. +2. OpenCode runs the bridge: + + ```bash + node tools/codex-bridge/dist/run-codex-cli.js --spec task-spec.yaml --json + ``` + +3. OpenCode reads JSON output and evaluates: + - `scope_report.ok == true` + - `apply.ok == true` + - acceptance commands success when `acceptance.must_pass == true` + - diff is consistent with batch boundaries + +4. If rejected, OpenCode narrows the Task Spec and retries. + +## Suggested OpenCode Checks + +- Refuse to apply when `scope_report.ok == false`. +- Require all commands to pass when `must_pass == true`. +- Store the JSON result alongside the spec for auditability. diff --git a/tools/codex-bridge/README.md b/tools/codex-bridge/README.md new file mode 100644 index 0000000..90c9998 --- /dev/null +++ b/tools/codex-bridge/README.md @@ -0,0 +1,95 @@ +# Codex Bridge (Patch-First) + +This bridge runs Codex as a local child process, enforces a patch-only contract, +audits scope and constraints before applying changes, runs acceptance commands, +and returns structured JSON. + +## Contract + +Codex must output a single unified diff in a single fenced block: + +```diff +diff --git a/file.txt b/file.txt +--- a/file.txt ++++ b/file.txt +@@ -1 +1,2 @@ + line ++added +``` + +No other text is allowed outside the diff block. + +## Usage + +Build first: + +```bash +npm run build +``` + +Run the bridge: + +```bash +node dist/run-codex-cli.js --spec path/to/task-spec.yaml --json +``` + +Optional flags: + +```bash +node dist/run-codex-cli.js \ + --spec path/to/task-spec.yaml \ + --repo-root . \ + --codex-bin /path/to/openai \ + --codex-args "codex" \ + --json +``` + +## Stub Codex Example + +```bash +cat > codex-stub.sh <<'EOF' +#!/usr/bin/env bash +cat <<'DIFF' +```diff +diff --git a/a.txt b/a.txt +--- a/a.txt ++++ b/a.txt +@@ -1 +1,2 @@ + hello ++world +``` +DIFF +EOF +chmod +x codex-stub.sh +``` + +Then run: + +```bash +node dist/run-codex-cli.js --spec examples/task-spec.yaml --json --codex-bin ./codex-stub.sh +``` + +## Result Shape (JSON) + +```json +{ + "task_id": "T-...", + "result_id": "R-...", + "status": "pass|fail", + "patch": "(unified diff)", + "scope_report": { "ok": true, "files_changed": ["..."], "violations": [] }, + "apply": { "ok": true, "stderr": "" }, + "commands": [ + { "name": "typecheck", "ok": true, "exit_code": 0, "duration_ms": 1234, + "stdout": "...", "stderr": "...", "timed_out": false } + ], + "vcs": { "git_status": "...", "git_diff": "..." }, + "notes": "" +} +``` + +## Tests + +```bash +npm test +``` diff --git a/tools/codex-bridge/dist/codex/run.js b/tools/codex-bridge/dist/codex/run.js new file mode 100644 index 0000000..c241f34 --- /dev/null +++ b/tools/codex-bridge/dist/codex/run.js @@ -0,0 +1,9 @@ +import { execa } from 'execa'; +export async function runCodex(args) { + const r = await execa(args.codexBin, args.codexArgs, { + cwd: args.cwd, + input: args.prompt, + timeout: args.timeoutMs + }); + return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 }; +} diff --git a/tools/codex-bridge/dist/exec/run.js b/tools/codex-bridge/dist/exec/run.js new file mode 100644 index 0000000..9e804fb --- /dev/null +++ b/tools/codex-bridge/dist/exec/run.js @@ -0,0 +1,33 @@ +import { execa } from 'execa'; +export async function runCommand(args) { + const start = Date.now(); + try { + const r = await execa(args.command, { cwd: args.cwd, shell: true, timeout: args.timeoutMs }); + return { + ok: true, + exitCode: r.exitCode ?? 0, + durationMs: Date.now() - start, + timedOut: false, + stdout: truncate(r.stdout ?? '', args.maxOutputBytes), + stderr: truncate(r.stderr ?? '', args.maxOutputBytes) + }; + } + catch (e) { + const stdout = e?.stdout ?? ''; + const stderr = e?.stderr ?? String(e); + return { + ok: false, + exitCode: e?.exitCode ?? 1, + durationMs: Date.now() - start, + timedOut: Boolean(e?.timedOut), + stdout: truncate(stdout, args.maxOutputBytes), + stderr: truncate(stderr, args.maxOutputBytes) + }; + } +} +function truncate(s, maxBytes) { + const b = Buffer.from(s, 'utf8'); + if (b.byteLength <= maxBytes) + return s; + return b.subarray(0, maxBytes).toString('utf8') + '\n[truncated]'; +} diff --git a/tools/codex-bridge/dist/index.js b/tools/codex-bridge/dist/index.js new file mode 100644 index 0000000..67f2b1e --- /dev/null +++ b/tools/codex-bridge/dist/index.js @@ -0,0 +1 @@ +export { runCodexCli } from './run-codex-cli.js'; diff --git a/tools/codex-bridge/dist/patch/changed-files.js b/tools/codex-bridge/dist/patch/changed-files.js new file mode 100644 index 0000000..2678aed --- /dev/null +++ b/tools/codex-bridge/dist/patch/changed-files.js @@ -0,0 +1,11 @@ +export function changedFilesFromUnifiedDiff(diff) { + const files = new Set(); + for (const line of diff.split('\n')) { + if (line.startsWith('+++ b/')) { + const p = line.slice('+++ b/'.length).trim(); + if (p !== '/dev/null') + files.add(p); + } + } + return [...files]; +} diff --git a/tools/codex-bridge/dist/patch/extract.js b/tools/codex-bridge/dist/patch/extract.js new file mode 100644 index 0000000..56e579b --- /dev/null +++ b/tools/codex-bridge/dist/patch/extract.js @@ -0,0 +1,11 @@ +export function extractSingleDiffBlock(stdout) { + const re = /```diff\n([\s\S]*?)\n```/g; + const matches = [...stdout.matchAll(re)]; + if (matches.length !== 1) + throw new Error('Expected exactly one ```diff block'); + const [full, body] = matches[0]; + const outside = stdout.replace(full, '').trim(); + if (outside.length) + throw new Error('Unexpected text outside diff block'); + return body; +} diff --git a/tools/codex-bridge/dist/run-codex-cli.js b/tools/codex-bridge/dist/run-codex-cli.js new file mode 100644 index 0000000..9b94b52 --- /dev/null +++ b/tools/codex-bridge/dist/run-codex-cli.js @@ -0,0 +1,228 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseTaskSpecYaml } from './spec.js'; +import { runCodex } from './codex/run.js'; +import { extractSingleDiffBlock } from './patch/extract.js'; +import { changedFilesFromUnifiedDiff } from './patch/changed-files.js'; +import { auditScope, auditConstraints } from './scope/audit.js'; +import { gitApply, gitApplyCheck, gitDiff, gitStatusPorcelain } from './vcs/git.js'; +import { runCommand } from './exec/run.js'; +export async function runCodexCli(args) { + const specText = await readFile(args.spec, 'utf8'); + const spec = parseTaskSpecYaml(specText); + const repoRoot = path.resolve(process.cwd(), args.repoRoot ?? spec.repo_root); + const codexBin = args.codexBin ?? 'openai'; + const codexArgs = args.codexArgs ?? ['codex']; + const resultBase = { + task_id: spec.task_id, + result_id: `R-${Date.now()}`, + status: 'fail', + patch: '', + scope_report: { ok: true, files_changed: [], violations: [] }, + apply: { ok: false, stderr: '' }, + commands: [], + vcs: { git_status: '', git_diff: '' }, + notes: '' + }; + const prompt = buildPrompt(spec); + let rawStdout = ''; + try { + const codexResult = await runCodex({ + cwd: repoRoot, + codexBin, + codexArgs, + prompt, + timeoutMs: 30 * 60 * 1000 + }); + rawStdout = codexResult.stdout; + } + catch (e) { + return await finalizeResult({ + ...resultBase, + error_kind: 'environment_failed', + error_message: e?.message ?? String(e) + }, repoRoot); + } + let patch = ''; + try { + patch = extractSingleDiffBlock(rawStdout); + if (!patch.endsWith('\n')) + patch += '\n'; + } + catch (e) { + return await finalizeResult({ + ...resultBase, + error_kind: 'patch_contract_violation', + error_message: e?.message ?? String(e) + }, repoRoot); + } + const filesChanged = changedFilesFromUnifiedDiff(patch); + const scopeReport = auditScope({ + files: filesChanged, + include: spec.scope.include, + exclude: spec.scope.exclude, + forbid: spec.constraints.forbid_paths + }); + const constraintReport = auditConstraints({ + filesChanged: filesChanged.length, + maxFilesChanged: spec.constraints.max_files_changed, + diffLines: patch.split('\n').length, + maxDiffLines: spec.constraints.max_diff_lines + }); + const binaryViolations = detectBinaryViolations(patch, spec.constraints.require_no_new_binary_files); + const violations = [...scopeReport.violations, ...constraintReport.violations, ...binaryViolations]; + const scopeReportMerged = { + ok: violations.length === 0, + files_changed: filesChanged, + violations + }; + if (!scopeReportMerged.ok) { + return await finalizeResult({ + ...resultBase, + patch, + scope_report: scopeReportMerged, + error_kind: 'scope_violation', + error_message: 'Patch violates scope or constraints' + }, repoRoot); + } + let applyOk = true; + let applyErr = ''; + try { + await gitApplyCheck(repoRoot, patch); + await gitApply(repoRoot, patch); + } + catch (e) { + applyOk = false; + applyErr = e?.stderr ?? e?.message ?? String(e); + } + if (!applyOk) { + return await finalizeResult({ + ...resultBase, + patch, + scope_report: scopeReportMerged, + apply: { ok: false, stderr: applyErr }, + error_kind: 'patch_apply_failed', + error_message: applyErr + }, repoRoot); + } + const commands = []; + for (const cmd of spec.acceptance.commands) { + const res = await runCommand({ + cwd: repoRoot, + command: cmd.run, + timeoutMs: cmd.timeout_sec * 1000, + maxOutputBytes: 50_000 + }); + commands.push({ + name: cmd.name, + ok: res.ok, + exit_code: res.exitCode, + duration_ms: res.durationMs, + stdout: res.stdout, + stderr: res.stderr, + timed_out: res.timedOut + }); + } + const commandsOk = spec.acceptance.must_pass ? commands.every((c) => c.ok) : true; + const status = scopeReportMerged.ok && applyOk && commandsOk ? 'pass' : 'fail'; + return await finalizeResult({ + ...resultBase, + status, + patch, + scope_report: scopeReportMerged, + apply: { ok: true, stderr: '' }, + commands, + error_kind: commandsOk ? undefined : 'acceptance_failed', + error_message: commandsOk ? undefined : 'Acceptance commands failed' + }, repoRoot); +} +async function finalizeResult(result, repoRoot) { + try { + result.vcs.git_status = await gitStatusPorcelain(repoRoot); + result.vcs.git_diff = await gitDiff(repoRoot); + } + catch (e) { + result.vcs.git_status = result.vcs.git_status || ''; + result.vcs.git_diff = result.vcs.git_diff || ''; + result.error_kind = result.error_kind ?? 'environment_failed'; + result.error_message = result.error_message ?? (e?.message ?? String(e)); + } + return result; +} +function detectBinaryViolations(patch, requireNoBinary) { + if (!requireNoBinary) + return []; + const violations = []; + if (/^GIT binary patch/m.test(patch) || /^Binary files /m.test(patch)) { + violations.push({ rule: 'constraints.require_no_new_binary_files', file: '', detail: 'binary patch detected' }); + } + return violations; +} +function buildPrompt(spec) { + const lines = [ + `Goal: ${spec.goal}`, + '', + 'Scope:', + `- Include: ${spec.scope.include.join(', ')}`, + `- Exclude: ${spec.scope.exclude.join(', ')}`, + '', + 'Constraints:', + `- max_files_changed: ${spec.constraints.max_files_changed}`, + `- max_diff_lines: ${spec.constraints.max_diff_lines}`, + `- forbid_paths: ${spec.constraints.forbid_paths.join(', ')}`, + `- require_no_new_binary_files: ${spec.constraints.require_no_new_binary_files}`, + '', + 'Acceptance Commands (read-only):', + ...spec.acceptance.commands.map((c) => `- ${c.name}: ${c.run}`), + '', + 'Output Contract:', + spec.codex.prompt_contract + ]; + return lines.join('\n'); +} +function parseArgs(argv) { + const args = { spec: '' }; + for (let i = 0; i < argv.length; i++) { + const token = argv[i]; + if (!token.startsWith('--')) + continue; + const key = token.slice(2); + if (key === 'json') { + args.json = true; + continue; + } + const value = argv[i + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for --${key}`); + } + i++; + if (key === 'spec') + args.spec = value; + if (key === 'repo-root') + args.repoRoot = value; + if (key === 'codex-bin') + args.codexBin = value; + if (key === 'codex-args') + args.codexArgs = value.split(' ').filter(Boolean); + } + if (!args.spec) + throw new Error('Missing --spec'); + return args; +} +async function runCli() { + const args = parseArgs(process.argv.slice(2)); + const result = await runCodexCli(args); + if (args.json) { + process.stdout.write(JSON.stringify(result)); + } + else { + process.stdout.write(JSON.stringify(result, null, 2)); + } +} +if (process.argv[1] === fileURLToPath(import.meta.url)) { + runCli().catch((err) => { + process.stderr.write(String(err)); + process.exit(1); + }); +} diff --git a/tools/codex-bridge/dist/scope/audit.js b/tools/codex-bridge/dist/scope/audit.js new file mode 100644 index 0000000..8a8e6cd --- /dev/null +++ b/tools/codex-bridge/dist/scope/audit.js @@ -0,0 +1,32 @@ +import { minimatch } from 'minimatch'; +function anyMatch(path, globs) { + return globs.some((g) => minimatch(path, g, { dot: true })); +} +export function auditScope(args) { + const violations = []; + for (const file of args.files) { + if (!anyMatch(file, args.include)) { + violations.push({ rule: 'scope.include', file, detail: 'not included' }); + continue; + } + if (anyMatch(file, args.exclude)) { + violations.push({ rule: 'scope.exclude', file, detail: 'excluded by pattern' }); + continue; + } + if (anyMatch(file, args.forbid)) { + violations.push({ rule: 'constraints.forbid_paths', file, detail: 'forbidden by policy' }); + continue; + } + } + return { ok: violations.length === 0, files_changed: args.files, violations }; +} +export function auditConstraints(args) { + const violations = []; + if (args.filesChanged > args.maxFilesChanged) { + violations.push({ rule: 'constraints.max_files_changed', file: '', detail: 'too many files changed' }); + } + if (args.diffLines > args.maxDiffLines) { + violations.push({ rule: 'constraints.max_diff_lines', file: '', detail: 'diff too large' }); + } + return { ok: violations.length === 0, violations }; +} diff --git a/tools/codex-bridge/dist/spec.js b/tools/codex-bridge/dist/spec.js new file mode 100644 index 0000000..15eb306 --- /dev/null +++ b/tools/codex-bridge/dist/spec.js @@ -0,0 +1,49 @@ +import yaml from 'js-yaml'; +import { z } from 'zod'; +const CommandSchema = z.object({ + name: z.string().min(1), + run: z.string().min(1), + timeout_sec: z.number().int().positive() +}); +export const TaskSpecSchema = z.object({ + task_id: z.string().min(1), + repo_root: z.string().min(1), + goal: z.string().min(1), + context: z + .object({ + branch: z.string().optional(), + last_result_id: z.string().nullable().optional(), + hints: z + .object({ + failing_files: z.array(z.string()).optional(), + last_error_summary: z.string().nullable().optional() + }) + .optional() + }) + .optional(), + scope: z.object({ + include: z.array(z.string()).min(1), + exclude: z.array(z.string()).default([]) + }), + constraints: z.object({ + max_files_changed: z.number().int().positive(), + max_diff_lines: z.number().int().positive(), + forbid_paths: z.array(z.string()).default([]), + require_no_new_binary_files: z.boolean().default(true) + }), + acceptance: z.object({ + must_pass: z.boolean(), + commands: z.array(CommandSchema).min(1) + }), + codex: z.object({ + mode: z.literal('patch_only'), + prompt_contract: z.string().min(1) + }), + outputs: z.object({ + capture: z.array(z.string()).default([]) + }) +}); +export function parseTaskSpecYaml(text) { + const raw = yaml.load(text); + return TaskSpecSchema.parse(raw); +} diff --git a/tools/codex-bridge/dist/src/codex/run.js b/tools/codex-bridge/dist/src/codex/run.js new file mode 100644 index 0000000..c241f34 --- /dev/null +++ b/tools/codex-bridge/dist/src/codex/run.js @@ -0,0 +1,9 @@ +import { execa } from 'execa'; +export async function runCodex(args) { + const r = await execa(args.codexBin, args.codexArgs, { + cwd: args.cwd, + input: args.prompt, + timeout: args.timeoutMs + }); + return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 }; +} diff --git a/tools/codex-bridge/dist/src/exec/run.js b/tools/codex-bridge/dist/src/exec/run.js new file mode 100644 index 0000000..9e804fb --- /dev/null +++ b/tools/codex-bridge/dist/src/exec/run.js @@ -0,0 +1,33 @@ +import { execa } from 'execa'; +export async function runCommand(args) { + const start = Date.now(); + try { + const r = await execa(args.command, { cwd: args.cwd, shell: true, timeout: args.timeoutMs }); + return { + ok: true, + exitCode: r.exitCode ?? 0, + durationMs: Date.now() - start, + timedOut: false, + stdout: truncate(r.stdout ?? '', args.maxOutputBytes), + stderr: truncate(r.stderr ?? '', args.maxOutputBytes) + }; + } + catch (e) { + const stdout = e?.stdout ?? ''; + const stderr = e?.stderr ?? String(e); + return { + ok: false, + exitCode: e?.exitCode ?? 1, + durationMs: Date.now() - start, + timedOut: Boolean(e?.timedOut), + stdout: truncate(stdout, args.maxOutputBytes), + stderr: truncate(stderr, args.maxOutputBytes) + }; + } +} +function truncate(s, maxBytes) { + const b = Buffer.from(s, 'utf8'); + if (b.byteLength <= maxBytes) + return s; + return b.subarray(0, maxBytes).toString('utf8') + '\n[truncated]'; +} diff --git a/tools/codex-bridge/dist/src/index.js b/tools/codex-bridge/dist/src/index.js new file mode 100644 index 0000000..67f2b1e --- /dev/null +++ b/tools/codex-bridge/dist/src/index.js @@ -0,0 +1 @@ +export { runCodexCli } from './run-codex-cli.js'; diff --git a/tools/codex-bridge/dist/src/patch/changed-files.js b/tools/codex-bridge/dist/src/patch/changed-files.js new file mode 100644 index 0000000..2678aed --- /dev/null +++ b/tools/codex-bridge/dist/src/patch/changed-files.js @@ -0,0 +1,11 @@ +export function changedFilesFromUnifiedDiff(diff) { + const files = new Set(); + for (const line of diff.split('\n')) { + if (line.startsWith('+++ b/')) { + const p = line.slice('+++ b/'.length).trim(); + if (p !== '/dev/null') + files.add(p); + } + } + return [...files]; +} diff --git a/tools/codex-bridge/dist/src/patch/extract.js b/tools/codex-bridge/dist/src/patch/extract.js new file mode 100644 index 0000000..56e579b --- /dev/null +++ b/tools/codex-bridge/dist/src/patch/extract.js @@ -0,0 +1,11 @@ +export function extractSingleDiffBlock(stdout) { + const re = /```diff\n([\s\S]*?)\n```/g; + const matches = [...stdout.matchAll(re)]; + if (matches.length !== 1) + throw new Error('Expected exactly one ```diff block'); + const [full, body] = matches[0]; + const outside = stdout.replace(full, '').trim(); + if (outside.length) + throw new Error('Unexpected text outside diff block'); + return body; +} diff --git a/tools/codex-bridge/dist/src/run-codex-cli.js b/tools/codex-bridge/dist/src/run-codex-cli.js new file mode 100644 index 0000000..9b94b52 --- /dev/null +++ b/tools/codex-bridge/dist/src/run-codex-cli.js @@ -0,0 +1,228 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parseTaskSpecYaml } from './spec.js'; +import { runCodex } from './codex/run.js'; +import { extractSingleDiffBlock } from './patch/extract.js'; +import { changedFilesFromUnifiedDiff } from './patch/changed-files.js'; +import { auditScope, auditConstraints } from './scope/audit.js'; +import { gitApply, gitApplyCheck, gitDiff, gitStatusPorcelain } from './vcs/git.js'; +import { runCommand } from './exec/run.js'; +export async function runCodexCli(args) { + const specText = await readFile(args.spec, 'utf8'); + const spec = parseTaskSpecYaml(specText); + const repoRoot = path.resolve(process.cwd(), args.repoRoot ?? spec.repo_root); + const codexBin = args.codexBin ?? 'openai'; + const codexArgs = args.codexArgs ?? ['codex']; + const resultBase = { + task_id: spec.task_id, + result_id: `R-${Date.now()}`, + status: 'fail', + patch: '', + scope_report: { ok: true, files_changed: [], violations: [] }, + apply: { ok: false, stderr: '' }, + commands: [], + vcs: { git_status: '', git_diff: '' }, + notes: '' + }; + const prompt = buildPrompt(spec); + let rawStdout = ''; + try { + const codexResult = await runCodex({ + cwd: repoRoot, + codexBin, + codexArgs, + prompt, + timeoutMs: 30 * 60 * 1000 + }); + rawStdout = codexResult.stdout; + } + catch (e) { + return await finalizeResult({ + ...resultBase, + error_kind: 'environment_failed', + error_message: e?.message ?? String(e) + }, repoRoot); + } + let patch = ''; + try { + patch = extractSingleDiffBlock(rawStdout); + if (!patch.endsWith('\n')) + patch += '\n'; + } + catch (e) { + return await finalizeResult({ + ...resultBase, + error_kind: 'patch_contract_violation', + error_message: e?.message ?? String(e) + }, repoRoot); + } + const filesChanged = changedFilesFromUnifiedDiff(patch); + const scopeReport = auditScope({ + files: filesChanged, + include: spec.scope.include, + exclude: spec.scope.exclude, + forbid: spec.constraints.forbid_paths + }); + const constraintReport = auditConstraints({ + filesChanged: filesChanged.length, + maxFilesChanged: spec.constraints.max_files_changed, + diffLines: patch.split('\n').length, + maxDiffLines: spec.constraints.max_diff_lines + }); + const binaryViolations = detectBinaryViolations(patch, spec.constraints.require_no_new_binary_files); + const violations = [...scopeReport.violations, ...constraintReport.violations, ...binaryViolations]; + const scopeReportMerged = { + ok: violations.length === 0, + files_changed: filesChanged, + violations + }; + if (!scopeReportMerged.ok) { + return await finalizeResult({ + ...resultBase, + patch, + scope_report: scopeReportMerged, + error_kind: 'scope_violation', + error_message: 'Patch violates scope or constraints' + }, repoRoot); + } + let applyOk = true; + let applyErr = ''; + try { + await gitApplyCheck(repoRoot, patch); + await gitApply(repoRoot, patch); + } + catch (e) { + applyOk = false; + applyErr = e?.stderr ?? e?.message ?? String(e); + } + if (!applyOk) { + return await finalizeResult({ + ...resultBase, + patch, + scope_report: scopeReportMerged, + apply: { ok: false, stderr: applyErr }, + error_kind: 'patch_apply_failed', + error_message: applyErr + }, repoRoot); + } + const commands = []; + for (const cmd of spec.acceptance.commands) { + const res = await runCommand({ + cwd: repoRoot, + command: cmd.run, + timeoutMs: cmd.timeout_sec * 1000, + maxOutputBytes: 50_000 + }); + commands.push({ + name: cmd.name, + ok: res.ok, + exit_code: res.exitCode, + duration_ms: res.durationMs, + stdout: res.stdout, + stderr: res.stderr, + timed_out: res.timedOut + }); + } + const commandsOk = spec.acceptance.must_pass ? commands.every((c) => c.ok) : true; + const status = scopeReportMerged.ok && applyOk && commandsOk ? 'pass' : 'fail'; + return await finalizeResult({ + ...resultBase, + status, + patch, + scope_report: scopeReportMerged, + apply: { ok: true, stderr: '' }, + commands, + error_kind: commandsOk ? undefined : 'acceptance_failed', + error_message: commandsOk ? undefined : 'Acceptance commands failed' + }, repoRoot); +} +async function finalizeResult(result, repoRoot) { + try { + result.vcs.git_status = await gitStatusPorcelain(repoRoot); + result.vcs.git_diff = await gitDiff(repoRoot); + } + catch (e) { + result.vcs.git_status = result.vcs.git_status || ''; + result.vcs.git_diff = result.vcs.git_diff || ''; + result.error_kind = result.error_kind ?? 'environment_failed'; + result.error_message = result.error_message ?? (e?.message ?? String(e)); + } + return result; +} +function detectBinaryViolations(patch, requireNoBinary) { + if (!requireNoBinary) + return []; + const violations = []; + if (/^GIT binary patch/m.test(patch) || /^Binary files /m.test(patch)) { + violations.push({ rule: 'constraints.require_no_new_binary_files', file: '', detail: 'binary patch detected' }); + } + return violations; +} +function buildPrompt(spec) { + const lines = [ + `Goal: ${spec.goal}`, + '', + 'Scope:', + `- Include: ${spec.scope.include.join(', ')}`, + `- Exclude: ${spec.scope.exclude.join(', ')}`, + '', + 'Constraints:', + `- max_files_changed: ${spec.constraints.max_files_changed}`, + `- max_diff_lines: ${spec.constraints.max_diff_lines}`, + `- forbid_paths: ${spec.constraints.forbid_paths.join(', ')}`, + `- require_no_new_binary_files: ${spec.constraints.require_no_new_binary_files}`, + '', + 'Acceptance Commands (read-only):', + ...spec.acceptance.commands.map((c) => `- ${c.name}: ${c.run}`), + '', + 'Output Contract:', + spec.codex.prompt_contract + ]; + return lines.join('\n'); +} +function parseArgs(argv) { + const args = { spec: '' }; + for (let i = 0; i < argv.length; i++) { + const token = argv[i]; + if (!token.startsWith('--')) + continue; + const key = token.slice(2); + if (key === 'json') { + args.json = true; + continue; + } + const value = argv[i + 1]; + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for --${key}`); + } + i++; + if (key === 'spec') + args.spec = value; + if (key === 'repo-root') + args.repoRoot = value; + if (key === 'codex-bin') + args.codexBin = value; + if (key === 'codex-args') + args.codexArgs = value.split(' ').filter(Boolean); + } + if (!args.spec) + throw new Error('Missing --spec'); + return args; +} +async function runCli() { + const args = parseArgs(process.argv.slice(2)); + const result = await runCodexCli(args); + if (args.json) { + process.stdout.write(JSON.stringify(result)); + } + else { + process.stdout.write(JSON.stringify(result, null, 2)); + } +} +if (process.argv[1] === fileURLToPath(import.meta.url)) { + runCli().catch((err) => { + process.stderr.write(String(err)); + process.exit(1); + }); +} diff --git a/tools/codex-bridge/dist/src/scope/audit.js b/tools/codex-bridge/dist/src/scope/audit.js new file mode 100644 index 0000000..8a8e6cd --- /dev/null +++ b/tools/codex-bridge/dist/src/scope/audit.js @@ -0,0 +1,32 @@ +import { minimatch } from 'minimatch'; +function anyMatch(path, globs) { + return globs.some((g) => minimatch(path, g, { dot: true })); +} +export function auditScope(args) { + const violations = []; + for (const file of args.files) { + if (!anyMatch(file, args.include)) { + violations.push({ rule: 'scope.include', file, detail: 'not included' }); + continue; + } + if (anyMatch(file, args.exclude)) { + violations.push({ rule: 'scope.exclude', file, detail: 'excluded by pattern' }); + continue; + } + if (anyMatch(file, args.forbid)) { + violations.push({ rule: 'constraints.forbid_paths', file, detail: 'forbidden by policy' }); + continue; + } + } + return { ok: violations.length === 0, files_changed: args.files, violations }; +} +export function auditConstraints(args) { + const violations = []; + if (args.filesChanged > args.maxFilesChanged) { + violations.push({ rule: 'constraints.max_files_changed', file: '', detail: 'too many files changed' }); + } + if (args.diffLines > args.maxDiffLines) { + violations.push({ rule: 'constraints.max_diff_lines', file: '', detail: 'diff too large' }); + } + return { ok: violations.length === 0, violations }; +} diff --git a/tools/codex-bridge/dist/src/spec.js b/tools/codex-bridge/dist/src/spec.js new file mode 100644 index 0000000..15eb306 --- /dev/null +++ b/tools/codex-bridge/dist/src/spec.js @@ -0,0 +1,49 @@ +import yaml from 'js-yaml'; +import { z } from 'zod'; +const CommandSchema = z.object({ + name: z.string().min(1), + run: z.string().min(1), + timeout_sec: z.number().int().positive() +}); +export const TaskSpecSchema = z.object({ + task_id: z.string().min(1), + repo_root: z.string().min(1), + goal: z.string().min(1), + context: z + .object({ + branch: z.string().optional(), + last_result_id: z.string().nullable().optional(), + hints: z + .object({ + failing_files: z.array(z.string()).optional(), + last_error_summary: z.string().nullable().optional() + }) + .optional() + }) + .optional(), + scope: z.object({ + include: z.array(z.string()).min(1), + exclude: z.array(z.string()).default([]) + }), + constraints: z.object({ + max_files_changed: z.number().int().positive(), + max_diff_lines: z.number().int().positive(), + forbid_paths: z.array(z.string()).default([]), + require_no_new_binary_files: z.boolean().default(true) + }), + acceptance: z.object({ + must_pass: z.boolean(), + commands: z.array(CommandSchema).min(1) + }), + codex: z.object({ + mode: z.literal('patch_only'), + prompt_contract: z.string().min(1) + }), + outputs: z.object({ + capture: z.array(z.string()).default([]) + }) +}); +export function parseTaskSpecYaml(text) { + const raw = yaml.load(text); + return TaskSpecSchema.parse(raw); +} diff --git a/tools/codex-bridge/dist/src/vcs/git.js b/tools/codex-bridge/dist/src/vcs/git.js new file mode 100644 index 0000000..aa62836 --- /dev/null +++ b/tools/codex-bridge/dist/src/vcs/git.js @@ -0,0 +1,15 @@ +import { execa } from 'execa'; +export async function gitStatusPorcelain(repoRoot) { + const r = await execa('git', ['status', '--porcelain'], { cwd: repoRoot }); + return r.stdout; +} +export async function gitDiff(repoRoot) { + const r = await execa('git', ['diff'], { cwd: repoRoot }); + return r.stdout; +} +export async function gitApplyCheck(repoRoot, patch) { + await execa('git', ['apply', '--check', '-'], { cwd: repoRoot, input: patch }); +} +export async function gitApply(repoRoot, patch) { + await execa('git', ['apply', '-'], { cwd: repoRoot, input: patch }); +} diff --git a/tools/codex-bridge/dist/test/changed-files.test.js b/tools/codex-bridge/dist/test/changed-files.test.js new file mode 100644 index 0000000..cacaad3 --- /dev/null +++ b/tools/codex-bridge/dist/test/changed-files.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { changedFilesFromUnifiedDiff } from '../src/patch/changed-files.js'; +describe('changedFilesFromUnifiedDiff', () => { + it('extracts b/ paths', () => { + const diff = [ + '--- a/src/a.ts', + '+++ b/src/a.ts', + '@@', + '+x' + ].join('\n'); + expect(changedFilesFromUnifiedDiff(diff)).toEqual(['src/a.ts']); + }); +}); diff --git a/tools/codex-bridge/dist/test/codex-run.test.js b/tools/codex-bridge/dist/test/codex-run.test.js new file mode 100644 index 0000000..f311210 --- /dev/null +++ b/tools/codex-bridge/dist/test/codex-run.test.js @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtemp, writeFile, chmod } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { runCodex } from '../src/codex/run.js'; +describe('runCodex', () => { + it('runs a stub codex binary', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'codex-')); + const scriptPath = path.join(dir, 'codex-stub.sh'); + const script = [ + '#!/usr/bin/env bash', + 'cat <<\'EOF\'', + '```diff', + '--- a/a.txt', + '+++ b/a.txt', + '@@', + '+hi', + '```', + 'EOF' + ].join('\n'); + await writeFile(scriptPath, script, 'utf8'); + await chmod(scriptPath, 0o755); + const r = await runCodex({ + cwd: dir, + codexBin: scriptPath, + codexArgs: [], + prompt: 'ignored', + timeoutMs: 2000 + }); + expect(r.exitCode).toBe(0); + expect(r.stdout).toContain('```diff'); + }); +}); diff --git a/tools/codex-bridge/dist/test/constraints.test.js b/tools/codex-bridge/dist/test/constraints.test.js new file mode 100644 index 0000000..71a382b --- /dev/null +++ b/tools/codex-bridge/dist/test/constraints.test.js @@ -0,0 +1,8 @@ +import { describe, it, expect } from 'vitest'; +import { auditConstraints } from '../src/scope/audit.js'; +describe('auditConstraints', () => { + it('rejects too many files', () => { + const r = auditConstraints({ filesChanged: 3, maxFilesChanged: 2, diffLines: 1, maxDiffLines: 10 }); + expect(r.ok).toBe(false); + }); +}); diff --git a/tools/codex-bridge/dist/test/e2e.test.js b/tools/codex-bridge/dist/test/e2e.test.js new file mode 100644 index 0000000..a581038 --- /dev/null +++ b/tools/codex-bridge/dist/test/e2e.test.js @@ -0,0 +1,69 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtemp, writeFile, chmod } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execa } from 'execa'; +describe('run_codex_cli', () => { + it('runs end-to-end with stub codex', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'bridge-e2e-')); + await execa('git', ['init'], { cwd: dir }); + await execa('git', ['config', 'user.email', 'test@example.com'], { cwd: dir }); + await execa('git', ['config', 'user.name', 'Test'], { cwd: dir }); + const filePath = path.join(dir, 'a.txt'); + await writeFile(filePath, 'hello\n'); + await execa('git', ['add', '.'], { cwd: dir }); + await execa('git', ['commit', '-m', 'init'], { cwd: dir }); + const codexPath = path.join(dir, 'codex-stub.sh'); + const codexScript = [ + '#!/usr/bin/env bash', + 'cat <<\'EOF\'', + '```diff', + 'diff --git a/a.txt b/a.txt', + '--- a/a.txt', + '+++ b/a.txt', + '@@ -1 +1,2 @@', + ' hello', + '+world', + '```', + 'EOF' + ].join('\n'); + await writeFile(codexPath, codexScript, 'utf8'); + await chmod(codexPath, 0o755); + const specPath = path.join(dir, 'task-spec.yaml'); + await writeFile(specPath, [ + 'task_id: T-1', + 'repo_root: .', + 'goal: add world', + 'scope:', + ' include: ["**"]', + ' exclude: []', + 'constraints:', + ' max_files_changed: 5', + ' max_diff_lines: 200', + ' forbid_paths: []', + ' require_no_new_binary_files: true', + 'acceptance:', + ' must_pass: true', + ' commands:', + ' - name: test', + ' run: "node -v"', + ' timeout_sec: 5', + 'codex:', + ' mode: patch_only', + ' prompt_contract: "Output only diff"', + 'outputs:', + ' capture: [patch, git_diff, git_status, command_logs, scope_report]' + ].join('\n'), 'utf8'); + const testDir = path.dirname(fileURLToPath(import.meta.url)); + const packageRoot = path.resolve(testDir, '..'); + const cliPath = path.join(packageRoot, 'dist', 'run-codex-cli.js'); + const result = await execa('node', [cliPath, '--spec', specPath, '--json', '--codex-bin', codexPath], { + cwd: dir + }); + const parsed = JSON.parse(result.stdout); + expect(parsed.status).toBe('pass'); + expect(parsed.scope_report.ok).toBe(true); + expect(parsed.vcs.git_diff).toContain('+world'); + }); +}); diff --git a/tools/codex-bridge/dist/test/exec-run.test.js b/tools/codex-bridge/dist/test/exec-run.test.js new file mode 100644 index 0000000..b8e277a --- /dev/null +++ b/tools/codex-bridge/dist/test/exec-run.test.js @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { runCommand } from '../src/exec/run.js'; +describe('runCommand', () => { + it('captures stdout', async () => { + const r = await runCommand({ + cwd: '.', + command: 'node -e "console.log(123)"', + timeoutMs: 2000, + maxOutputBytes: 10_000 + }); + expect(r.exitCode).toBe(0); + expect(r.stdout).toContain('123'); + }); +}); diff --git a/tools/codex-bridge/dist/test/git-apply.test.js b/tools/codex-bridge/dist/test/git-apply.test.js new file mode 100644 index 0000000..a56e197 --- /dev/null +++ b/tools/codex-bridge/dist/test/git-apply.test.js @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { execa } from 'execa'; +import { gitApplyCheck, gitApply } from '../src/vcs/git.js'; +describe('git apply', () => { + it('applies a patch', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'bridge-')); + await execa('git', ['init'], { cwd: dir }); + await writeFile(path.join(dir, 'a.txt'), 'hello\n'); + await execa('git', ['add', '.'], { cwd: dir }); + await execa('git', ['commit', '-m', 'init'], { cwd: dir }); + await writeFile(path.join(dir, 'a.txt'), 'hello\nworld\n'); + const patch = (await execa('git', ['diff'], { cwd: dir })).stdout + '\n'; + await execa('git', ['checkout', '--', 'a.txt'], { cwd: dir }); + await gitApplyCheck(dir, patch); + await gitApply(dir, patch); + const out = await execa('git', ['diff'], { cwd: dir }); + expect(out.stdout).toContain('+world'); + }); +}); diff --git a/tools/codex-bridge/dist/test/patch-extract.test.js b/tools/codex-bridge/dist/test/patch-extract.test.js new file mode 100644 index 0000000..58171f4 --- /dev/null +++ b/tools/codex-bridge/dist/test/patch-extract.test.js @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; +import { extractSingleDiffBlock } from '../src/patch/extract.js'; +describe('extractSingleDiffBlock', () => { + it('extracts one diff block', () => { + const out = "```diff\n--- a/a.txt\n+++ b/a.txt\n@@\n+hi\n```\n"; + expect(extractSingleDiffBlock(out)).toContain('--- a/a.txt'); + }); + it('rejects missing block', () => { + expect(() => extractSingleDiffBlock('nope')).toThrow(/diff block/i); + }); + it('rejects extra prose outside the block', () => { + const out = "```diff\n--- a/a.txt\n+++ b/a.txt\n```\nextra"; + expect(() => extractSingleDiffBlock(out)).toThrow(/outside/i); + }); +}); diff --git a/tools/codex-bridge/dist/test/scope-audit.test.js b/tools/codex-bridge/dist/test/scope-audit.test.js new file mode 100644 index 0000000..b773659 --- /dev/null +++ b/tools/codex-bridge/dist/test/scope-audit.test.js @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { auditScope } from '../src/scope/audit.js'; +describe('auditScope', () => { + it('rejects excluded files', () => { + const r = auditScope({ + files: ['dist/a.js'], + include: ['**'], + exclude: ['dist/**'], + forbid: [] + }); + expect(r.ok).toBe(false); + expect(r.violations[0].rule).toMatch(/exclude/); + }); +}); diff --git a/tools/codex-bridge/dist/test/spec.test.js b/tools/codex-bridge/dist/test/spec.test.js new file mode 100644 index 0000000..7125b2b --- /dev/null +++ b/tools/codex-bridge/dist/test/spec.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import { parseTaskSpecYaml } from '../src/spec.js'; +describe('TaskSpec', () => { + it('rejects missing task_id', () => { + expect(() => parseTaskSpecYaml('goal: x')).toThrow(/task_id/i); + }); + it('parses minimal valid spec', () => { + const spec = parseTaskSpecYaml(` +task_id: T-1 +repo_root: . +goal: hello +scope: + include: ["src/**"] + exclude: [] +constraints: + max_files_changed: 5 + max_diff_lines: 200 + forbid_paths: [] + require_no_new_binary_files: true +acceptance: + must_pass: true + commands: + - name: test + run: "node -v" + timeout_sec: 5 +codex: + mode: patch_only + prompt_contract: "Output only diff" +outputs: + capture: [patch] +`); + expect(spec.task_id).toBe('T-1'); + }); +}); diff --git a/tools/codex-bridge/dist/vcs/git.js b/tools/codex-bridge/dist/vcs/git.js new file mode 100644 index 0000000..aa62836 --- /dev/null +++ b/tools/codex-bridge/dist/vcs/git.js @@ -0,0 +1,15 @@ +import { execa } from 'execa'; +export async function gitStatusPorcelain(repoRoot) { + const r = await execa('git', ['status', '--porcelain'], { cwd: repoRoot }); + return r.stdout; +} +export async function gitDiff(repoRoot) { + const r = await execa('git', ['diff'], { cwd: repoRoot }); + return r.stdout; +} +export async function gitApplyCheck(repoRoot, patch) { + await execa('git', ['apply', '--check', '-'], { cwd: repoRoot, input: patch }); +} +export async function gitApply(repoRoot, patch) { + await execa('git', ['apply', '-'], { cwd: repoRoot, input: patch }); +} diff --git a/tools/codex-bridge/examples/task-spec.yaml b/tools/codex-bridge/examples/task-spec.yaml new file mode 100644 index 0000000..3ff463e --- /dev/null +++ b/tools/codex-bridge/examples/task-spec.yaml @@ -0,0 +1,29 @@ +task_id: T-EXAMPLE-001 +repo_root: . +goal: "Apply a minimal patch" + +scope: + include: ["**"] + exclude: ["**/dist/**", "**/node_modules/**"] + +constraints: + max_files_changed: 5 + max_diff_lines: 200 + forbid_paths: [".git/**", "**/.env*"] + require_no_new_binary_files: true + +acceptance: + must_pass: true + commands: + - name: test + run: "node -v" + timeout_sec: 5 + +codex: + mode: patch_only + prompt_contract: | + Output ONLY a unified diff in a single fenced block: ```diff ... ```. + Do not include any prose outside the diff block. + +outputs: + capture: [patch, command_logs, git_status, git_diff, scope_report] diff --git a/tools/codex-bridge/package-lock.json b/tools/codex-bridge/package-lock.json new file mode 100644 index 0000000..a8521c6 --- /dev/null +++ b/tools/codex-bridge/package-lock.json @@ -0,0 +1,1864 @@ +{ + "name": "codex-bridge", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codex-bridge", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "execa": "^9.6.1", + "js-yaml": "^4.1.1", + "minimatch": "^10.2.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^25.2.3", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", + "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", + "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz", + "integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/tools/codex-bridge/src/codex/run.ts b/tools/codex-bridge/src/codex/run.ts new file mode 100644 index 0000000..2f5b8a8 --- /dev/null +++ b/tools/codex-bridge/src/codex/run.ts @@ -0,0 +1,16 @@ +import { execa } from 'execa' + +export async function runCodex(args: { + cwd: string + codexBin: string + codexArgs: string[] + prompt: string + timeoutMs: number +}) { + const r = await execa(args.codexBin, args.codexArgs, { + cwd: args.cwd, + input: args.prompt, + timeout: args.timeoutMs + }) + return { stdout: r.stdout ?? '', stderr: r.stderr ?? '', exitCode: r.exitCode ?? 0 } +} diff --git a/tools/codex-bridge/src/exec/run.ts b/tools/codex-bridge/src/exec/run.ts new file mode 100644 index 0000000..7ea593f --- /dev/null +++ b/tools/codex-bridge/src/exec/run.ts @@ -0,0 +1,38 @@ +import { execa } from 'execa' + +export async function runCommand(args: { + cwd: string + command: string + timeoutMs: number + maxOutputBytes: number +}) { + const start = Date.now() + try { + const r = await execa(args.command, { cwd: args.cwd, shell: true, timeout: args.timeoutMs }) + return { + ok: true, + exitCode: r.exitCode ?? 0, + durationMs: Date.now() - start, + timedOut: false, + stdout: truncate(r.stdout ?? '', args.maxOutputBytes), + stderr: truncate(r.stderr ?? '', args.maxOutputBytes) + } + } catch (e: any) { + const stdout = e?.stdout ?? '' + const stderr = e?.stderr ?? String(e) + return { + ok: false, + exitCode: e?.exitCode ?? 1, + durationMs: Date.now() - start, + timedOut: Boolean(e?.timedOut), + stdout: truncate(stdout, args.maxOutputBytes), + stderr: truncate(stderr, args.maxOutputBytes) + } + } +} + +function truncate(s: string, maxBytes: number) { + const b = Buffer.from(s, 'utf8') + if (b.byteLength <= maxBytes) return s + return b.subarray(0, maxBytes).toString('utf8') + '\n[truncated]' +} diff --git a/tools/codex-bridge/src/index.ts b/tools/codex-bridge/src/index.ts new file mode 100644 index 0000000..42e3e7e --- /dev/null +++ b/tools/codex-bridge/src/index.ts @@ -0,0 +1 @@ +export { runCodexCli } from './run-codex-cli.js' diff --git a/tools/codex-bridge/src/patch/changed-files.ts b/tools/codex-bridge/src/patch/changed-files.ts new file mode 100644 index 0000000..4badad9 --- /dev/null +++ b/tools/codex-bridge/src/patch/changed-files.ts @@ -0,0 +1,10 @@ +export function changedFilesFromUnifiedDiff(diff: string): string[] { + const files = new Set() + for (const line of diff.split('\n')) { + if (line.startsWith('+++ b/')) { + const p = line.slice('+++ b/'.length).trim() + if (p !== '/dev/null') files.add(p) + } + } + return [...files] +} diff --git a/tools/codex-bridge/src/patch/extract.ts b/tools/codex-bridge/src/patch/extract.ts new file mode 100644 index 0000000..7eaf783 --- /dev/null +++ b/tools/codex-bridge/src/patch/extract.ts @@ -0,0 +1,9 @@ +export function extractSingleDiffBlock(stdout: string): string { + const re = /```diff\n([\s\S]*?)\n```/g + const matches = [...stdout.matchAll(re)] + if (matches.length !== 1) throw new Error('Expected exactly one ```diff block') + const [full, body] = matches[0] + const outside = stdout.replace(full, '').trim() + if (outside.length) throw new Error('Unexpected text outside diff block') + return body +} diff --git a/tools/codex-bridge/src/run-codex-cli.ts b/tools/codex-bridge/src/run-codex-cli.ts new file mode 100644 index 0000000..9614117 --- /dev/null +++ b/tools/codex-bridge/src/run-codex-cli.ts @@ -0,0 +1,274 @@ +import { readFile } from 'node:fs/promises' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { parseTaskSpecYaml } from './spec.js' +import { runCodex } from './codex/run.js' +import { extractSingleDiffBlock } from './patch/extract.js' +import { changedFilesFromUnifiedDiff } from './patch/changed-files.js' +import { auditScope, auditConstraints, ScopeViolation } from './scope/audit.js' +import { gitApply, gitApplyCheck, gitDiff, gitStatusPorcelain } from './vcs/git.js' +import { runCommand } from './exec/run.js' + +type Result = { + task_id: string + result_id: string + status: 'pass' | 'fail' + patch: string + scope_report: { ok: boolean; files_changed: string[]; violations: ScopeViolation[] } + apply: { ok: boolean; stderr: string } + commands: Array<{ + name: string + ok: boolean + exit_code: number + duration_ms: number + stdout: string + stderr: string + timed_out: boolean + }> + vcs: { git_status: string; git_diff: string } + notes: string + error_kind?: string + error_message?: string +} + +type CliArgs = { + spec: string + repoRoot?: string + codexBin?: string + codexArgs?: string[] + json?: boolean +} + +export async function runCodexCli(args: CliArgs): Promise { + const specText = await readFile(args.spec, 'utf8') + const spec = parseTaskSpecYaml(specText) + const repoRoot = path.resolve(process.cwd(), args.repoRoot ?? spec.repo_root) + const codexBin = args.codexBin ?? 'openai' + const codexArgs = args.codexArgs ?? ['codex'] + + const resultBase: Result = { + task_id: spec.task_id, + result_id: `R-${Date.now()}`, + status: 'fail', + patch: '', + scope_report: { ok: true, files_changed: [], violations: [] }, + apply: { ok: false, stderr: '' }, + commands: [], + vcs: { git_status: '', git_diff: '' }, + notes: '' + } + + const prompt = buildPrompt(spec) + + let rawStdout = '' + try { + const codexResult = await runCodex({ + cwd: repoRoot, + codexBin, + codexArgs, + prompt, + timeoutMs: 30 * 60 * 1000 + }) + rawStdout = codexResult.stdout + } catch (e: any) { + return await finalizeResult({ + ...resultBase, + error_kind: 'environment_failed', + error_message: e?.message ?? String(e) + }, repoRoot) + } + + let patch = '' + try { + patch = extractSingleDiffBlock(rawStdout) + if (!patch.endsWith('\n')) patch += '\n' + } catch (e: any) { + return await finalizeResult({ + ...resultBase, + error_kind: 'patch_contract_violation', + error_message: e?.message ?? String(e) + }, repoRoot) + } + + const filesChanged = changedFilesFromUnifiedDiff(patch) + const scopeReport = auditScope({ + files: filesChanged, + include: spec.scope.include, + exclude: spec.scope.exclude, + forbid: spec.constraints.forbid_paths + }) + + const constraintReport = auditConstraints({ + filesChanged: filesChanged.length, + maxFilesChanged: spec.constraints.max_files_changed, + diffLines: patch.split('\n').length, + maxDiffLines: spec.constraints.max_diff_lines + }) + + const binaryViolations = detectBinaryViolations(patch, spec.constraints.require_no_new_binary_files) + const violations = [...scopeReport.violations, ...constraintReport.violations, ...binaryViolations] + const scopeReportMerged = { + ok: violations.length === 0, + files_changed: filesChanged, + violations + } + + if (!scopeReportMerged.ok) { + return await finalizeResult( + { + ...resultBase, + patch, + scope_report: scopeReportMerged, + error_kind: 'scope_violation', + error_message: 'Patch violates scope or constraints' + }, + repoRoot + ) + } + + let applyOk = true + let applyErr = '' + try { + await gitApplyCheck(repoRoot, patch) + await gitApply(repoRoot, patch) + } catch (e: any) { + applyOk = false + applyErr = e?.stderr ?? e?.message ?? String(e) + } + + if (!applyOk) { + return await finalizeResult( + { + ...resultBase, + patch, + scope_report: scopeReportMerged, + apply: { ok: false, stderr: applyErr }, + error_kind: 'patch_apply_failed', + error_message: applyErr + }, + repoRoot + ) + } + + const commands = [] as Result['commands'] + for (const cmd of spec.acceptance.commands) { + const res = await runCommand({ + cwd: repoRoot, + command: cmd.run, + timeoutMs: cmd.timeout_sec * 1000, + maxOutputBytes: 50_000 + }) + commands.push({ + name: cmd.name, + ok: res.ok, + exit_code: res.exitCode, + duration_ms: res.durationMs, + stdout: res.stdout, + stderr: res.stderr, + timed_out: res.timedOut + }) + } + + const commandsOk = spec.acceptance.must_pass ? commands.every((c) => c.ok) : true + const status: Result['status'] = scopeReportMerged.ok && applyOk && commandsOk ? 'pass' : 'fail' + + return await finalizeResult( + { + ...resultBase, + status, + patch, + scope_report: scopeReportMerged, + apply: { ok: true, stderr: '' }, + commands, + error_kind: commandsOk ? undefined : 'acceptance_failed', + error_message: commandsOk ? undefined : 'Acceptance commands failed' + }, + repoRoot + ) +} + +async function finalizeResult(result: Result, repoRoot: string): Promise { + try { + result.vcs.git_status = await gitStatusPorcelain(repoRoot) + result.vcs.git_diff = await gitDiff(repoRoot) + } catch (e: any) { + result.vcs.git_status = result.vcs.git_status || '' + result.vcs.git_diff = result.vcs.git_diff || '' + result.error_kind = result.error_kind ?? 'environment_failed' + result.error_message = result.error_message ?? (e?.message ?? String(e)) + } + return result +} + +function detectBinaryViolations(patch: string, requireNoBinary: boolean): ScopeViolation[] { + if (!requireNoBinary) return [] + const violations: ScopeViolation[] = [] + if (/^GIT binary patch/m.test(patch) || /^Binary files /m.test(patch)) { + violations.push({ rule: 'constraints.require_no_new_binary_files', file: '', detail: 'binary patch detected' }) + } + return violations +} + +function buildPrompt(spec: ReturnType) { + const lines = [ + `Goal: ${spec.goal}`, + '', + 'Scope:', + `- Include: ${spec.scope.include.join(', ')}`, + `- Exclude: ${spec.scope.exclude.join(', ')}`, + '', + 'Constraints:', + `- max_files_changed: ${spec.constraints.max_files_changed}`, + `- max_diff_lines: ${spec.constraints.max_diff_lines}`, + `- forbid_paths: ${spec.constraints.forbid_paths.join(', ')}`, + `- require_no_new_binary_files: ${spec.constraints.require_no_new_binary_files}`, + '', + 'Acceptance Commands (read-only):', + ...spec.acceptance.commands.map((c) => `- ${c.name}: ${c.run}`), + '', + 'Output Contract:', + spec.codex.prompt_contract + ] + return lines.join('\n') +} + +function parseArgs(argv: string[]): CliArgs { + const args: CliArgs = { spec: '' } + for (let i = 0; i < argv.length; i++) { + const token = argv[i] + if (!token.startsWith('--')) continue + const key = token.slice(2) + if (key === 'json') { + args.json = true + continue + } + const value = argv[i + 1] + if (!value || value.startsWith('--')) { + throw new Error(`Missing value for --${key}`) + } + i++ + if (key === 'spec') args.spec = value + if (key === 'repo-root') args.repoRoot = value + if (key === 'codex-bin') args.codexBin = value + if (key === 'codex-args') args.codexArgs = value.split(' ').filter(Boolean) + } + if (!args.spec) throw new Error('Missing --spec') + return args +} + +async function runCli() { + const args = parseArgs(process.argv.slice(2)) + const result = await runCodexCli(args) + if (args.json) { + process.stdout.write(JSON.stringify(result)) + } else { + process.stdout.write(JSON.stringify(result, null, 2)) + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + runCli().catch((err) => { + process.stderr.write(String(err)) + process.exit(1) + }) +} diff --git a/tools/codex-bridge/src/scope/audit.ts b/tools/codex-bridge/src/scope/audit.ts new file mode 100644 index 0000000..f5b9a52 --- /dev/null +++ b/tools/codex-bridge/src/scope/audit.ts @@ -0,0 +1,50 @@ +import { minimatch } from 'minimatch' + +export type ScopeViolation = { rule: string; file: string; detail: string } +export type ScopeReport = { ok: boolean; files_changed: string[]; violations: ScopeViolation[] } + +function anyMatch(path: string, globs: string[]) { + return globs.some((g) => minimatch(path, g, { dot: true })) +} + +export function auditScope(args: { + files: string[] + include: string[] + exclude: string[] + forbid: string[] +}): ScopeReport { + const violations: ScopeViolation[] = [] + + for (const file of args.files) { + if (!anyMatch(file, args.include)) { + violations.push({ rule: 'scope.include', file, detail: 'not included' }) + continue + } + if (anyMatch(file, args.exclude)) { + violations.push({ rule: 'scope.exclude', file, detail: 'excluded by pattern' }) + continue + } + if (anyMatch(file, args.forbid)) { + violations.push({ rule: 'constraints.forbid_paths', file, detail: 'forbidden by policy' }) + continue + } + } + + return { ok: violations.length === 0, files_changed: args.files, violations } +} + +export function auditConstraints(args: { + filesChanged: number + maxFilesChanged: number + diffLines: number + maxDiffLines: number +}) { + const violations: ScopeViolation[] = [] + if (args.filesChanged > args.maxFilesChanged) { + violations.push({ rule: 'constraints.max_files_changed', file: '', detail: 'too many files changed' }) + } + if (args.diffLines > args.maxDiffLines) { + violations.push({ rule: 'constraints.max_diff_lines', file: '', detail: 'diff too large' }) + } + return { ok: violations.length === 0, violations } +} diff --git a/tools/codex-bridge/src/spec.ts b/tools/codex-bridge/src/spec.ts new file mode 100644 index 0000000..d88ea24 --- /dev/null +++ b/tools/codex-bridge/src/spec.ts @@ -0,0 +1,54 @@ +import yaml from 'js-yaml' +import { z } from 'zod' + +const CommandSchema = z.object({ + name: z.string().min(1), + run: z.string().min(1), + timeout_sec: z.number().int().positive() +}) + +export const TaskSpecSchema = z.object({ + task_id: z.string().min(1), + repo_root: z.string().min(1), + goal: z.string().min(1), + context: z + .object({ + branch: z.string().optional(), + last_result_id: z.string().nullable().optional(), + hints: z + .object({ + failing_files: z.array(z.string()).optional(), + last_error_summary: z.string().nullable().optional() + }) + .optional() + }) + .optional(), + scope: z.object({ + include: z.array(z.string()).min(1), + exclude: z.array(z.string()).default([]) + }), + constraints: z.object({ + max_files_changed: z.number().int().positive(), + max_diff_lines: z.number().int().positive(), + forbid_paths: z.array(z.string()).default([]), + require_no_new_binary_files: z.boolean().default(true) + }), + acceptance: z.object({ + must_pass: z.boolean(), + commands: z.array(CommandSchema).min(1) + }), + codex: z.object({ + mode: z.literal('patch_only'), + prompt_contract: z.string().min(1) + }), + outputs: z.object({ + capture: z.array(z.string()).default([]) + }) +}) + +export type TaskSpec = z.infer + +export function parseTaskSpecYaml(text: string): TaskSpec { + const raw = yaml.load(text) + return TaskSpecSchema.parse(raw) +} diff --git a/tools/codex-bridge/src/vcs/git.ts b/tools/codex-bridge/src/vcs/git.ts new file mode 100644 index 0000000..dfa46f2 --- /dev/null +++ b/tools/codex-bridge/src/vcs/git.ts @@ -0,0 +1,19 @@ +import { execa } from 'execa' + +export async function gitStatusPorcelain(repoRoot: string) { + const r = await execa('git', ['status', '--porcelain'], { cwd: repoRoot }) + return r.stdout +} + +export async function gitDiff(repoRoot: string) { + const r = await execa('git', ['diff'], { cwd: repoRoot }) + return r.stdout +} + +export async function gitApplyCheck(repoRoot: string, patch: string) { + await execa('git', ['apply', '--check', '-'], { cwd: repoRoot, input: patch }) +} + +export async function gitApply(repoRoot: string, patch: string) { + await execa('git', ['apply', '-'], { cwd: repoRoot, input: patch }) +} diff --git a/tools/codex-bridge/test/changed-files.test.js b/tools/codex-bridge/test/changed-files.test.js new file mode 100644 index 0000000..cacaad3 --- /dev/null +++ b/tools/codex-bridge/test/changed-files.test.js @@ -0,0 +1,13 @@ +import { describe, it, expect } from 'vitest'; +import { changedFilesFromUnifiedDiff } from '../src/patch/changed-files.js'; +describe('changedFilesFromUnifiedDiff', () => { + it('extracts b/ paths', () => { + const diff = [ + '--- a/src/a.ts', + '+++ b/src/a.ts', + '@@', + '+x' + ].join('\n'); + expect(changedFilesFromUnifiedDiff(diff)).toEqual(['src/a.ts']); + }); +}); diff --git a/tools/codex-bridge/test/changed-files.test.ts b/tools/codex-bridge/test/changed-files.test.ts new file mode 100644 index 0000000..cd4cae1 --- /dev/null +++ b/tools/codex-bridge/test/changed-files.test.ts @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest' +import { changedFilesFromUnifiedDiff } from '../src/patch/changed-files.js' + +describe('changedFilesFromUnifiedDiff', () => { + it('extracts b/ paths', () => { + const diff = [ + '--- a/src/a.ts', + '+++ b/src/a.ts', + '@@', + '+x' + ].join('\n') + expect(changedFilesFromUnifiedDiff(diff)).toEqual(['src/a.ts']) + }) +}) diff --git a/tools/codex-bridge/test/codex-run.test.js b/tools/codex-bridge/test/codex-run.test.js new file mode 100644 index 0000000..f311210 --- /dev/null +++ b/tools/codex-bridge/test/codex-run.test.js @@ -0,0 +1,33 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtemp, writeFile, chmod } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { runCodex } from '../src/codex/run.js'; +describe('runCodex', () => { + it('runs a stub codex binary', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'codex-')); + const scriptPath = path.join(dir, 'codex-stub.sh'); + const script = [ + '#!/usr/bin/env bash', + 'cat <<\'EOF\'', + '```diff', + '--- a/a.txt', + '+++ b/a.txt', + '@@', + '+hi', + '```', + 'EOF' + ].join('\n'); + await writeFile(scriptPath, script, 'utf8'); + await chmod(scriptPath, 0o755); + const r = await runCodex({ + cwd: dir, + codexBin: scriptPath, + codexArgs: [], + prompt: 'ignored', + timeoutMs: 2000 + }); + expect(r.exitCode).toBe(0); + expect(r.stdout).toContain('```diff'); + }); +}); diff --git a/tools/codex-bridge/test/codex-run.test.ts b/tools/codex-bridge/test/codex-run.test.ts new file mode 100644 index 0000000..29935ca --- /dev/null +++ b/tools/codex-bridge/test/codex-run.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest' +import { mkdtemp, writeFile, chmod } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { runCodex } from '../src/codex/run.js' + +describe('runCodex', () => { + it('runs a stub codex binary', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'codex-')) + const scriptPath = path.join(dir, 'codex-stub.sh') + const script = [ + '#!/usr/bin/env bash', + 'cat <<\'EOF\'', + '```diff', + '--- a/a.txt', + '+++ b/a.txt', + '@@', + '+hi', + '```', + 'EOF' + ].join('\n') + await writeFile(scriptPath, script, 'utf8') + await chmod(scriptPath, 0o755) + + const r = await runCodex({ + cwd: dir, + codexBin: scriptPath, + codexArgs: [], + prompt: 'ignored', + timeoutMs: 2000 + }) + + expect(r.exitCode).toBe(0) + expect(r.stdout).toContain('```diff') + }) +}) diff --git a/tools/codex-bridge/test/constraints.test.js b/tools/codex-bridge/test/constraints.test.js new file mode 100644 index 0000000..71a382b --- /dev/null +++ b/tools/codex-bridge/test/constraints.test.js @@ -0,0 +1,8 @@ +import { describe, it, expect } from 'vitest'; +import { auditConstraints } from '../src/scope/audit.js'; +describe('auditConstraints', () => { + it('rejects too many files', () => { + const r = auditConstraints({ filesChanged: 3, maxFilesChanged: 2, diffLines: 1, maxDiffLines: 10 }); + expect(r.ok).toBe(false); + }); +}); diff --git a/tools/codex-bridge/test/constraints.test.ts b/tools/codex-bridge/test/constraints.test.ts new file mode 100644 index 0000000..2e095bb --- /dev/null +++ b/tools/codex-bridge/test/constraints.test.ts @@ -0,0 +1,9 @@ +import { describe, it, expect } from 'vitest' +import { auditConstraints } from '../src/scope/audit.js' + +describe('auditConstraints', () => { + it('rejects too many files', () => { + const r = auditConstraints({ filesChanged: 3, maxFilesChanged: 2, diffLines: 1, maxDiffLines: 10 }) + expect(r.ok).toBe(false) + }) +}) diff --git a/tools/codex-bridge/test/e2e.test.js b/tools/codex-bridge/test/e2e.test.js new file mode 100644 index 0000000..b3c83a5 --- /dev/null +++ b/tools/codex-bridge/test/e2e.test.js @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtemp, writeFile, chmod } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { execa } from 'execa'; +describe('run_codex_cli', () => { + it('runs end-to-end with stub codex', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'bridge-e2e-')); + await execa('git', ['init'], { cwd: dir }); + await execa('git', ['config', 'user.email', 'test@example.com'], { cwd: dir }); + await execa('git', ['config', 'user.name', 'Test'], { cwd: dir }); + const filePath = path.join(dir, 'a.txt'); + await writeFile(filePath, 'hello\n'); + await execa('git', ['add', '.'], { cwd: dir }); + await execa('git', ['commit', '-m', 'init'], { cwd: dir }); + const codexPath = path.join(dir, 'codex-stub.sh'); + const codexScript = [ + '#!/usr/bin/env bash', + 'cat <<\'EOF\'', + '```diff', + 'diff --git a/a.txt b/a.txt', + 'index e69de29..b7f783b 100644', + '--- a/a.txt', + '+++ b/a.txt', + '@@ -1 +1,2 @@', + ' hello', + '+world', + '```', + 'EOF' + ].join('\n'); + await writeFile(codexPath, codexScript, 'utf8'); + await chmod(codexPath, 0o755); + const specPath = path.join(dir, 'task-spec.yaml'); + await writeFile(specPath, [ + 'task_id: T-1', + 'repo_root: .', + 'goal: add world', + 'scope:', + ' include: ["**"]', + ' exclude: []', + 'constraints:', + ' max_files_changed: 5', + ' max_diff_lines: 200', + ' forbid_paths: []', + ' require_no_new_binary_files: true', + 'acceptance:', + ' must_pass: true', + ' commands:', + ' - name: test', + ' run: "node -v"', + ' timeout_sec: 5', + 'codex:', + ' mode: patch_only', + ' prompt_contract: "Output only diff"', + 'outputs:', + ' capture: [patch, git_diff, git_status, command_logs, scope_report]' + ].join('\n'), 'utf8'); + const testDir = path.dirname(fileURLToPath(import.meta.url)); + const packageRoot = path.resolve(testDir, '..'); + const cliPath = path.join(packageRoot, 'dist', 'run-codex-cli.js'); + const result = await execa('node', [cliPath, '--spec', specPath, '--json', '--codex-bin', codexPath], { + cwd: dir + }); + const parsed = JSON.parse(result.stdout); + expect(parsed.status).toBe('pass'); + expect(parsed.scope_report.ok).toBe(true); + expect(parsed.vcs.git_diff).toContain('+world'); + }); +}); diff --git a/tools/codex-bridge/test/e2e.test.ts b/tools/codex-bridge/test/e2e.test.ts new file mode 100644 index 0000000..a030540 --- /dev/null +++ b/tools/codex-bridge/test/e2e.test.ts @@ -0,0 +1,79 @@ +import { describe, it, expect } from 'vitest' +import { mkdtemp, writeFile, chmod } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { execa } from 'execa' + +describe('run_codex_cli', () => { + it('runs end-to-end with stub codex', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'bridge-e2e-')) + await execa('git', ['init'], { cwd: dir }) + await execa('git', ['config', 'user.email', 'test@example.com'], { cwd: dir }) + await execa('git', ['config', 'user.name', 'Test'], { cwd: dir }) + + const filePath = path.join(dir, 'a.txt') + await writeFile(filePath, 'hello\n') + await execa('git', ['add', '.'], { cwd: dir }) + await execa('git', ['commit', '-m', 'init'], { cwd: dir }) + + const codexPath = path.join(dir, 'codex-stub.sh') + const codexScript = [ + '#!/usr/bin/env bash', + 'cat <<\'EOF\'', + '```diff', + 'diff --git a/a.txt b/a.txt', + '--- a/a.txt', + '+++ b/a.txt', + '@@ -1 +1,2 @@', + ' hello', + '+world', + '```', + 'EOF' + ].join('\n') + await writeFile(codexPath, codexScript, 'utf8') + await chmod(codexPath, 0o755) + + const specPath = path.join(dir, 'task-spec.yaml') + await writeFile( + specPath, + [ + 'task_id: T-1', + 'repo_root: .', + 'goal: add world', + 'scope:', + ' include: ["**"]', + ' exclude: []', + 'constraints:', + ' max_files_changed: 5', + ' max_diff_lines: 200', + ' forbid_paths: []', + ' require_no_new_binary_files: true', + 'acceptance:', + ' must_pass: true', + ' commands:', + ' - name: test', + ' run: "node -v"', + ' timeout_sec: 5', + 'codex:', + ' mode: patch_only', + ' prompt_contract: "Output only diff"', + 'outputs:', + ' capture: [patch, git_diff, git_status, command_logs, scope_report]' + ].join('\n'), + 'utf8' + ) + + const testDir = path.dirname(fileURLToPath(import.meta.url)) + const packageRoot = path.resolve(testDir, '..') + const cliPath = path.join(packageRoot, 'dist', 'run-codex-cli.js') + const result = await execa('node', [cliPath, '--spec', specPath, '--json', '--codex-bin', codexPath], { + cwd: dir + }) + + const parsed = JSON.parse(result.stdout) + expect(parsed.status).toBe('pass') + expect(parsed.scope_report.ok).toBe(true) + expect(parsed.vcs.git_diff).toContain('+world') + }) +}) diff --git a/tools/codex-bridge/test/exec-run.test.js b/tools/codex-bridge/test/exec-run.test.js new file mode 100644 index 0000000..b8e277a --- /dev/null +++ b/tools/codex-bridge/test/exec-run.test.js @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { runCommand } from '../src/exec/run.js'; +describe('runCommand', () => { + it('captures stdout', async () => { + const r = await runCommand({ + cwd: '.', + command: 'node -e "console.log(123)"', + timeoutMs: 2000, + maxOutputBytes: 10_000 + }); + expect(r.exitCode).toBe(0); + expect(r.stdout).toContain('123'); + }); +}); diff --git a/tools/codex-bridge/test/exec-run.test.ts b/tools/codex-bridge/test/exec-run.test.ts new file mode 100644 index 0000000..b88f829 --- /dev/null +++ b/tools/codex-bridge/test/exec-run.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest' +import { runCommand } from '../src/exec/run.js' + +describe('runCommand', () => { + it('captures stdout', async () => { + const r = await runCommand({ + cwd: '.', + command: 'node -e "console.log(123)"', + timeoutMs: 2000, + maxOutputBytes: 10_000 + }) + expect(r.exitCode).toBe(0) + expect(r.stdout).toContain('123') + }) +}) diff --git a/tools/codex-bridge/test/git-apply.test.js b/tools/codex-bridge/test/git-apply.test.js new file mode 100644 index 0000000..a56e197 --- /dev/null +++ b/tools/codex-bridge/test/git-apply.test.js @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest'; +import { mkdtemp, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { execa } from 'execa'; +import { gitApplyCheck, gitApply } from '../src/vcs/git.js'; +describe('git apply', () => { + it('applies a patch', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'bridge-')); + await execa('git', ['init'], { cwd: dir }); + await writeFile(path.join(dir, 'a.txt'), 'hello\n'); + await execa('git', ['add', '.'], { cwd: dir }); + await execa('git', ['commit', '-m', 'init'], { cwd: dir }); + await writeFile(path.join(dir, 'a.txt'), 'hello\nworld\n'); + const patch = (await execa('git', ['diff'], { cwd: dir })).stdout + '\n'; + await execa('git', ['checkout', '--', 'a.txt'], { cwd: dir }); + await gitApplyCheck(dir, patch); + await gitApply(dir, patch); + const out = await execa('git', ['diff'], { cwd: dir }); + expect(out.stdout).toContain('+world'); + }); +}); diff --git a/tools/codex-bridge/test/git-apply.test.ts b/tools/codex-bridge/test/git-apply.test.ts new file mode 100644 index 0000000..fd47cad --- /dev/null +++ b/tools/codex-bridge/test/git-apply.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest' +import { mkdtemp, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { execa } from 'execa' +import { gitApplyCheck, gitApply } from '../src/vcs/git.js' + +describe('git apply', () => { + it('applies a patch', async () => { + const dir = await mkdtemp(path.join(tmpdir(), 'bridge-')) + await execa('git', ['init'], { cwd: dir }) + await writeFile(path.join(dir, 'a.txt'), 'hello\n') + await execa('git', ['add', '.'], { cwd: dir }) + await execa('git', ['commit', '-m', 'init'], { cwd: dir }) + + await writeFile(path.join(dir, 'a.txt'), 'hello\nworld\n') + const patch = (await execa('git', ['diff'], { cwd: dir })).stdout + '\n' + await execa('git', ['checkout', '--', 'a.txt'], { cwd: dir }) + + await gitApplyCheck(dir, patch) + await gitApply(dir, patch) + const out = await execa('git', ['diff'], { cwd: dir }) + expect(out.stdout).toContain('+world') + }) +}) diff --git a/tools/codex-bridge/test/patch-extract.test.js b/tools/codex-bridge/test/patch-extract.test.js new file mode 100644 index 0000000..58171f4 --- /dev/null +++ b/tools/codex-bridge/test/patch-extract.test.js @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest'; +import { extractSingleDiffBlock } from '../src/patch/extract.js'; +describe('extractSingleDiffBlock', () => { + it('extracts one diff block', () => { + const out = "```diff\n--- a/a.txt\n+++ b/a.txt\n@@\n+hi\n```\n"; + expect(extractSingleDiffBlock(out)).toContain('--- a/a.txt'); + }); + it('rejects missing block', () => { + expect(() => extractSingleDiffBlock('nope')).toThrow(/diff block/i); + }); + it('rejects extra prose outside the block', () => { + const out = "```diff\n--- a/a.txt\n+++ b/a.txt\n```\nextra"; + expect(() => extractSingleDiffBlock(out)).toThrow(/outside/i); + }); +}); diff --git a/tools/codex-bridge/test/patch-extract.test.ts b/tools/codex-bridge/test/patch-extract.test.ts new file mode 100644 index 0000000..f22f119 --- /dev/null +++ b/tools/codex-bridge/test/patch-extract.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from 'vitest' +import { extractSingleDiffBlock } from '../src/patch/extract.js' + +describe('extractSingleDiffBlock', () => { + it('extracts one diff block', () => { + const out = "```diff\n--- a/a.txt\n+++ b/a.txt\n@@\n+hi\n```\n" + expect(extractSingleDiffBlock(out)).toContain('--- a/a.txt') + }) + + it('rejects missing block', () => { + expect(() => extractSingleDiffBlock('nope')).toThrow(/diff block/i) + }) + + it('rejects extra prose outside the block', () => { + const out = "```diff\n--- a/a.txt\n+++ b/a.txt\n```\nextra" + expect(() => extractSingleDiffBlock(out)).toThrow(/outside/i) + }) +}) diff --git a/tools/codex-bridge/test/scope-audit.test.js b/tools/codex-bridge/test/scope-audit.test.js new file mode 100644 index 0000000..b773659 --- /dev/null +++ b/tools/codex-bridge/test/scope-audit.test.js @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { auditScope } from '../src/scope/audit.js'; +describe('auditScope', () => { + it('rejects excluded files', () => { + const r = auditScope({ + files: ['dist/a.js'], + include: ['**'], + exclude: ['dist/**'], + forbid: [] + }); + expect(r.ok).toBe(false); + expect(r.violations[0].rule).toMatch(/exclude/); + }); +}); diff --git a/tools/codex-bridge/test/scope-audit.test.ts b/tools/codex-bridge/test/scope-audit.test.ts new file mode 100644 index 0000000..b7df598 --- /dev/null +++ b/tools/codex-bridge/test/scope-audit.test.ts @@ -0,0 +1,15 @@ +import { describe, it, expect } from 'vitest' +import { auditScope } from '../src/scope/audit.js' + +describe('auditScope', () => { + it('rejects excluded files', () => { + const r = auditScope({ + files: ['dist/a.js'], + include: ['**'], + exclude: ['dist/**'], + forbid: [] + }) + expect(r.ok).toBe(false) + expect(r.violations[0].rule).toMatch(/exclude/) + }) +}) diff --git a/tools/codex-bridge/test/spec.test.js b/tools/codex-bridge/test/spec.test.js new file mode 100644 index 0000000..7125b2b --- /dev/null +++ b/tools/codex-bridge/test/spec.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect } from 'vitest'; +import { parseTaskSpecYaml } from '../src/spec.js'; +describe('TaskSpec', () => { + it('rejects missing task_id', () => { + expect(() => parseTaskSpecYaml('goal: x')).toThrow(/task_id/i); + }); + it('parses minimal valid spec', () => { + const spec = parseTaskSpecYaml(` +task_id: T-1 +repo_root: . +goal: hello +scope: + include: ["src/**"] + exclude: [] +constraints: + max_files_changed: 5 + max_diff_lines: 200 + forbid_paths: [] + require_no_new_binary_files: true +acceptance: + must_pass: true + commands: + - name: test + run: "node -v" + timeout_sec: 5 +codex: + mode: patch_only + prompt_contract: "Output only diff" +outputs: + capture: [patch] +`); + expect(spec.task_id).toBe('T-1'); + }); +}); diff --git a/tools/codex-bridge/test/spec.test.ts b/tools/codex-bridge/test/spec.test.ts new file mode 100644 index 0000000..ce797da --- /dev/null +++ b/tools/codex-bridge/test/spec.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest' +import { parseTaskSpecYaml } from '../src/spec.js' + +describe('TaskSpec', () => { + it('rejects missing task_id', () => { + expect(() => parseTaskSpecYaml('goal: x')).toThrow(/task_id/i) + }) + + it('parses minimal valid spec', () => { + const spec = parseTaskSpecYaml(` +task_id: T-1 +repo_root: . +goal: hello +scope: + include: ["src/**"] + exclude: [] +constraints: + max_files_changed: 5 + max_diff_lines: 200 + forbid_paths: [] + require_no_new_binary_files: true +acceptance: + must_pass: true + commands: + - name: test + run: "node -v" + timeout_sec: 5 +codex: + mode: patch_only + prompt_contract: "Output only diff" +outputs: + capture: [patch] +`) + expect(spec.task_id).toBe('T-1') + }) +}) diff --git a/tools/codex-bridge/tsconfig.json b/tools/codex-bridge/tsconfig.json new file mode 100644 index 0000000..e1e787d --- /dev/null +++ b/tools/codex-bridge/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["test"] +} diff --git a/tools/codex-bridge/vitest.config.ts b/tools/codex-bridge/vitest.config.ts new file mode 100644 index 0000000..6345968 --- /dev/null +++ b/tools/codex-bridge/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + environment: 'node', + exclude: ['**/node_modules/**', '**/.git/**', '**/dist/**'] + } +})