@lyupro/codex-bridge 0.6.9 → 0.6.10

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +1 -1
  3. package/cli/lifecycle-lock-inspect.mjs +3 -2
  4. package/cli/lifecycle-lock.mjs +19 -132
  5. package/cli/read.mjs +20 -0
  6. package/cli/settings-merge.mjs +3 -29
  7. package/package.json +2 -2
  8. package/scripts/home-write-inventory.mjs +12 -2
  9. package/src/agents/codex-advisor.md +22 -12
  10. package/src/agents/codex-build.md +24 -14
  11. package/src/agents/codex-review.md +17 -10
  12. package/src/agents/codex-scout.md +22 -12
  13. package/src/home/hooks/order-gate.mjs +25 -11
  14. package/src/home/hooks/reply-parser.mjs +1 -1
  15. package/src/home/lib/command-path.mjs +32 -0
  16. package/src/home/lib/dispatcher-command.mjs +4 -2
  17. package/src/home/lib/kernel-lock.mjs +171 -0
  18. package/src/home/lib/meta/chain.mjs +19 -8
  19. package/src/home/lib/meta/context-only.mjs +43 -0
  20. package/src/home/lib/meta/events.mjs +14 -2
  21. package/src/home/lib/meta/launch-rows.mjs +23 -0
  22. package/src/home/lib/meta/reply.mjs +37 -3
  23. package/src/home/lib/meta/run-state.mjs +3 -0
  24. package/src/home/lib/meta/verdict.mjs +16 -8
  25. package/src/home/lib/required-inputs.mjs +48 -8
  26. package/src/home/lib/run-codex.mjs +6 -3
  27. package/src/home/lib/runner/advise-carry.mjs +2 -1
  28. package/src/home/lib/runner/args.mjs +16 -5
  29. package/src/home/lib/runner/attach.mjs +21 -14
  30. package/src/home/lib/runner/build-evidence.mjs +29 -0
  31. package/src/home/lib/runner/codex-availability.mjs +50 -0
  32. package/src/home/lib/runner/codex-cmd.mjs +2 -19
  33. package/src/home/lib/runner/continuation.mjs +60 -8
  34. package/src/home/lib/runner/exit-codes.mjs +13 -0
  35. package/src/home/lib/runner/flag-baseline.mjs +105 -0
  36. package/src/home/lib/runner/flag-scan.mjs +165 -0
  37. package/src/home/lib/runner/git-state.mjs +0 -33
  38. package/src/home/lib/runner/launcher.mjs +233 -273
  39. package/src/home/lib/runner/order-claim.mjs +84 -0
  40. package/src/home/lib/runner/order-owner.mjs +18 -0
  41. package/src/home/lib/runner/pass-gate.mjs +109 -0
  42. package/src/home/lib/runner/preflight.mjs +22 -25
  43. package/src/home/lib/runner/prompts.mjs +7 -2
  44. package/src/home/lib/runner/question-kind.mjs +41 -0
  45. package/src/home/lib/runner/run-env.mjs +2 -1
  46. package/src/home/lib/runner/sandbox-probe.mjs +1 -1
  47. package/src/home/lib/runner/scope-check.mjs +29 -11
  48. package/src/home/lib/runner/task-input.mjs +11 -2
  49. package/src/home/lib/runner/worker-admission.mjs +43 -0
  50. package/src/home/lib/runner/worker.mjs +19 -18
  51. package/src/home/lib/task-header.mjs +127 -0
  52. package/src/home/lib/write-meta.mjs +17 -5
