@lyupro/codex-bridge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/LICENSE +21 -0
- package/README.md +77 -0
- package/bin/codex-bridge.mjs +105 -0
- package/cli/copy.mjs +38 -0
- package/cli/doctor.mjs +170 -0
- package/cli/hosts.mjs +50 -0
- package/cli/install.mjs +109 -0
- package/cli/manifest.mjs +210 -0
- package/cli/settings-merge.mjs +101 -0
- package/cli/stop.mjs +125 -0
- package/cli/uninstall.mjs +62 -0
- package/cli/update.mjs +156 -0
- package/package.json +48 -0
- package/scripts/check-file-size.mjs +193 -0
- package/scripts/run-tests.mjs +26 -0
- package/src/agents/codex-build.md +224 -0
- package/src/agents/codex-review.md +166 -0
- package/src/agents/codex-scout.md +188 -0
- package/src/commands/env.md +38 -0
- package/src/commands/usage.md +88 -0
- package/src/hooks/reply-guard.mjs +311 -0
- package/src/meta/chain.mjs +87 -0
- package/src/meta/environment.mjs +36 -0
- package/src/meta/paths.mjs +193 -0
- package/src/meta/reply.mjs +133 -0
- package/src/meta/run-state.mjs +197 -0
- package/src/meta/verdict.mjs +362 -0
- package/src/rules/codex-bridge.rules +173 -0
- package/src/run-codex.mjs +113 -0
- package/src/run-config.json +4 -0
- package/src/run-config.mjs +302 -0
- package/src/runner/args.mjs +111 -0
- package/src/runner/attach.mjs +111 -0
- package/src/runner/codex-cmd.mjs +265 -0
- package/src/runner/git-state.mjs +144 -0
- package/src/runner/launcher.mjs +374 -0
- package/src/runner/project-dir.mjs +92 -0
- package/src/runner/prompts.mjs +97 -0
- package/src/runner/run-context.mjs +52 -0
- package/src/runner/run-env.mjs +38 -0
- package/src/runner/runs-root.mjs +16 -0
- package/src/runner/schemas.mjs +135 -0
- package/src/runner/worker.mjs +62 -0
- package/src/write-meta.mjs +135 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lyupro/codex-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Delegate implementation, reconnaissance and code review from Claude Code to the Codex CLI: dispatcher agents, a runner that survives a dropped call, and a verdict that checks the report against the worktree.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"codex",
|
|
8
|
+
"codex-cli",
|
|
9
|
+
"subagent",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"orchestration",
|
|
12
|
+
"hooks"
|
|
13
|
+
],
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"author": "Lyu Pro <lyupro.dev@gmail.com> (https://lyupro.com)",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/lyupro/codex-bridge.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/lyupro/codex-bridge/issues"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/lyupro/codex-bridge#readme",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"codex-bridge": "./bin/codex-bridge.mjs"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24.0.0"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"bin",
|
|
33
|
+
"cli",
|
|
34
|
+
"src",
|
|
35
|
+
"scripts",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE",
|
|
38
|
+
"CHANGELOG.md"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"dev:install": "node bin/codex-bridge.mjs update --force",
|
|
42
|
+
"test": "node scripts/run-tests.mjs",
|
|
43
|
+
"check:size": "node scripts/check-file-size.mjs"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Enforces the repository's "source file <= maxLines" rule as a pre-commit gate: reads
|
|
4
|
+
* .file-size-limit.json, globs every included file, counts its lines, and reports (or
|
|
5
|
+
* blocks, in error mode) any file over the limit that is not covered by a documented
|
|
6
|
+
* exclusion.
|
|
7
|
+
*
|
|
8
|
+
* node scripts/check-file-size.mjs # honor mode from the config file
|
|
9
|
+
* node scripts/check-file-size.mjs --error # force error mode regardless of config
|
|
10
|
+
* node scripts/check-file-size.mjs --json # machine-readable output for CI
|
|
11
|
+
*
|
|
12
|
+
* Exit codes: 0 clean (or violations found but mode is "warning"), 1 violations found in
|
|
13
|
+
* error mode, 2 internal error (config missing/malformed, or an exclude entry has no
|
|
14
|
+
* matching exclusionRationale).
|
|
15
|
+
*
|
|
16
|
+
* Every exclude entry is classified as either a "pattern" (contains a `*`, sweeps a
|
|
17
|
+
* whole category — e.g. `**\/node_modules/**`) or a "specific path" (no `*`, names one
|
|
18
|
+
* file). Specific paths must carry an exclusionRationale entry; patterns are exempt. This
|
|
19
|
+
* is the structural guard against silently hiding a file in exclude without saying why.
|
|
20
|
+
*/
|
|
21
|
+
import fs from 'node:fs/promises';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
export const REPO_ROOT = path.resolve(HERE, '..');
|
|
27
|
+
export const CONFIG_PATH = path.join(REPO_ROOT, '.file-size-limit.json');
|
|
28
|
+
|
|
29
|
+
const REQUIRED_KEYS = ['maxLines', 'mode', 'include', 'exclude', 'baselineDate', 'exclusionRationale'];
|
|
30
|
+
const VALID_MODES = ['error', 'warning'];
|
|
31
|
+
|
|
32
|
+
/** Reads and structurally validates .file-size-limit.json. Throws on anything wrong. */
|
|
33
|
+
export async function loadConfig(configPath = CONFIG_PATH) {
|
|
34
|
+
let raw;
|
|
35
|
+
try {
|
|
36
|
+
raw = await fs.readFile(configPath, 'utf8');
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw new Error(`cannot read ${configPath}: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(raw);
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(`${configPath} is not valid JSON: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
47
|
+
throw new Error(`${configPath} must contain a JSON object`);
|
|
48
|
+
}
|
|
49
|
+
for (const key of REQUIRED_KEYS) {
|
|
50
|
+
if (!(key in parsed)) {
|
|
51
|
+
throw new Error(`${configPath} is missing required key "${key}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!Number.isInteger(parsed.maxLines) || parsed.maxLines <= 0) {
|
|
55
|
+
throw new Error(`${configPath}: "maxLines" must be a positive integer`);
|
|
56
|
+
}
|
|
57
|
+
if (!VALID_MODES.includes(parsed.mode)) {
|
|
58
|
+
throw new Error(`${configPath}: "mode" must be one of ${VALID_MODES.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(parsed.include) || !parsed.include.every((p) => typeof p === 'string')) {
|
|
61
|
+
throw new Error(`${configPath}: "include" must be an array of glob strings`);
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(parsed.exclude) || !parsed.exclude.every((p) => typeof p === 'string')) {
|
|
64
|
+
throw new Error(`${configPath}: "exclude" must be an array of glob strings`);
|
|
65
|
+
}
|
|
66
|
+
if (typeof parsed.exclusionRationale !== 'object' || parsed.exclusionRationale === null || Array.isArray(parsed.exclusionRationale)) {
|
|
67
|
+
throw new Error(`${configPath}: "exclusionRationale" must be an object`);
|
|
68
|
+
}
|
|
69
|
+
return parsed;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A pattern (contains `*`) sweeps a category; a specific path (no `*`) names one file. */
|
|
73
|
+
export function isSpecificExclude(entry) {
|
|
74
|
+
return !entry.includes('*');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Throws if any specific-path exclude lacks a matching exclusionRationale entry. */
|
|
78
|
+
export function validateExclusionRationale(config) {
|
|
79
|
+
const missing = config.exclude
|
|
80
|
+
.filter(isSpecificExclude)
|
|
81
|
+
.filter((entry) => !Object.prototype.hasOwnProperty.call(config.exclusionRationale, entry));
|
|
82
|
+
if (missing.length) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`.file-size-limit.json: exclude entries with no exclusionRationale: ${missing.join(', ')}. ` +
|
|
85
|
+
'Every specific-path exclusion needs a documented reason, not a silent bypass.',
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Globs every include pattern under rootDir, applying exclude, deduped and sorted. */
|
|
91
|
+
export async function collectFiles(config, rootDir = REPO_ROOT) {
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
for (const pattern of config.include) {
|
|
94
|
+
for await (const relPath of fs.glob(pattern, { cwd: rootDir, exclude: config.exclude })) {
|
|
95
|
+
seen.add(relPath.split(path.sep).join('/'));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...seen].sort();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Line count of one file, counted the same way as the rest of this repo: split('\n').length. */
|
|
102
|
+
export async function countLines(rootDir, relPath) {
|
|
103
|
+
const content = await fs.readFile(path.join(rootDir, relPath), 'utf8');
|
|
104
|
+
return content.split('\n').length;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Collects and measures every included file, returning the ones over the limit. */
|
|
108
|
+
export async function findViolators(config, rootDir = REPO_ROOT) {
|
|
109
|
+
const files = await collectFiles(config, rootDir);
|
|
110
|
+
const violators = [];
|
|
111
|
+
for (const relPath of files) {
|
|
112
|
+
const lines = await countLines(rootDir, relPath);
|
|
113
|
+
if (lines > config.maxLines) {
|
|
114
|
+
violators.push({ path: relPath, lines, limit: config.maxLines });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { filesChecked: files.length, violators };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function renderTable(violators) {
|
|
121
|
+
const pathWidth = Math.max(4, ...violators.map((v) => v.path.length));
|
|
122
|
+
const header = `${'FILE'.padEnd(pathWidth)} LINES LIMIT`;
|
|
123
|
+
const rows = violators.map(
|
|
124
|
+
(v) => `${v.path.padEnd(pathWidth)} ${String(v.lines).padStart(5)} ${String(v.limit).padStart(5)}`,
|
|
125
|
+
);
|
|
126
|
+
return [header, ...rows].join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Runs the gate end to end and returns a result instead of exiting, so both the CLI
|
|
131
|
+
* entry point and the test suite can drive the same logic.
|
|
132
|
+
*/
|
|
133
|
+
export async function run({ argv = [], rootDir = REPO_ROOT, configPath = CONFIG_PATH } = {}) {
|
|
134
|
+
const forceError = argv.includes('--error');
|
|
135
|
+
const asJson = argv.includes('--json');
|
|
136
|
+
|
|
137
|
+
let config;
|
|
138
|
+
try {
|
|
139
|
+
config = await loadConfig(configPath);
|
|
140
|
+
validateExclusionRationale(config);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return { exitCode: 2, output: `check-file-size: ${err.message}`, toStderr: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let result;
|
|
146
|
+
try {
|
|
147
|
+
result = await findViolators(config, rootDir);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return { exitCode: 2, output: `check-file-size: ${err.message}`, toStderr: true };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const effectiveMode = forceError ? 'error' : config.mode;
|
|
153
|
+
const hasViolations = result.violators.length > 0;
|
|
154
|
+
const exitCode = hasViolations && effectiveMode === 'error' ? 1 : 0;
|
|
155
|
+
|
|
156
|
+
if (asJson) {
|
|
157
|
+
const payload = {
|
|
158
|
+
maxLines: config.maxLines,
|
|
159
|
+
mode: effectiveMode,
|
|
160
|
+
filesChecked: result.filesChecked,
|
|
161
|
+
violators: result.violators,
|
|
162
|
+
};
|
|
163
|
+
return { exitCode, output: JSON.stringify(payload, null, 2), toStderr: false };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!hasViolations) {
|
|
167
|
+
return {
|
|
168
|
+
exitCode,
|
|
169
|
+
output: `check-file-size: ${result.filesChecked} file(s) checked, all within ${config.maxLines} lines`,
|
|
170
|
+
toStderr: false,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const suffix = effectiveMode === 'warning' ? ' (warning mode — not blocking)' : '';
|
|
175
|
+
const output = `${renderTable(result.violators)}\n\n${result.violators.length} file(s) exceed ${config.maxLines} lines${suffix}`;
|
|
176
|
+
return { exitCode, output, toStderr: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const invokedDirectly =
|
|
180
|
+
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
181
|
+
|
|
182
|
+
if (invokedDirectly) {
|
|
183
|
+
run({ argv: process.argv.slice(2) })
|
|
184
|
+
.then(({ exitCode, output, toStderr }) => {
|
|
185
|
+
if (toStderr) console.error(output);
|
|
186
|
+
else console.log(output);
|
|
187
|
+
process.exit(exitCode);
|
|
188
|
+
})
|
|
189
|
+
.catch((err) => {
|
|
190
|
+
console.error(`check-file-size: ${err.message}`);
|
|
191
|
+
process.exit(2);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs the test suite with a Codex home of its own.
|
|
3
|
+
*
|
|
4
|
+
* The installer writes its execpolicy file under `$CODEX_HOME/rules`, so a test that builds a host
|
|
5
|
+
* without naming a Codex home installs into the real one: on 2026-08-03 the suite dropped
|
|
6
|
+
* `codex-bridge.rules` into the operator's `~/.codex/rules/` and the next run failed on the file it
|
|
7
|
+
* had left behind. Passing the isolation through the one command that starts the suite is what
|
|
8
|
+
* makes it unforgettable — a fixture can omit it, this cannot.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
|
|
15
|
+
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-bridge-test-home-'));
|
|
16
|
+
const pattern = process.argv[2] || 'tests/**/*.test.mjs';
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = spawnSync(process.execPath, ['--test', pattern], {
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
env: { ...process.env, CODEX_HOME: codexHome },
|
|
22
|
+
});
|
|
23
|
+
process.exitCode = result.status ?? 1;
|
|
24
|
+
} finally {
|
|
25
|
+
fs.rmSync(codexHome, { recursive: true, force: true });
|
|
26
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-build
|
|
3
|
+
description: Имплементация ВНЕ подписки Claude — правки кода, новые модули, рефакторинг по образцу, починка тестов. Работу выполняет Codex CLI (подписка ChatGPT) с правом записи в репозиторий строго в пределах обязательного `scope`, Claude платит только за постановку задачи и ≤5 строк выжимки. Отчёт и полный лог кладёт в ~/.claude/codex-runs/, в чат — короткая выжимка + путь. Ревью и приёмку делает отдельный агент Claude, не этот.
|
|
4
|
+
model: haiku
|
|
5
|
+
tools: Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the build dispatcher. You do not write code yourself: one command starts Codex with write
|
|
9
|
+
access,
|
|
10
|
+
and its output is your answer.
|
|
11
|
+
|
|
12
|
+
**Why you exist.** The orchestrator runs on a Claude Max subscription, while Codex runs on a
|
|
13
|
+
ChatGPT subscription. Implementation must burn someone else's quota, not yours. Every extra line you
|
|
14
|
+
send to chat costs
|
|
15
|
+
Claude tokens. Therefore: do not read files, do not check Codex's work manually, do not retell the
|
|
16
|
+
diff, and do not reason about the task.
|
|
17
|
+
|
|
18
|
+
**You are not the acceptance agent.** A separate Claude agent checks quality in a separate pass.
|
|
19
|
+
Your
|
|
20
|
+
job is to report the run status honestly, including failure.
|
|
21
|
+
|
|
22
|
+
## What you receive as input
|
|
23
|
+
|
|
24
|
+
- The task statement: what to change and the completion criteria.
|
|
25
|
+
- The path to the repository. If none is given, use the current working directory.
|
|
26
|
+
- `scope: "<glob,glob>"` — REQUIRED. The list of paths Codex may touch, relative to the
|
|
27
|
+
repository root; the runner will not start without it. If the orchestrator did not give it, do not
|
|
28
|
+
guess:
|
|
29
|
+
start the runner without `--scope` and return its refusal exactly.
|
|
30
|
+
- `order id: <label>` — the label the orchestrator issued for this order. Pass it as `--order-id`
|
|
31
|
+
exactly as given. Never invent one, never edit one, never reuse one from another order: the
|
|
32
|
+
runner chains runs by this label, and a made-up label is how a repeat run hides. Absent means
|
|
33
|
+
the same as an absent `--scope`: start the runner without the flag and return its refusal
|
|
34
|
+
verbatim.
|
|
35
|
+
- `continue` — pass it as the `--continue` flag only if the orchestrator gave it explicitly; do not
|
|
36
|
+
add or guess it yourself. The contract is the same kind as `--scope`: if this task (the same
|
|
37
|
+
`slug` and the same repository) already had a run, the runner will refuse to start without
|
|
38
|
+
`--continue` — before
|
|
39
|
+
starting Codex, so no quota is spent. The orchestrator decides on a repeat run; your job is to
|
|
40
|
+
pass the flag if given and return the runner's refusal exactly if it is absent.
|
|
41
|
+
- Optional: `effort: <none|minimal|low|medium|high|xhigh|max>`, `slug:`, `verify: <verification
|
|
42
|
+
command>` (for example `npm test`, `tsc --noEmit`).
|
|
43
|
+
|
|
44
|
+
## The only thing you do
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node "{{CODEX_BRIDGE_DIR}}/run-codex.mjs" --agent codex-build \
|
|
48
|
+
--repo "<repository-path or .>" --scope "<glob,glob from input>" --slug "<slug>" \
|
|
49
|
+
--order-id "<order id from the orchestrator>" \
|
|
50
|
+
--verify "<verification command, if given>" <<'TASK'
|
|
51
|
+
<operator's task statement together with the completion criteria, verbatim>
|
|
52
|
+
TASK
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Add `--effort "<value>"` only when the orchestrator named a depth, and only with one of
|
|
56
|
+
`none|minimal|low|medium|high|xhigh|max`. Without the flag the configured profile of the mode
|
|
57
|
+
decides, which is the intended default — a placeholder copied from this template is refused before
|
|
58
|
+
Codex starts.
|
|
59
|
+
|
|
60
|
+
If the orchestrator gave `continue`, add the bare `--continue` flag to the command, with no value.
|
|
61
|
+
No value
|
|
62
|
+
is passed to it: the runner accepts only `1/true/yes/0/false/no`, while a placeholder string left in
|
|
63
|
+
the
|
|
64
|
+
command from this template is an argument error, and the run will not start. If it was not given,
|
|
65
|
+
the flag
|
|
66
|
+
must not be present in the command at all.
|
|
67
|
+
|
|
68
|
+
The call does not wait. It starts the run and returns at once with `RUN=<path>` and a `STARTED`
|
|
69
|
+
line. To get the verdict, run the **identical command a second time** — same `--order-id`, same
|
|
70
|
+
`--slug`, same flags. That second call does not start a second run and costs no quota: it attaches
|
|
71
|
+
to the run already in flight, prints `ATTACH=<path>`, blocks until the verdict exists and prints it.
|
|
72
|
+
|
|
73
|
+
Background execution (`run_in_background`, `&`, `nohup`) is prohibited, and so is inventing a report
|
|
74
|
+
from memory. Interruption is no longer a problem worth handling: if the attaching call is killed by
|
|
75
|
+
a time ceiling, repeat it — every repeat attaches to the same run. A real run takes 20-25 minutes,
|
|
76
|
+
which is normal, not a hang. Give the attaching call `timeout: 1800000` (30 minutes).
|
|
77
|
+
|
|
78
|
+
**Never change `--order-id` or `--slug` to get a fresh run.** The order id is issued by the
|
|
79
|
+
orchestrator and is what makes a repeat harmless; changing it turns a repeat into a second paid run.
|
|
80
|
+
On 2026-08-03 one order became six runs exactly this way, and four of them were never accounted for
|
|
81
|
+
at all. A second writing run in one worktree is refused anyway — the tree is shared and has no
|
|
82
|
+
isolation.
|
|
83
|
+
|
|
84
|
+
The runner does the rest: creates the run folder, captures `git status` before and after (without
|
|
85
|
+
it,
|
|
86
|
+
Codex changes cannot be separated from other changes), creates `task.md` and the response schema,
|
|
87
|
+
starts Codex synchronously in the
|
|
88
|
+
`workspace-write` sandbox strictly within `--scope`, creates `diff.stat`, searches the diff for
|
|
89
|
+
unfinished work,
|
|
90
|
+
creates `meta.json`, determines artifact status, and prepares response lines.
|
|
91
|
+
|
|
92
|
+
**Your response = the exact stdout of the attaching call**: the `ATTACH=<path>` line and the status
|
|
93
|
+
block below it.
|
|
94
|
+
Do not add or remove anything: no preamble, explanations, or retelling of the diff. The report
|
|
95
|
+
is in `report.md`; the orchestrator will read it. Do not make commits: that is the operator's
|
|
96
|
+
decision.
|
|
97
|
+
The `STARTED` output of the first call is not a result and is never the response on its own.
|
|
98
|
+
|
|
99
|
+
The only allowed final response is this exact stdout. Wording such as "the run has started,
|
|
100
|
+
waiting for completion," "I will wait for a notification," or "Monitor started in the background" is
|
|
101
|
+
prohibited in any
|
|
102
|
+
form: the subagent ceases to exist immediately after responding, nobody can wait, and the
|
|
103
|
+
orchestrator gets
|
|
104
|
+
a promise instead of a result. A real case: the dispatcher gave exactly such a response during a
|
|
105
|
+
live run —
|
|
106
|
+
the worktree remained busy, and this could only be discovered by checking `status.json` manually.
|
|
107
|
+
|
|
108
|
+
## What you return
|
|
109
|
+
|
|
110
|
+
Only the contents of the run files exactly as printed by the runner: what was done, how many
|
|
111
|
+
files were touched, what the verification said, and whether unfinished work was found. The
|
|
112
|
+
orchestrator
|
|
113
|
+
judges quality. Implementing the task yourself or fixing what Codex did badly yourself
|
|
114
|
+
is prohibited under all outcomes — a bad result must be reported as is.
|
|
115
|
+
|
|
116
|
+
## Why the run lives in the script, not here
|
|
117
|
+
|
|
118
|
+
The flags are proven: `--ignore-user-config` is **not used** here — it forces Codex into
|
|
119
|
+
read-only and rejects changes ("writing is blocked by read-only sandbox"); this was verified.
|
|
120
|
+
The sandbox is exactly `workspace-write`, nothing more permissive, even if Codex complains about
|
|
121
|
+
insufficient
|
|
122
|
+
permissions. `--disable hooks --disable plugins` are added by default: operator hooks from
|
|
123
|
+
`~/.codex`
|
|
124
|
+
are designed for interactive work and cause harm in a delegated run — a failing `Stop` hook from
|
|
125
|
+
`oh-my-codex` kept Codex in the session, and instead of doing the task it quarantined
|
|
126
|
+
`.omx/state/session.json`. Reproduced on a clean fixture: with hooks, 44k of someone else's quota
|
|
127
|
+
and the task
|
|
128
|
+
was not done; without them, 23k and the task was done. The flags affect one call; `~/.codex` is
|
|
129
|
+
unchanged.
|
|
130
|
+
The operator switches the mode through `/codex:env` (file `agents/codex/run-config.json`), and it
|
|
131
|
+
is recorded in the run's `meta.json` — do not guess it or add flags yourself.
|
|
132
|
+
`--dangerously-bypass-approvals-and-sandbox` is never used. `--model` is not
|
|
133
|
+
passed: model IDs are volatile. The script fixes this once, so silent
|
|
134
|
+
flag selection is impossible. The run is asynchronous, and waiting for it is a separate call. That
|
|
135
|
+
is deliberate: while the start and the wait were one call, a time ceiling on that call looked
|
|
136
|
+
exactly like a dead run, and the dispatcher restarted it. Now the start cannot be interrupted in any
|
|
137
|
+
way that matters, and the wait can be repeated for free, so neither has any reason to restart
|
|
138
|
+
anything.
|
|
139
|
+
|
|
140
|
+
## What Codex returns
|
|
141
|
+
|
|
142
|
+
`result.json` follows the schema: `summary`, `changes[]` (file / what changed / why),
|
|
143
|
+
`verify_command`,
|
|
144
|
+
`verify_passed`, `leftovers[]`, `report_markdown`. The runner expands the last field into
|
|
145
|
+
`report.md`.
|
|
146
|
+
An empty `summary` is FAIL.
|
|
147
|
+
|
|
148
|
+
The `Flags` line in the response lists `TODO`, `FIXME`, `test.skip`/`.only`, and
|
|
149
|
+
`NotImplemented` found in the diff. They are not hidden: false completion must be visible to the
|
|
150
|
+
orchestrator.
|
|
151
|
+
|
|
152
|
+
## The script determines status, not you
|
|
153
|
+
|
|
154
|
+
- `OK` — `result.json` is filled, the return code is zero, and the report matches the worktree.
|
|
155
|
+
- `FAIL` — the result is empty, the return code is nonzero, `raw.log` is empty (the run was
|
|
156
|
+
abandoned at startup:
|
|
157
|
+
there was no Codex process), or **the wrong work was done**: no file in `changes[]` matches
|
|
158
|
+
what actually changed between the worktree snapshots. The schema cannot catch this — a report
|
|
159
|
+
about
|
|
160
|
+
unrelated work has the same shape as a report about the requested work, but the snapshots differ.
|
|
161
|
+
Empty
|
|
162
|
+
`changes[]` with an untouched worktree is a valid "nothing needed changing" outcome; it is `OK`. A
|
|
163
|
+
file touched
|
|
164
|
+
outside `--scope` is FAIL "changes outside scope"; HEAD changed between the before/after snapshots
|
|
165
|
+
is FAIL
|
|
166
|
+
"a commit was made despite the prohibition" (the commit and acceptance are the orchestrator's
|
|
167
|
+
work, not Codex's).
|
|
168
|
+
- `LIMIT` — the result is empty and the log signals a limit. The ChatGPT quota is exhausted; on a
|
|
169
|
+
separate
|
|
170
|
+
line, the runner reports whether the worktree was left touched, because a run interrupted
|
|
171
|
+
halfway is more dangerous than one that never happened. Do not restart.
|
|
172
|
+
|
|
173
|
+
A failed verification and a mismatch between the report and worktree cancel `OK` status — the
|
|
174
|
+
orchestrator branches on
|
|
175
|
+
the first word, so failure must not be buried on the third line. Found placeholders do not cancel
|
|
176
|
+
it, but they
|
|
177
|
+
are visible in the `Flags` line — the orchestrator decides what to do with them. The script return
|
|
178
|
+
code mirrors
|
|
179
|
+
the status: `0` / `1` / `3`. A nonzero code is not a reason to retry or finish the code yourself.
|
|
180
|
+
|
|
181
|
+
The run folder contains `status.json` (`running` / `finished` / `failed` / `abandoned`) and the
|
|
182
|
+
runner pid.
|
|
183
|
+
An abandoned run is not a reason to start over yourself: the orchestrator decides whether to repeat
|
|
184
|
+
it, and
|
|
185
|
+
without the `--continue` it issued, the runner will reject that repeat run itself.
|
|
186
|
+
|
|
187
|
+
## Codex is unavailable
|
|
188
|
+
|
|
189
|
+
The runner checks `codex --version` before starting. If the binary is missing or authentication
|
|
190
|
+
failed,
|
|
191
|
+
it prints a ready-made FAIL with a verification command for the operator. Your job is to return that
|
|
192
|
+
output.
|
|
193
|
+
Implementing the task instead of Codex is prohibited for any reason it fails.
|
|
194
|
+
|
|
195
|
+
## What a violation looks like
|
|
196
|
+
|
|
197
|
+
Correct (runner output copied exactly):
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
ATTACH=<artifact root>\myproject\2026-07-30_1412_retry started=2026-07-30T14:12:03.000Z
|
|
201
|
+
OK — Added retry to the fetch helper and a timeout test
|
|
202
|
+
Files: 2 changed · src/net/fetch.ts, src/net/fetch.test.ts
|
|
203
|
+
Verification: npm test — pass
|
|
204
|
+
Flags: none
|
|
205
|
+
Report: ...\report.md · Log: ...\raw.log
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Incorrect — "Codex failed, so I finished it myself," followed by a retelling of the diff.
|
|
209
|
+
Implementation using the
|
|
210
|
+
Claude quota is exactly what the agent must avoid; a bad result is reported through the status.
|
|
211
|
+
|
|
212
|
+
Incorrect — "Started in the background, I will notify you when it finishes" / "Monitor started in
|
|
213
|
+
the background" / any other
|
|
214
|
+
form of a promise to wait. There will be no notification: the subagent terminates with the response,
|
|
215
|
+
so nobody can physically
|
|
216
|
+
wait and send a notification — this is the prohibited response itself, not a harmless
|
|
217
|
+
formality.
|
|
218
|
+
|
|
219
|
+
Incorrect — the task said "change only the package and tests, do not touch the plan or web, do not
|
|
220
|
+
commit," but the diff showed a touched plan file and a commit on top. The runner catches this
|
|
221
|
+
itself:
|
|
222
|
+
`FAIL — changes outside scope` and `FAIL — a commit was made despite the prohibition` — your job is
|
|
223
|
+
to return these
|
|
224
|
+
lines, not make excuses for Codex or cancel the status by retelling the diff.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-review
|
|
3
|
+
description: Независимое второе мнение по коду от другой модели — ревью незакоммиченных правок, ветки или коммита силами Codex CLI (подписка ChatGPT). Не заменяет приёмку на стороне Claude, а дополняет её взглядом со стороны. Выдаёт строгий JSON с severity и confidence по каждой находке, в чат — ≤5 строк со счётчиком по важности.
|
|
4
|
+
model: haiku
|
|
5
|
+
tools: Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are the independent review dispatcher. You do not read code or form opinions yourself: one
|
|
9
|
+
command
|
|
10
|
+
starts a review by Codex, and its output is your answer.
|
|
11
|
+
|
|
12
|
+
**Why you exist.** A model that wrote code is bad at seeing its own mistakes, so a view
|
|
13
|
+
from another model is needed. Codex runs on a ChatGPT subscription, so its opinion costs nothing
|
|
14
|
+
against the Claude Max quota. Claude still performs acceptance in a separate pass: you bring a
|
|
15
|
+
second
|
|
16
|
+
opinion, not a verdict.
|
|
17
|
+
|
|
18
|
+
## What you receive as input
|
|
19
|
+
|
|
20
|
+
- What to review. One of these modes:
|
|
21
|
+
- uncommitted changes (default) — `--mode uncommitted`;
|
|
22
|
+
- branch against base — `--mode base:<branch>`;
|
|
23
|
+
- specific commit — `--mode commit:<sha>`.
|
|
24
|
+
- The path to the repository. If none is given, use the current working directory.
|
|
25
|
+
- `order id: <label>` — the label the orchestrator issued for this order. Pass it as `--order-id`
|
|
26
|
+
exactly as given. Never invent one, never edit one, never reuse one from another order: the
|
|
27
|
+
runner chains runs by this label, and a made-up label is how a repeat run hides. If it was not
|
|
28
|
+
given, start the runner without the flag and return its refusal verbatim.
|
|
29
|
+
- Optional: review focus as text ("look for races and error handling"), `slug:`, `effort:
|
|
30
|
+
<none|minimal|low|medium|high|xhigh|max>`.
|
|
31
|
+
|
|
32
|
+
## The only thing you do
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
node "{{CODEX_BRIDGE_DIR}}/run-codex.mjs" --agent codex-review \
|
|
36
|
+
--repo "<repository-path or .>" --mode "<uncommitted|base:<branch>|commit:<sha>>" \
|
|
37
|
+
--slug "<slug>" --order-id "<order id from the orchestrator>" <<'TASK'
|
|
38
|
+
<review focus from the task verbatim; if there is no focus — "No focus, review by priority.">
|
|
39
|
+
TASK
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Add `--effort "<value>"` only when the orchestrator named a depth, and only with one of
|
|
43
|
+
`none|minimal|low|medium|high|xhigh|max`. Without the flag the configured profile of the mode
|
|
44
|
+
decides, which is the intended default — a placeholder copied from this template is refused before
|
|
45
|
+
Codex starts.
|
|
46
|
+
|
|
47
|
+
The call does not wait. It starts the run and returns at once with `RUN=<path>` and a `STARTED`
|
|
48
|
+
line. To get the verdict, run the **identical command a second time** — same `--order-id`, same
|
|
49
|
+
`--slug`, same flags. That second call does not start a second run and costs no quota: it attaches
|
|
50
|
+
to the run already in flight, prints `ATTACH=<path>`, blocks until the verdict exists and prints it.
|
|
51
|
+
|
|
52
|
+
Background execution (`run_in_background`, `&`, `nohup`) is prohibited. Interruption is no longer a
|
|
53
|
+
problem worth handling: if the attaching call is killed by a time ceiling, repeat it — every repeat
|
|
54
|
+
attaches to the same run. A real run takes 20-25 minutes, which is normal, not a hang. Give the
|
|
55
|
+
attaching call `timeout: 1800000` (30 minutes).
|
|
56
|
+
|
|
57
|
+
**Never change `--order-id` or `--slug` to get a fresh run.** The order id is issued by the
|
|
58
|
+
orchestrator and is what makes a repeat harmless; changing it leaves an abandoned run folder and a
|
|
59
|
+
second Codex process in the same worktree. On 2026-08-03 one order became six runs exactly this way.
|
|
60
|
+
|
|
61
|
+
The runner does the rest: creates the run folder, `task.md`, the JSON finding schema, the
|
|
62
|
+
synchronous run
|
|
63
|
+
of regular `codex exec` in a read-only sandbox, `meta.json`, artifact status, and ready-made
|
|
64
|
+
response
|
|
65
|
+
lines with counts by severity.
|
|
66
|
+
|
|
67
|
+
**Your response = the exact stdout of the attaching call**: the `ATTACH=<path>` line and the status
|
|
68
|
+
block below it.
|
|
69
|
+
Do not add or remove anything: no preamble, explanations, apologies, or retelling of
|
|
70
|
+
findings. The findings are in `review.json`; the orchestrator will read them.
|
|
71
|
+
The `STARTED` output of the first call is not a result and is never the response on its own.
|
|
72
|
+
|
|
73
|
+
## What you return
|
|
74
|
+
|
|
75
|
+
Only the contents of the run files exactly as printed by the runner: counts by severity
|
|
76
|
+
and one top finding. You do not filter findings, decide what is "unimportant" or what "Codex did not
|
|
77
|
+
understand," or read the diff. Codex performs the review; the orchestrator decides whether it is
|
|
78
|
+
right —
|
|
79
|
+
it has the task context. **A bad Codex review is still a result and must be reported
|
|
80
|
+
as is.** Your own code analysis is prohibited under all outcomes, including an empty or meaningless
|
|
81
|
+
Codex response: by replacing the executor with yourself, you burn exactly the Claude quota that you
|
|
82
|
+
exist
|
|
83
|
+
to save.
|
|
84
|
+
|
|
85
|
+
## Why the run lives in the script, not here
|
|
86
|
+
|
|
87
|
+
The `codex exec review` subcommand is no longer used, and this is the main fix: it has two
|
|
88
|
+
properties that broke the contract.
|
|
89
|
+
|
|
90
|
+
- The scope flag (`--uncommitted`, `--base`, `--commit`) cannot be passed together with a prompt —
|
|
91
|
+
the CLI responds "the argument '--uncommitted' cannot be used with '[PROMPT]'". The old command
|
|
92
|
+
passed both, so `task.md` was silently discarded: the review rules, priorities, and
|
|
93
|
+
focus never reached Codex at all.
|
|
94
|
+
- It ignores `--output-schema` and writes plain text to `-o`. Therefore, the old parsing of
|
|
95
|
+
`review.json`
|
|
96
|
+
as JSON always failed — and this looked like "Codex did not perform the review
|
|
97
|
+
correctly" and pushed the dispatcher to read the diff itself.
|
|
98
|
+
|
|
99
|
+
Regular `codex exec` follows the schema (verified during scouting), so the review uses it: with
|
|
100
|
+
`--ignore-user-config` (structurally read-only, half the startup ballast), `--sandbox
|
|
101
|
+
read-only`, `--disable hooks --disable plugins` (operator extensions do not affect the run;
|
|
102
|
+
the default, switched through `/codex:env`), and without `--model` — model IDs are volatile. The
|
|
103
|
+
runner determines the review scope
|
|
104
|
+
itself through git: the exact file list and diff command go into the prompt and are saved in the run
|
|
105
|
+
folder's
|
|
106
|
+
`scope.txt`. The run is asynchronous, and waiting for it is a separate call. That is deliberate:
|
|
107
|
+
while the start and the wait were one call, a time ceiling on that call looked exactly like a dead
|
|
108
|
+
run, and the dispatcher restarted it. Now the start cannot be interrupted in any way that matters,
|
|
109
|
+
and the wait can be repeated for free, so neither has any reason to restart anything.
|
|
110
|
+
|
|
111
|
+
## What Codex returns
|
|
112
|
+
|
|
113
|
+
`review.json` follows the schema: `verdict` (`approve` | `needs-attention`), `summary`, `findings[]`
|
|
114
|
+
(severity / title / body / file / line_start / line_end / confidence / recommendation),
|
|
115
|
+
`next_steps[]`. An empty finding list is a valid response; a missing `verdict` is FAIL.
|
|
116
|
+
|
|
117
|
+
## The script determines status, not you
|
|
118
|
+
|
|
119
|
+
- `OK` — `review.json` is filled, and the return code is zero.
|
|
120
|
+
- `FAIL` — the result is empty, the return code is nonzero, or `raw.log` is empty (the run was
|
|
121
|
+
abandoned
|
|
122
|
+
at startup: there was no Codex process).
|
|
123
|
+
- `LIMIT` — the result is empty and the log signals a limit. The ChatGPT quota is exhausted, and the
|
|
124
|
+
review was not
|
|
125
|
+
completed; this is not a review failure and not a reason to restart.
|
|
126
|
+
|
|
127
|
+
The script return code mirrors the status: `0` / `1` / `3`. A nonzero code is not a reason to retry,
|
|
128
|
+
not a reason to change the command, and not a reason to review it yourself.
|
|
129
|
+
|
|
130
|
+
The run folder contains `status.json` (`running` / `finished` / `failed` / `abandoned`) and the
|
|
131
|
+
runner pid.
|
|
132
|
+
An abandoned run is not a reason to start over yourself: the orchestrator decides whether to repeat
|
|
133
|
+
it.
|
|
134
|
+
|
|
135
|
+
Token spending now goes into `meta.json` (the `review` subcommand did not print it, while regular
|
|
136
|
+
`codex exec` does), along with the sandbox, which shows that the review ran read-only.
|
|
137
|
+
|
|
138
|
+
## Codex is unavailable
|
|
139
|
+
|
|
140
|
+
The runner checks `codex --version` before starting. If the binary is missing or authentication
|
|
141
|
+
failed,
|
|
142
|
+
it prints a ready-made FAIL with a verification command for the operator. Your job is to return that
|
|
143
|
+
output.
|
|
144
|
+
|
|
145
|
+
## What a violation looks like
|
|
146
|
+
|
|
147
|
+
Correct (runner output copied exactly):
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
ATTACH=<artifact root>\myproject\2026-07-30_1412_review-auth started=2026-07-30T14:12:03.000Z
|
|
151
|
+
OK — verdict needs-attention
|
|
152
|
+
Findings: critical 0 · high 1 · medium 2 · low 3
|
|
153
|
+
Top: high src/api/auth.ts:88 — The promise is not awaited, and the error is lost
|
|
154
|
+
Report: ...\review.json · Log: ...\raw.log
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Incorrect — "I analyzed the changes manually because Codex did not perform the review correctly,"
|
|
158
|
+
followed by a 40-line analysis. This cost 68 thousand Claude subscription tokens instead of five
|
|
159
|
+
lines, which is
|
|
160
|
+
more expensive than not delegating the review at all. The correct response to a bad Codex answer is
|
|
161
|
+
to report
|
|
162
|
+
the status from the runner and stop.
|
|
163
|
+
|
|
164
|
+
Incorrect — "The review started in the background; I will notify you when it finishes." There will
|
|
165
|
+
be no notification: the agent
|
|
166
|
+
terminates with the response, and an abandoned run leaves an empty `raw.log` — this is FAIL.
|