Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/runbook/opencode-codex-bridge.md
Original file line number Diff line number Diff line change
@@ -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 <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.
95 changes: 95 additions & 0 deletions tools/codex-bridge/README.md
Original file line number Diff line number Diff line change
@@ -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
```
9 changes: 9 additions & 0 deletions tools/codex-bridge/dist/codex/run.js
Original file line number Diff line number Diff line change
@@ -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 };
}
33 changes: 33 additions & 0 deletions tools/codex-bridge/dist/exec/run.js
Original file line number Diff line number Diff line change
@@ -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]';
}
1 change: 1 addition & 0 deletions tools/codex-bridge/dist/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { runCodexCli } from './run-codex-cli.js';
11 changes: 11 additions & 0 deletions tools/codex-bridge/dist/patch/changed-files.js
Original file line number Diff line number Diff line change
@@ -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];
}
11 changes: 11 additions & 0 deletions tools/codex-bridge/dist/patch/extract.js
Original file line number Diff line number Diff line change
@@ -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;
}
Loading