package/CHANGELOG.md CHANGED
@@ -4,6 +4,57 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.6.10] - 2026-10-04
8
+
9
+ ### Added
10
+
11
+ - `UNAVAILABLE` is a status of its own, with exit code `5`. Before any quota is spent, a bounded
12
+ `codex --version` and `codex login status` tell a missing or signed-out Codex from an unclear answer.
13
+ Only a missing or signed-out Codex refuses as `UNAVAILABLE`, without a run folder. After the start,
14
+ only a measured sample (a final `401 Unauthorized` turn failure) ends a run `UNAVAILABLE`. Any other
15
+ error stays `FAIL` with its raw text, so a lost subscription is never mistaken for a failed task.
16
+ - A failed pass can be repeated once: `retry: <failed run> — <reason>` in the task header, with `--continue`.
17
+ The retry is accepted only for the last run of the chain, with a finished non-`OK` verdict and a worker
18
+ proven dead. It keeps the order id, agent, phase and continuation base, and does not spend another
19
+ continuation of the order. The reply says `Attempt: N of this pass`. Refusals met at a failed last run
20
+ print a ready `retry:` line.
21
+ - One order id admits one paid worker. A launcher takes a kernel-held claim on the project's run store and
22
+ the exact order id (named pipe on Windows, abstract socket on Linux). It holds the claim until its worker is
23
+ registered, and the worker admits itself under the same claim before Codex starts. A second launch of the
24
+ same order attaches to the first run instead of paying again. A claim that stays busy for 60 seconds is a
25
+ free refusal, never a second start. On 2026-09-24 two launchers of one order started 2.8 s apart and
26
+ both paid.
27
+ - A scout question can be marked `[context-only]` by its orderer. Such questions are about startup
28
+ context and pass without commands. An answer must cite `startup:<source>`, and a repository `path:line`
29
+ read by no command fails as an unread address. `questions.json` records `{id, text, kind}`.
30
+ - The build reply adds a `Flags coverage: incomplete` line when the start content of a file could not be
31
+ judged. The status stays as it is.
32
+
33
+ ### Changed
34
+
35
+ - Every runner exit code comes from one table, and a guard ties it to the docs and the prompts:
36
+ `0` OK, `1` FAIL, `2` usage error, `3` LIMIT, `4` still running or no run (`--no-wait`), `5` UNAVAILABLE.
37
+ - Service lines of a task file live only in its header: consecutive `advice:`, `continue:` and `retry:`
38
+ lines from the first line. One parser serves the runner, the task hash and the `order-gate` hook. The task
39
+ hash covers the body only, and new runs record `task_hash_scheme: 2`. A label line below the header that
40
+ looks like metadata is refused for free, with its line number and the repair. **Orchestrators that write
41
+ `advice:` further down a build task must move it to the top.**
42
+ - TODO, FIXME, skip and only flags judge only the lines a build run added. Before the run, the start content
43
+ of every dirty and untracked file is recorded, and only that run's additions are compared. Before this
44
+ change, a run was accused of markers it found in the tree.
45
+ - A scope refusal names the flag the pattern came from (`--scope` or `--scope-new`). When git cannot list
46
+ the folder the run started in, the refusal says so instead of claiming a tracked file does not exist.
47
+
48
+ ### Fixed
49
+
50
+ - A grant line no longer changes the task hash, so a run named in `continue:` is found again under a new
51
+ order id (it used to answer "the current last run is none").
52
+ - An order owned by a run from before the task header is refused truthfully, not as a different task.
53
+ - `codex-bridge read` on a run refused before Codex started now names that refusal. It no longer says
54
+ the run predates the event stream.
55
+ - A reply no longer repeats its verdict (`OK — OK —`) when the model began its own summary with one.
56
+ - An unparsable scope pattern is named in its refusal instead of printing `undefined`.
57
+
7
58
  ## [0.6.9] - 2026-10-02
8
59
 
9
60
  ### Added
package/README.md CHANGED
@@ -62,7 +62,7 @@ If a global install and a clone coexist, you have two package copies. `codex-bri
62
62
  - **Every run leaves an audit folder.** The verbatim task, scope, before/after git state, events, report, verdict, and reason remain together.
63
63
  - **Zero runtime dependencies and zero build steps.** The package is plain `.mjs` on the Node.js standard library.
64
64
 
65
- The current suite contains **2570 automated tests: 2569 passing and 1 skipped**.
65
+ The current suite contains **2848 automated tests: 2847 passing and 1 skipped**.
66
66
 
67
67
  ## Verify
68
68
 
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import fsp from 'node:fs/promises';
7
7
  import net from 'node:net';
8
+ import { tryHoldSocket } from '../src/home/lib/kernel-lock.mjs';
8
9
  import {
9
- holderLiveness, lifecycleLockAddress, lifecycleLockStrategy, parseHolder, tryHoldSocket,
10
+ holderLiveness, lifecycleLockAddress, lifecycleLockStrategy, parseHolder,
10
11
  } from './lifecycle-lock.mjs';
11
12
 
12
13
  const refusedCodes = new Set(['ECONNREFUSED', 'ENOENT']);
@@ -81,7 +82,7 @@ export async function inspectLifecycleLock(homeRoot, {
81
82
  const answer = await readSocketAnswer(target.address, answerTimeoutMs, createConnection);
82
83
  if (answer.kind === 'answer') return result({ state: 'held', holder: answer.holder });
83
84
  if (answer.kind !== 'refused') return result({ state: 'unverified', reason: answer.reason });
84
- const release = await tryHoldSocket(target.address, createServer);
85
+ const release = await tryHoldSocket(target.address, createServer, 'lifecycle lock');
85
86
  if (release === 'in-use') return result({ state: 'held', holder: null });
86
87
  await release();
87
88
  return result({ state: 'free' });
@@ -17,12 +17,14 @@
17
17
  * the holder must fail, or the platform did not give exclusivity and the call refuses rather than
18
18
  * switch protocols. What the holder answers a waiter is diagnosis only — it never grants anything.
19
19
  */
20
- import { createHash, randomUUID } from 'node:crypto';
21
- import fs from 'node:fs';
20
+ import { randomUUID } from 'node:crypto';
22
21
  import fsp from 'node:fs/promises';
23
22
  import net from 'node:net';
24
23
  import path from 'node:path';
25
24
  import { createHomeWriter } from '../src/home/lib/home-write.mjs';
25
+ import {
26
+ acquireKernelLock, directoryDigest, kernelLockAddress, kernelLockStrategy,
27
+ } from '../src/home/lib/kernel-lock.mjs';
26
28
  import { parseJsonText } from '../src/home/lib/json-file.mjs';
27
29
  import { processIdentity } from '../src/home/lib/process-identity.mjs';
28
30
 
@@ -46,25 +48,23 @@ function validateOptions(homeRoot, { command, hostRoot, waitMs, retryMs, answerT
46
48
 
47
49
  function statIdentity(homeRoot) {
48
50
  try {
49
- const stat = fs.statSync(homeRoot, { bigint: true });
50
- if (!stat.isDirectory()) throw new Error(`lifecycle lock home is not a directory: ${homeRoot}`);
51
- return createHash('sha256').update(`${stat.dev}:${stat.ino}`).digest('hex');
51
+ return directoryDigest(homeRoot);
52
52
  } catch (error) {
53
- if (error && error.code !== 'ENOENT') throw error;
54
- return null;
53
+ if (error.message === `${homeRoot} is not a directory`) {
54
+ throw new Error(`lifecycle lock home is not a directory: ${homeRoot}`);
55
+ }
56
+ throw error;
55
57
  }
56
58
  }
57
59
 
58
60
  export function lifecycleLockStrategy(platform = process.platform) {
59
- if (platform === 'win32') return 'named-pipe';
60
- return platform === 'linux' ? 'abstract-socket' : 'file';
61
+ return kernelLockStrategy(platform) ?? 'file';
61
62
  }
62
63
 
63
64
  function lockAddress(homeRoot, digest, platform) {
64
65
  const strategy = lifecycleLockStrategy(platform);
65
- if (strategy === 'named-pipe') return { strategy, address: `\\\\.\\pipe\\codex-bridge-lifecycle-${digest}` };
66
- if (strategy === 'abstract-socket') return { strategy, address: `\0codex-bridge-lifecycle-${digest}` };
67
- return { strategy, address: path.join(homeRoot, LOCK_NAME) };
66
+ const address = kernelLockAddress('lifecycle', digest, platform);
67
+ return { strategy, address: address ?? path.join(homeRoot, LOCK_NAME) };
68
68
  }
69
69
 
70
70
  export function lifecycleLockAddress(homeRoot, { platform = process.platform } = {}) {
@@ -87,73 +87,6 @@ async function prepareHome(homeRoot) {
87
87
  return { writer, digest };
88
88
  }
89
89
 
90
- function listen(server, address) {
91
- return new Promise((resolve, reject) => {
92
- const listening = () => { server.removeListener('error', failed); resolve(); };
93
- const failed = (error) => { server.removeListener('listening', listening); reject(error); };
94
- server.once('listening', listening);
95
- server.once('error', failed);
96
- try {
97
- server.listen(address);
98
- } catch (error) {
99
- server.removeListener('listening', listening);
100
- server.removeListener('error', failed);
101
- reject(error);
102
- }
103
- });
104
- }
105
-
106
- function closeServer(server) {
107
- if (!server) return Promise.resolve();
108
- return new Promise((resolve, reject) => {
109
- try {
110
- server.close((error) => {
111
- if (error && error.code !== 'ERR_SERVER_NOT_RUNNING') reject(error);
112
- else resolve();
113
- });
114
- } catch (error) {
115
- if (error.code === 'ERR_SERVER_NOT_RUNNING') resolve();
116
- else reject(error);
117
- }
118
- });
119
- }
120
-
121
- async function closeQuietly(server) {
122
- try { await closeServer(server); } catch { /* Preserve the original failure. */ }
123
- }
124
-
125
- async function selfCheck(server, address, createServer) {
126
- let probe;
127
- let detail;
128
- try {
129
- probe = createServer();
130
- await listen(probe, address);
131
- detail = 'a second listener succeeded';
132
- } catch (error) {
133
- if (!error || error.code !== 'EADDRINUSE') detail = (error && (error.code || error.message)) || String(error);
134
- }
135
- if (detail === undefined) {
136
- await closeQuietly(probe);
137
- return;
138
- }
139
- await Promise.all([closeQuietly(probe), closeQuietly(server)]);
140
- throw new Error(`lifecycle lock self-check failed: ${detail}`);
141
- }
142
-
143
- export async function tryHoldSocket(address, createServer = net.createServer) {
144
- const server = createServer((socket) => socket.destroy());
145
- try {
146
- await listen(server, address);
147
- await selfCheck(server, address, createServer);
148
- let releasePromise;
149
- return () => (releasePromise ??= closeServer(server));
150
- } catch (error) {
151
- await closeQuietly(server);
152
- if (error?.code === 'EADDRINUSE') return 'in-use';
153
- throw error;
154
- }
155
- }
156
-
157
90
  function validHolder(value) {
158
91
  return value && Number.isInteger(value.pid) && value.pid > 0
159
92
  && typeof value.command === 'string' && value.command.length > 0
@@ -188,29 +121,6 @@ function busyMessage(homeRoot, holder, hint = '') {
188
121
  return new Error(`lifecycle lock busy for ${homeRoot}: ${details}; nothing was changed${hint}`);
189
122
  }
190
123
 
191
- function readSocketHolder(address, timeoutMs) {
192
- return new Promise((resolve) => {
193
- let data = '';
194
- let settled = false;
195
- const socket = net.createConnection(address);
196
- const timer = setTimeout(() => finish(null), timeoutMs);
197
- const finish = (holder) => {
198
- if (settled) return;
199
- settled = true;
200
- clearTimeout(timer);
201
- socket.destroy();
202
- resolve(holder);
203
- };
204
- socket.on('data', (chunk) => {
205
- data += chunk.toString('utf8');
206
- const newline = data.indexOf('\n');
207
- if (newline !== -1) finish(parseHolder(data.slice(0, newline)));
208
- });
209
- socket.once('end', () => finish(parseHolder(data)));
210
- socket.once('error', () => finish(null));
211
- });
212
- }
213
-
214
124
  function lockResult(token, strategy, releaseAction) {
215
125
  let releasePromise;
216
126
  return {
@@ -226,36 +136,13 @@ function lockResult(token, strategy, releaseAction) {
226
136
  async function acquireSocketLock(homeRoot, holder, {
227
137
  address, strategy, waitMs, retryMs, answerTimeoutMs, createServer,
228
138
  }) {
229
- const accepted = new Set();
230
- const onConnection = (socket) => {
231
- accepted.add(socket);
232
- socket.once('close', () => accepted.delete(socket));
233
- socket.end(`${JSON.stringify(holder)}\n`);
234
- };
235
- const deadline = Date.now() + waitMs;
236
-
237
- for (;;) {
238
- const server = createServer(onConnection);
239
- try {
240
- await listen(server, address);
241
- holder.acquiredAt = new Date().toISOString();
242
- await selfCheck(server, address, createServer);
243
- server.unref();
244
- return lockResult(holder.token, strategy, async () => {
245
- const closing = closeServer(server);
246
- for (const socket of accepted) socket.destroy();
247
- await closing;
248
- });
249
- } catch (error) {
250
- await closeQuietly(server);
251
- if (!error || error.code !== 'EADDRINUSE') throw error;
252
- const remaining = deadline - Date.now();
253
- if (remaining <= 0) break;
254
- await wait(Math.min(retryMs, remaining));
255
- }
256
- }
257
-
258
- throw busyMessage(homeRoot, await readSocketHolder(address, answerTimeoutMs));
139
+ const result = await acquireKernelLock({
140
+ address, holder, waitMs, retryMs, answerTimeoutMs, createServer,
141
+ parseAnswer: parseHolder,
142
+ label: 'lifecycle lock',
143
+ });
144
+ if (!result.held) throw busyMessage(homeRoot, result.holder);
145
+ return lockResult(holder.token, strategy, result.release);
259
146
  }
260
147
 
261
148
  async function acquireFileLock(homeRoot, writer, holder, { waitMs, retryMs }) {
package/cli/read.mjs CHANGED
@@ -1,4 +1,6 @@
1
1
  /** Renders a run's structured Codex transport as a readable event-by-event report. */
2
+ import path from 'node:path';
3
+ import { readJsonFileSync } from '../src/home/lib/json-file.mjs';
2
4
  import { readEvents } from '../src/home/lib/meta/events.mjs';
3
5
  import { safeSlice } from '../src/home/lib/meta/paths.mjs';
4
6
  import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
@@ -92,6 +94,24 @@ export function read({ run, cwd = process.cwd(), runsRootPath = runsRoot() } = {
92
94
  // run from before the event stream existed has no file, while a run killed before the CLI
93
95
  // spoke has an empty one. Reporting both as "predates this change" would hide the second.
94
96
  if (!eventData.hasStream) {
97
+ let state;
98
+ try {
99
+ state = readJsonFileSync(path.join(lookup.runDir, 'status.json'))?.state;
100
+ } catch {
101
+ // The 2026-09-06 refusal needs its state; unreadable status keeps the legacy explanation.
102
+ }
103
+ if (state === 'aborted_pre_start') {
104
+ return result(
105
+ 1,
106
+ `Run ${lookup.runDir} was refused before Codex started (state aborted_pre_start), so it has no events.jsonl.`,
107
+ );
108
+ }
109
+ if (state === 'running') {
110
+ return result(
111
+ 1,
112
+ `Run ${lookup.runDir} has no events.jsonl yet: Codex has not started; repeat the command shortly.`,
113
+ );
114
+ }
95
115
  return result(1, `Run ${lookup.runDir} has no events.jsonl; it predates the event stream.`);
96
116
  }
97
117
  if (!eventData.hasEvents) {
@@ -1,11 +1,11 @@
1
1
  /** Merges and removes named codex-bridge hooks without disturbing host settings. */
2
2
  import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import { spawnSync } from 'node:child_process';
4
- import { existsSync, statSync } from 'node:fs';
5
4
  import fs from 'node:fs/promises';
6
5
  import path from 'node:path';
7
6
  import { randomUUID } from 'node:crypto';
8
7
  import { readJsonFileWithRaw } from '../src/home/lib/json-file.mjs';
8
+ import { envValue, resolveCommandOnPath } from '../src/home/lib/command-path.mjs';
9
9
 
10
10
  const LEGACY_SPEC = { event: 'SubagentStop', matcher: '*' };
11
11
  const settingsRuns = new AsyncLocalStorage();
@@ -18,34 +18,8 @@ export function shortCommandFor(name) {
18
18
  return `codex-bridge hook ${name}`;
19
19
  }
20
20
 
21
- function envValue(env, name) {
22
- const key = Object.keys(env).find((entry) => entry.toLowerCase() === name);
23
- return key ? env[key] : '';
24
- }
25
-
26
- function pathValue(env) {
27
- return envValue(env, 'path');
28
- }
29
-
30
- function resolveCommand(name, env) {
31
- const extensions = process.platform === 'win32'
32
- ? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
33
- : [''];
34
- for (const directory of pathValue(env).split(path.delimiter).filter(Boolean)) {
35
- for (const extension of extensions) {
36
- const candidate = path.join(path.resolve(directory), `${name}${extension}`);
37
- try {
38
- if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
39
- } catch {
40
- continue;
41
- }
42
- }
43
- }
44
- return null;
45
- }
46
-
47
21
  export function commandReachable(name = 'codex-bridge', env = process.env) {
48
- return Boolean(resolveCommand(name, env));
22
+ return Boolean(resolveCommandOnPath(name, env));
49
23
  }
50
24
 
51
25
  /**
@@ -63,7 +37,7 @@ export function reachableCommandVersion(name = 'codex-bridge', env = process.env
63
37
  }
64
38
 
65
39
  export function runReachableCommand(name, args, env = process.env) {
66
- const shimPath = resolveCommand(name, env);
40
+ const shimPath = resolveCommandOnPath(name, env);
67
41
  if (!shimPath) return null;
68
42
  // Node 24 warned with DEP0190 during update (2026-09-07): invoke cmd explicitly for npm shims.
69
43
  // Live probes covered plain, bin with space, Program Files (x86), weird & name, and caret^dir.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lyupro/codex-bridge",
3
- "version": "0.6.9",
4
- "description": "Claude Code-to-Codex delegation with 7 guards, 2570 tests, zero runtime dependencies, and zero build steps.",
3
+ "version": "0.6.10",
4
+ "description": "Claude Code-to-Codex delegation with 7 guards, 2848 tests, zero runtime dependencies, and zero build steps.",
5
5
  "keywords": [
6
6
  "claude-code",
7
7
  "codex",
@@ -37,13 +37,23 @@ export const HOME_WRITE_INVENTORY = Object.freeze([
37
37
  outside('src/home/lib/retention.mjs', 'fs-binding-escape', 1, 'The injected filesystem falls back to fs while cleaning run folders under the runs root, outside package home.'),
38
38
  outside('src/home/lib/retention.mjs', 'unlinkSync', 1, 'Removes transport files inside run folders under the runs root, outside package home.'),
39
39
  outside('src/home/lib/runner/git-state.mjs', 'openSync', 1, 'Read-only open with the r flag, hashing an untracked file of the worked repository in chunks (Plan_73 B4b).'),
40
+ outside('src/home/lib/runner/build-evidence.mjs', 'writeFileSync', 11, 'Writes the before and after tree evidence of a build run, flags.txt and flags-coverage.txt inside a run folder.'),
40
41
  outside('src/home/lib/runner/codex-cmd.mjs', 'createWriteStream', 2, 'Appends stderr.log and events.jsonl inside a run folder.'),
42
+ outside('src/home/lib/runner/flag-baseline.mjs', 'mkdirSync', 1, 'Creates the start-content copy folder inside a run folder (Plan_60 D3a).'),
43
+ outside('src/home/lib/runner/flag-baseline.mjs', 'rmSync', 1, 'Removes the start-content copies inside a run folder after the flag scan; the manifest stays.'),
44
+ outside('src/home/lib/runner/flag-baseline.mjs', 'writeFileSync', 2, 'Writes start-content copies and the flag baseline manifest inside a run folder.'),
45
+ outside('src/home/lib/runner/flag-scan.mjs', 'mkdirSync', 1,
46
+ 'Creates the temporary HEAD-content folder inside a run folder (Plan_60 D3a).'),
47
+ outside('src/home/lib/runner/flag-scan.mjs', 'unlinkSync', 1,
48
+ 'Removes a temporary HEAD-content file immediately after its flag comparison.'),
49
+ outside('src/home/lib/runner/flag-scan.mjs', 'writeFileSync', 1,
50
+ 'Writes temporary HEAD bytes inside a run folder for the added-line flag comparison.'),
41
51
  outside('src/home/lib/runner/launcher.mjs', 'mkdirSync', 2, 'Creates project and run folders under the runs root.'),
42
- outside('src/home/lib/runner/launcher.mjs', 'writeFileSync', 11, 'Writes run prompt, environment, scope, schema, and launch evidence into a run folder.'),
52
+ outside('src/home/lib/runner/launcher.mjs', 'writeFileSync', 7, 'Writes run prompt, environment, scope and schema into a run folder.'),
43
53
  outside('src/home/lib/runner/project-dir.mjs', 'mkdirSync', 2, 'Creates the runs root and a project folder under the runs root.'),
44
54
  outside('src/home/lib/runner/project-dir.mjs', 'writeFileSync', 1, 'Writes the project ownership marker under the runs root.'),
45
55
  outside('src/home/lib/runner/run-context.mjs', 'writeFileSync', 1, 'Writes a worker reply inside a run folder.'),
46
56
  outside('src/home/lib/runner/worker-order.mjs', 'writeFileSync', 1, 'Writes worker.json inside a run folder.'),
47
- outside('src/home/lib/runner/worker.mjs', 'writeFileSync', 7, 'Writes worker git snapshots and report.md inside a run folder.'),
57
+ outside('src/home/lib/runner/worker.mjs', 'writeFileSync', 1, 'Writes report.md inside a run folder.'),
48
58
  outside('src/home/lib/write-meta.mjs', 'writeFileSync', 1, 'Writes meta.json inside a run folder under the runs root.'),
49
59
  ].map((entry) => Object.freeze(entry)));
@@ -46,9 +46,11 @@ and do not reason about the design yourself.
46
46
  `order id` as `--order-id`, `task file` as `--task-file`, and `phase` as `--phase`. Use the same
47
47
  order id for both phases; never invent or edit it. If the orchestrator did not give a required
48
48
  input, do not guess — start the runner without its flag and return its refusal verbatim.
49
- - `continue` — when the task text contains a line beginning with the `continue:` label, pass the
50
- bare `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair,
51
- or swallow this grant; pass it through and let the runner issue the refusal. A continuation is
49
+ - `continue` — when the orchestrator's call carries a line beginning with `continue:` or `retry:`,
50
+ pass the bare `--continue` flag even when its run name or reason looks malformed. A `retry:`
51
+ grant lets the orchestrator repeat a failed pass (FAIL, LIMIT, UNAVAILABLE) of the same order
52
+ once; it is not the next pass. Do not inspect, repair, or swallow this grant; pass it through
53
+ and let the runner issue the refusal. A continuation is
52
54
  assigned by the orchestrator, never chosen by you. After the verdict, return the exact attaching
53
55
  output and stop; do not issue or invent another continuation. If no such grant line is present,
54
56
  the flag must not be present. The advise phase requires this grant to continue the scope run.
@@ -82,10 +84,11 @@ unmatchable by construction. Reaching for one does not rescue the run; it guaran
82
84
  codex-bridge run --agent codex-advisor --repo "<repository-path or .>" --phase "<phase from the orchestrator>" --slug "<slug>" --order-id "<order id from the orchestrator>" --task-file "<task-file path from the orchestrator>"
83
85
  ```
84
86
 
85
- That is one command line. Add only the bare `--continue` when the task text contains a line
86
- beginning with `continue:`; the flag itself takes no value (the runner accepts only
87
- `1/true/yes/0/false/no` there). The grant line names a run folder and a reason, and the runner parses
88
- it, so a malformed grant is passed through for the runner to refuse. If
87
+ That is one command line. Add only the bare `--continue` when the orchestrator's call carries a line
88
+ beginning with `continue:` or `retry:`; the flag itself takes no value (the runner accepts only
89
+ `1/true/yes/0/false/no` there). The grant line names a run folder and a reason; never inspect, repair,
90
+ or swallow it. The runner parses it, so a malformed grant is passed through for the runner to
91
+ refuse. If
89
92
  there is no grant line, do not add the flag. Add `--effort "<value>"` only if the orchestrator
90
93
  named a depth, and only with one of `none|low|medium|high|xhigh|max`. Never add free text or any
91
94
  flags beyond those two conditional flags.
@@ -142,9 +145,13 @@ path.
142
145
  - `FAIL` — the result is empty, invalid, the return code is nonzero, or the run was abandoned.
143
146
  - `LIMIT` — the result is empty and the log signals a ChatGPT quota limit. The quota is spent and
144
147
  the task was not completed; do not restart it.
148
+ - `UNAVAILABLE` — Codex is missing or signed out (checked before start, or recognised from the CLI's own
149
+ sign-in refusal after start). The task was not attempted or could not run; this is not a task failure and
150
+ not a reason to restart — return the block as is, the orchestrator hands the task to the next executor.
145
151
 
146
- The script return code mirrors the status: `0` / `1` / `3`. A nonzero code is not a reason to
152
+ The script return code mirrors the status: `0` / `1` / `3` / `5`. A nonzero code is not a reason to
147
153
  retry, change the command, or investigate on your own.
154
+ Code `5` means `UNAVAILABLE`.
148
155
 
149
156
  For `scope`, `OK — scope: sufficient; …` means the listed paths cover the question; continue with
150
157
  `--phase advise` only when the orchestrator supplies that continuation. `OK — scope: insufficient;
@@ -159,14 +166,17 @@ verbatim; compose nothing.
159
166
  The run folder contains its status and artifacts. An abandoned run is not a reason to start over
160
167
  yourself: the orchestrator decides whether to continue, and without the `--continue` it issued,
161
168
  the runner will reject that continuation itself.
162
- The orchestrator repeats a failed scope under a NEW order id, never with `continue:` or `--continue`:
169
+ The orchestrator repeats a failed scope or a failed advise with a `retry:` grant naming that failed
170
+ run, under the same order id. A successful scope is never continued as scope:
163
171
  the runner refuses a continued scope, because it would spend the continuation that advise needs.
164
172
 
165
173
  ## Codex is unavailable
166
174
 
167
- The runner checks `codex --version` before starting. If the binary is missing or authentication
168
- failed, it prints a ready-made `FAIL` with a verification command for the operator. Return that
169
- output. Performing the task manually instead of Codex is prohibited for any reason it fails.
175
+ The runner looks `codex` up on PATH and asks `codex login status` before starting. A missing binary
176
+ or the measured "Not logged in" answer prints a ready-made `UNAVAILABLE` block (first line
177
+ `UNAVAILABLE — `, exit 5, no run folder). An unclear probe prints an ordinary refusal starting
178
+ `Codex CLI unavailable:` (exit 1). Return that output verbatim.
179
+ Performing the task manually instead of Codex is prohibited for any reason it fails.
170
180
 
171
181
  ## What a violation looks like
172
182
 
@@ -39,9 +39,11 @@ job is to report the run status honestly, including failure.
39
39
  reuse an order id from another order — the runner chains runs by that label, and a made-up label
40
40
  is how a repeat run hides. If the orchestrator did not give a required input, do not guess: start
41
41
  the runner without its flag and return the runner's refusal exactly.
42
- - `continue` — when the task text contains a line beginning with the `continue:` label, pass the
43
- bare `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair,
44
- or swallow this grant; pass it through and let the runner issue the refusal. A continuation is
42
+ - `continue` — when the orchestrator's call carries a line beginning with `continue:` or `retry:`,
43
+ pass the bare `--continue` flag even when its run name or reason looks malformed. A `retry:`
44
+ grant lets the orchestrator repeat a failed pass (FAIL, LIMIT, UNAVAILABLE) of the same order
45
+ once; it is not the next pass. Do not inspect, repair, or swallow this grant; pass it through
46
+ and let the runner issue the refusal. A continuation is
45
47
  assigned by the orchestrator, never chosen by you. After the verdict, return the exact attaching
46
48
  output and stop; do not issue or invent another continuation. If no such grant line is present,
47
49
  the flag must not be present.
@@ -51,11 +53,13 @@ job is to report the run status honestly, including failure.
51
53
  heading, one line. You never read that file and never put the command on the command line: an
52
54
  operator's real check command contained `&&`, and a compound operator in an argument makes the
53
55
  host stop applying its permission rule, which is how a delegation dies on a refusal.
54
- - Every build task file must contain exactly one `advice:` line from the orchestrator: an absolute
56
+ - Every build task file must start with a header of consecutive lowercase `label: value` lines
57
+ from the first line: exactly one `advice:` line from the orchestrator and the `continue:` or
58
+ `retry:` grant when there is one, then a blank line. The `advice:` value is an absolute
55
59
  path to an existing run folder whose `meta.json` identifies a `codex-advisor` advise pass with
56
60
  status `OK`, or exactly
57
61
  `mechanical`, `revert`, `docs-only`, or `test-only`. Anything else is refused before the run
58
- starts. Pass the task file as supplied; the orchestrator owns this line.
62
+ starts. Pass the task file as supplied and never edit it; the orchestrator owns this header.
59
63
 
60
64
  ## When the host refuses the command
61
65
 
@@ -90,8 +94,9 @@ Add `--effort "<value>"` only when the orchestrator named a depth, and only with
90
94
  decides, which is the intended default — a placeholder copied from this template is refused before
91
95
  Codex starts.
92
96
 
93
- If the task text contains a line beginning with the `continue:` label, add the bare `--continue`
94
- flag even when its run name or reason looks malformed. Do not inspect, repair, or swallow the line;
97
+ If the orchestrator's call carries a line beginning with `continue:` or `retry:`, add the bare
98
+ `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair, or
99
+ swallow the line;
95
100
  the runner parses it and issues the refusal. If no such grant line is present, the flag must not be
96
101
  present in the command.
97
102
  No value
@@ -191,8 +196,9 @@ anything.
191
196
  An empty `summary` is FAIL.
192
197
 
193
198
  The `Flags` line in the response lists `TODO`, `FIXME`, `test.skip`/`.only`, and
194
- `NotImplemented` found in the diff. They are not hidden: false completion must be visible to the
195
- orchestrator.
199
+ `NotImplemented` markers on lines THIS run added, compared with its start content. They are not
200
+ hidden: false completion must be visible to the orchestrator. `Flags coverage: incomplete` means
201
+ some files could not be judged.
196
202
 
197
203
  ## The script determines status, not you
198
204
 
@@ -214,6 +220,9 @@ orchestrator.
214
220
  separate
215
221
  line, the runner reports whether the worktree was left touched, because a run interrupted
216
222
  halfway is more dangerous than one that never happened. Do not restart.
223
+ - `UNAVAILABLE` — Codex is missing or signed out (checked before start, or recognised from the CLI's own
224
+ sign-in refusal after start). The task was not attempted or could not run; this is not a task failure and
225
+ not a reason to restart — return the block as is, the orchestrator hands the task to the next executor.
217
226
 
218
227
  A failed verification and a mismatch between the report and worktree cancel `OK` status — the
219
228
  orchestrator branches on
@@ -221,7 +230,8 @@ the first word, so failure must not be buried on the third line. Found placehold
221
230
  it, but they
222
231
  are visible in the `Flags` line — the orchestrator decides what to do with them. The script return
223
232
  code mirrors
224
- the status: `0` / `1` / `3`. A nonzero code is not a reason to retry or finish the code yourself.
233
+ the status: `0` / `1` / `3` / `5`. A nonzero code is not a reason to retry or finish the code yourself.
234
+ Code `5` means `UNAVAILABLE`.
225
235
 
226
236
  The run folder contains `status.json` (`running` / `finished` / `failed` / `abandoned`) and the
227
237
  runner pid.
@@ -231,10 +241,10 @@ without the `--continue` it issued, the runner will reject that repeat run itsel
231
241
 
232
242
  ## Codex is unavailable
233
243
 
234
- The runner checks `codex --version` before starting. If the binary is missing or authentication
235
- failed,
236
- it prints a ready-made FAIL with a verification command for the operator. Your job is to return that
237
- output.
244
+ The runner looks `codex` up on PATH and asks `codex login status` before starting. A missing binary
245
+ or the measured "Not logged in" answer prints a ready-made `UNAVAILABLE` block (first line
246
+ `UNAVAILABLE — `, exit 5, no run folder). An unclear probe prints an ordinary refusal starting
247
+ `Codex CLI unavailable:` (exit 1). Return that output verbatim.
238
248
  Implementing the task instead of Codex is prohibited for any reason it fails.
239
249
 
240
250
  ## What a violation looks like
@@ -38,9 +38,11 @@ opinion, not a verdict.
38
38
  another order — the runner chains runs by that label, and a made-up label is how a repeat run
39
39
  hides. If the orchestrator did not give a required input, do not guess — start the runner without
40
40
  its flag and return the runner's refusal verbatim.
41
- - `continue` — when the task text contains a line beginning with the `continue:` label, pass the
42
- bare `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair,
43
- or swallow this grant; pass it through and let the runner issue the refusal. A continuation is
41
+ - `continue` — when the orchestrator's call carries a line beginning with `continue:` or `retry:`,
42
+ pass the bare `--continue` flag even when its run name or reason looks malformed. A `retry:`
43
+ grant lets the orchestrator repeat a failed pass (FAIL, LIMIT, UNAVAILABLE) of the same order
44
+ once; it is not the next pass. Do not inspect, repair, or swallow this grant; pass it through
45
+ and let the runner issue the refusal. A continuation is
44
46
  assigned by the orchestrator, never chosen by you. After the verdict, return the exact attaching
45
47
  output and stop; do not issue or invent another continuation. If no such grant line is present,
46
48
  the flag must not be present.
@@ -90,8 +92,9 @@ If the runner refuses with an order id collision — the id already belongs to a
90
92
  differs — that refusal is the whole answer: return `FAIL` with the runner's text, which names the
91
93
  remedy. Never retry under a different id of your own choosing; the order id is the orchestrator's.
92
94
 
93
- If the task text contains a line beginning with the `continue:` label, add the bare `--continue`
94
- flag even when its run name or reason looks malformed. Do not inspect, repair, or swallow the line;
95
+ If the orchestrator's call carries a line beginning with `continue:` or `retry:`, add the bare
96
+ `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair, or
97
+ swallow the line;
95
98
  the runner parses it and issues the refusal. If no such grant line is present, the flag must not be
96
99
  present in the command at all.
97
100
 
@@ -176,9 +179,13 @@ and the wait can be repeated for free, so neither has any reason to restart anyt
176
179
  - `LIMIT` — the result is empty and the log signals a limit. The ChatGPT quota is exhausted, and the
177
180
  review was not
178
181
  completed; this is not a review failure and not a reason to restart.
182
+ - `UNAVAILABLE` — Codex is missing or signed out (checked before start, or recognised from the CLI's own
183
+ sign-in refusal after start). The task was not attempted or could not run; this is not a task failure and
184
+ not a reason to restart — return the block as is, the orchestrator hands the task to the next executor.
179
185
 
180
- The script return code mirrors the status: `0` / `1` / `3`. A nonzero code is not a reason to retry,
186
+ The script return code mirrors the status: `0` / `1` / `3` / `5`. A nonzero code is not a reason to retry,
181
187
  not a reason to change the command, and not a reason to review it yourself.
188
+ Code `5` means `UNAVAILABLE`.
182
189
 
183
190
  The run folder contains `status.json` (`running` / `finished` / `failed` / `abandoned`) and the
184
191
  runner pid.
@@ -190,10 +197,10 @@ Token spending now goes into `meta.json` (the `review` subcommand did not print
190
197
 
191
198
  ## Codex is unavailable
192
199
 
193
- The runner checks `codex --version` before starting. If the binary is missing or authentication
194
- failed,
195
- it prints a ready-made FAIL with a verification command for the operator. Your job is to return that
196
- output.
200
+ The runner looks `codex` up on PATH and asks `codex login status` before starting. A missing binary
201
+ or the measured "Not logged in" answer prints a ready-made `UNAVAILABLE` block (first line
202
+ `UNAVAILABLE — `, exit 5, no run folder). An unclear probe prints an ordinary refusal starting
203
+ `Codex CLI unavailable:` (exit 1). Return that output verbatim.
197
204
 
198
205
  ## What a violation looks like
199
206