@lyupro/codex-bridge 0.6.8 → 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 (128) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +6 -4
  3. package/bin/codex-bridge.mjs +35 -177
  4. package/cli/command-help.mjs +49 -0
  5. package/cli/command-registry.mjs +284 -0
  6. package/cli/copy.mjs +42 -5
  7. package/cli/dispatcher-contract-record.mjs +3 -2
  8. package/cli/doctor-host-side.mjs +110 -0
  9. package/cli/doctor.mjs +17 -1
  10. package/cli/home-inspection.mjs +122 -0
  11. package/cli/home-plan-execute.mjs +140 -0
  12. package/cli/home-removal-plan.mjs +61 -0
  13. package/cli/hook-recognizer.mjs +78 -0
  14. package/cli/host-contract.mjs +11 -5
  15. package/cli/host-inspection.mjs +156 -0
  16. package/cli/host-removal.mjs +91 -0
  17. package/cli/image-evidence.mjs +44 -0
  18. package/cli/install-owners.mjs +244 -0
  19. package/cli/install-record.mjs +71 -9
  20. package/cli/install.mjs +88 -75
  21. package/cli/inventory-removal.mjs +84 -0
  22. package/cli/inventory-transition.mjs +66 -0
  23. package/cli/lifecycle-lock-clear.mjs +104 -0
  24. package/cli/lifecycle-lock-inspect.mjs +92 -0
  25. package/cli/lifecycle-lock.mjs +222 -0
  26. package/cli/lifecycle-transaction.mjs +57 -0
  27. package/cli/link-segments.mjs +45 -0
  28. package/cli/model-set.mjs +4 -3
  29. package/cli/model-speed.mjs +9 -2
  30. package/cli/model.mjs +5 -3
  31. package/cli/owner-record.mjs +40 -0
  32. package/cli/owner-sync-apply.mjs +108 -0
  33. package/cli/owner-sync.mjs +115 -0
  34. package/cli/projects.mjs +5 -2
  35. package/cli/prune-args.mjs +9 -4
  36. package/cli/prune.mjs +23 -72
  37. package/cli/purge-live-runs.mjs +98 -0
  38. package/cli/purge-preflight.mjs +103 -0
  39. package/cli/read.mjs +22 -1
  40. package/cli/record-removal.mjs +43 -0
  41. package/cli/removal-outcomes.mjs +115 -0
  42. package/cli/removal-plan.mjs +187 -0
  43. package/cli/remove-layout.mjs +40 -1
  44. package/cli/rules-owners.mjs +4 -2
  45. package/cli/settings-merge.mjs +3 -29
  46. package/cli/terminal-question.mjs +41 -0
  47. package/cli/uninstall-purge.mjs +139 -0
  48. package/cli/uninstall-rules.mjs +72 -0
  49. package/cli/uninstall.mjs +208 -127
  50. package/cli/unlock-lifecycle.mjs +102 -0
  51. package/cli/unlock.mjs +7 -4
  52. package/cli/update.mjs +102 -27
  53. package/package.json +4 -3
  54. package/scripts/home-write-audit.mjs +286 -0
  55. package/scripts/home-write-inventory.mjs +59 -0
  56. package/scripts/home-write-lexer.mjs +182 -0
  57. package/scripts/isolated-roots.mjs +6 -1
  58. package/scripts/open-work/check.mjs +180 -0
  59. package/scripts/open-work/live-thresholds.mjs +155 -0
  60. package/scripts/open-work/register.mjs +56 -0
  61. package/scripts/root-guard/root-entries.mjs +15 -0
  62. package/scripts/run-tests.mjs +26 -1
  63. package/src/agents/codex-advisor.md +23 -10
  64. package/src/agents/codex-build.md +24 -14
  65. package/src/agents/codex-review.md +17 -10
  66. package/src/agents/codex-scout.md +22 -12
  67. package/src/home/conventions.md +9 -0
  68. package/src/home/hooks/guard-tries.mjs +6 -4
  69. package/src/home/hooks/order-gate.mjs +27 -19
  70. package/src/home/hooks/reply-guard.mjs +2 -7
  71. package/src/home/hooks/reply-parser.mjs +1 -1
  72. package/src/home/hooks/worktree-witness.mjs +6 -6
  73. package/src/home/lib/agents.mjs +1 -1
  74. package/src/home/lib/atomic-json.mjs +24 -6
  75. package/src/home/lib/command-path.mjs +32 -0
  76. package/src/home/lib/config-edit.mjs +25 -10
  77. package/src/home/lib/dispatcher-command.mjs +4 -2
  78. package/src/home/lib/dispatcher-state.mjs +25 -10
  79. package/src/home/lib/file-lock.mjs +41 -10
  80. package/src/home/lib/handback-witness.mjs +7 -4
  81. package/src/home/lib/home-registry.mjs +211 -0
  82. package/src/home/lib/home-write.mjs +189 -0
  83. package/src/home/lib/hook-diagnostics.mjs +20 -0
  84. package/src/home/lib/host-observations.mjs +7 -4
  85. package/src/home/lib/kernel-lock.mjs +171 -0
  86. package/src/home/lib/meta/advice-status.mjs +2 -2
  87. package/src/home/lib/meta/advice-verdict.mjs +30 -1
  88. package/src/home/lib/meta/chain.mjs +19 -8
  89. package/src/home/lib/meta/context-only.mjs +43 -0
  90. package/src/home/lib/meta/events.mjs +16 -3
  91. package/src/home/lib/meta/launch-rows.mjs +23 -0
  92. package/src/home/lib/meta/paths.mjs +24 -24
  93. package/src/home/lib/meta/reply.mjs +37 -3
  94. package/src/home/lib/meta/run-snapshots.mjs +29 -0
  95. package/src/home/lib/meta/run-state.mjs +3 -0
  96. package/src/home/lib/meta/snapshot-format.mjs +96 -0
  97. package/src/home/lib/meta/verdict.mjs +30 -24
  98. package/src/home/lib/required-inputs.mjs +48 -8
  99. package/src/home/lib/run-codex.mjs +6 -3
  100. package/src/home/lib/run-config.mjs +10 -1
  101. package/src/home/lib/runner/advise-carry.mjs +2 -1
  102. package/src/home/lib/runner/args.mjs +16 -5
  103. package/src/home/lib/runner/attach.mjs +21 -14
  104. package/src/home/lib/runner/build-evidence.mjs +29 -0
  105. package/src/home/lib/runner/codex-availability.mjs +50 -0
  106. package/src/home/lib/runner/codex-cmd.mjs +2 -19
  107. package/src/home/lib/runner/continuation.mjs +60 -8
  108. package/src/home/lib/runner/exit-codes.mjs +13 -0
  109. package/src/home/lib/runner/flag-baseline.mjs +105 -0
  110. package/src/home/lib/runner/flag-scan.mjs +165 -0
  111. package/src/home/lib/runner/git-paths.mjs +74 -0
  112. package/src/home/lib/runner/git-state.mjs +37 -66
  113. package/src/home/lib/runner/launcher.mjs +233 -273
  114. package/src/home/lib/runner/order-claim.mjs +84 -0
  115. package/src/home/lib/runner/order-owner.mjs +18 -0
  116. package/src/home/lib/runner/pass-gate.mjs +109 -0
  117. package/src/home/lib/runner/preflight.mjs +22 -18
  118. package/src/home/lib/runner/prompts.mjs +42 -4
  119. package/src/home/lib/runner/question-kind.mjs +41 -0
  120. package/src/home/lib/runner/run-env.mjs +2 -1
  121. package/src/home/lib/runner/sandbox-probe.mjs +1 -1
  122. package/src/home/lib/runner/schemas.mjs +4 -1
  123. package/src/home/lib/runner/scope-check.mjs +55 -28
  124. package/src/home/lib/runner/task-input.mjs +11 -2
  125. package/src/home/lib/runner/worker-admission.mjs +43 -0
  126. package/src/home/lib/runner/worker.mjs +19 -18
  127. package/src/home/lib/task-header.mjs +127 -0
  128. package/src/home/lib/write-meta.mjs +17 -5
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Admits a worker and records its identity under its order claim before any paid work starts.
3
+ * Plan_60 D4/D4c, A4 r4: a launcher killed after spawn frees the claim before recording the
4
+ * detached worker, so a second launcher could abandon its run and start another paid run.
5
+ */
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { readJsonFileSync } from '../json-file.mjs';
9
+ import { writeStatus } from '../meta/run-state.mjs';
10
+ import { acquireOrderClaim } from './order-claim.mjs';
11
+
12
+ export async function admitWorker({ runDir, orderId, timing, platform, createServer }) {
13
+ const claim = await acquireOrderClaim({
14
+ projectRunsRoot: path.dirname(runDir),
15
+ orderId,
16
+ role: 'worker',
17
+ ...(timing !== undefined ? { timing } : {}),
18
+ ...(platform !== undefined ? { platform } : {}),
19
+ ...(createServer !== undefined ? { createServer } : {}),
20
+ });
21
+ try {
22
+ if (claim.busy) return { admitted: false, reason: 'claim-timeout', holder: claim.holder };
23
+ let status = null;
24
+ try {
25
+ status = readJsonFileSync(path.join(runDir, 'status.json'));
26
+ } catch {
27
+ // Plan_60 A4 r4: an unreadable run cannot authorize paid work after a launcher dies.
28
+ }
29
+ if (status?.state !== 'running' || fs.existsSync(path.join(runDir, 'meta.json'))) {
30
+ return { admitted: false, reason: 'closed', state: status?.state ?? null };
31
+ }
32
+ // The launcher cannot know this detached process's clock origin; takeover must record the
33
+ // worker's own identity before it starts producing the run's artifacts.
34
+ writeStatus(runDir, {
35
+ pid: process.pid,
36
+ runner_pid: process.pid,
37
+ process_started_at: performance.timeOrigin,
38
+ });
39
+ return { admitted: true };
40
+ } finally {
41
+ await claim.release?.();
42
+ }
43
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * It takes its whole order from worker.json, written by launcher.mjs into the run folder —
5
5
  * after the split that file is the only connection between the two halves. Read here:
6
- * `repo`, `agent`, `args`, `is_git_repo`, `budget_minutes`. Written there: those five plus
6
+ * `repo`, `agent`, `args`, `is_git_repo`, `budget_minutes`, `order_id`. Written there: those six plus
7
7
  * `phase`, `slug`, `launcher_pid` and `scope_new`, which nothing here needs but a run folder read back
8
8
  * months later does. The launcher must write the selected phase's budget into worker.json before
9
9
  * spawning this half, so the worker never re-reads config.json and one run cannot acquire two
@@ -13,11 +13,14 @@
13
13
  import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { readJsonFileSync } from '../json-file.mjs';
16
- import { collect, exitCodeFor, AGENTS } from '../write-meta.mjs';
16
+ import { collect, exitCodeFor, AGENTS, writeFailure } from '../write-meta.mjs';
17
17
  import { writeStatus } from '../meta/run-state.mjs';
18
18
  import { setRun, emitReply } from './run-context.mjs';
19
- import { git, headSha, branchName, worktreeSnapshot, findFakeDone } from './git-state.mjs';
19
+ import { writeBuildAfter } from './build-evidence.mjs';
20
20
  import { runCodex } from './codex-cmd.mjs';
21
+ import { admitWorker } from './worker-admission.mjs';
22
+ import { orderClaimHolderText } from './order-claim.mjs';
23
+ import { EXIT } from './exit-codes.mjs';
21
24
 
22
25
  /**
23
26
  * Runs the order in runDir and answers into reply.txt, never to a console — nothing here
@@ -27,13 +30,18 @@ export async function worker(runDir) {
27
30
  const cfg = readJsonFileSync(path.join(runDir, 'worker.json'));
28
31
  const repoRoot = cfg.repo;
29
32
  setRun(runDir, cfg.agent);
30
- // The launcher cannot know this detached process's clock origin; takeover must record the
31
- // worker's own identity before it starts producing the run's artifacts.
32
- writeStatus(runDir, {
33
- pid: process.pid,
34
- runner_pid: process.pid,
35
- process_started_at: performance.timeOrigin,
36
- });
33
+ const admission = await admitWorker({ runDir, orderId: cfg.order_id });
34
+ if (!admission.admitted) {
35
+ // Plan_60 A4 r4: a replacement launcher may have closed this run while the worker was starting.
36
+ if (admission.reason === 'claim-timeout') {
37
+ const { reply } = writeFailure(
38
+ runDir, cfg.agent, 'run worker was not admitted: the order claim stayed busy',
39
+ [`Claim held by ${orderClaimHolderText(admission.holder)}`, 'Codex was not started; quota was not spent'],
40
+ );
41
+ emitReply(reply);
42
+ }
43
+ process.exit(EXIT.FAIL);
44
+ }
37
45
 
38
46
  const run = await runCodex(
39
47
  cfg.args,
@@ -52,14 +60,7 @@ export async function worker(runDir) {
52
60
  stdio_drained: run.stdioDrained,
53
61
  });
54
62
 
55
- if (cfg.agent === 'codex-build') {
56
- fs.writeFileSync(path.join(runDir, 'head-after.txt'), `${cfg.is_git_repo ? headSha(repoRoot) : ''}\n`);
57
- fs.writeFileSync(path.join(runDir, 'branch-after.txt'), `${cfg.is_git_repo ? branchName(repoRoot) : ''}\n`);
58
- fs.writeFileSync(path.join(runDir, 'git-after.txt'), git(repoRoot, ['status', '--porcelain']).stdout || '');
59
- fs.writeFileSync(path.join(runDir, 'state-after.txt'), `${worktreeSnapshot(repoRoot)}\n`);
60
- fs.writeFileSync(path.join(runDir, 'diff.stat'), git(repoRoot, ['diff', '--stat']).stdout || '');
61
- fs.writeFileSync(path.join(runDir, 'flags.txt'), findFakeDone(repoRoot));
62
- }
63
+ if (cfg.agent === 'codex-build') writeBuildAfter({ runDir, repoRoot, isGitRepo: cfg.is_git_repo });
63
64
 
64
65
  // Human-readable report is carried inside the schema: a read-only scout cannot write
65
66
  // files itself, so the runner unpacks it.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Reads the orchestrator's metadata header at the top of a task file — the one place a grant or an
3
+ * advice line may stand.
4
+ *
5
+ * Plan_75 D5: on 2026-10-03 20:42 wrapped prose in a task file began a line with `continue:`, and
6
+ * the runner, which looked for grants on any line, refused the order over its own prose. Grants and
7
+ * advice had two parsers and the task hash a third rule; this module is the single reader, and a
8
+ * line below the header that still looks like real metadata is refused rather than silently obeyed
9
+ * or silently ignored.
10
+ */
11
+ import path from 'node:path';
12
+ import { splitGrantValue } from './required-inputs.mjs';
13
+
14
+ const BARE_RUN = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
15
+ const MISPLACED_GRANT = /^[A-Za-z0-9._-]+(?:$| — | - |:)/;
16
+
17
+ function validateGrant(value) {
18
+ const grant = splitGrantValue(value);
19
+ return grant && BARE_RUN.test(grant.run)
20
+ ? null
21
+ : 'grant must name a bare run folder (not . or ..) and a non-placeholder reason';
22
+ }
23
+
24
+ function descriptor(label, validate, misplaced) {
25
+ const spellings = [`${label}[ \\t]*[:=]`];
26
+ for (const wrapper of ['\\*', '_', '`', '\\*\\*', '__']) {
27
+ spellings.push(`${wrapper}${label}${wrapper}[ \\t]*[:=]`);
28
+ spellings.push(`${wrapper}${label}[ \\t]*[:=]${wrapper}`);
29
+ }
30
+ return Object.freeze({
31
+ label,
32
+ validate,
33
+ misplaced,
34
+ shape: new RegExp(
35
+ // Plan_75 D5: try a wrapper before treating its first star as a list marker.
36
+ `^[ \\t]*(?:[-*][ \\t]*)??(?:${spellings.join('|')})[ \\t]*(.*?)[ \\t]*$`,
37
+ 'i',
38
+ ),
39
+ repairHint: `move it into the header at the top of the file, spelled ${label}: <value>; `
40
+ + 'if it is an example, quote it with > or reword it inside a sentence',
41
+ });
42
+ }
43
+
44
+ // Plan_75 D5, 2026-10-03 20:42: one registry prevents wrapped task prose becoming a grant.
45
+ const LABELS = Object.freeze([
46
+ // Plan_75 D5: empty advice closes the strict header but must still explain the missing value.
47
+ descriptor('advice', (value) => value.trim() && !/[\r\n]/.test(value)
48
+ ? null : 'advice value must be non-empty and single line',
49
+ (value) => !value || /^\S+$/.test(value)
50
+ || path.posix.isAbsolute(value) || path.win32.isAbsolute(value)),
51
+ descriptor('continue', validateGrant, (value) => MISPLACED_GRANT.test(value)),
52
+ descriptor('retry', validateGrant, (value) => MISPLACED_GRANT.test(value)),
53
+ ]);
54
+
55
+ const HEADER_LINE = new RegExp(`^(${LABELS.map(({ label }) => label).join('|')}):[ \\t]*(\\S.*?)[ \\t]*$`);
56
+
57
+ export function parseTaskHeader(text) {
58
+ const lines = text.replace(/^\uFEFF/, '').split(/\r\n|\n|\r/);
59
+ const fields = {};
60
+ const entries = [];
61
+ const problems = [];
62
+ let headerEnd = 0;
63
+
64
+ for (const line of lines) {
65
+ const match = line.match(HEADER_LINE);
66
+ if (!match) break;
67
+ const [, label, value] = match;
68
+ const entry = { label, value, lineNo: headerEnd + 1, line };
69
+ if (Object.hasOwn(fields, label)) {
70
+ problems.push({ lineNo: entry.lineNo, line, reason: `duplicate ${label} label` });
71
+ } else {
72
+ fields[label] = value;
73
+ }
74
+ entries.push(entry);
75
+ headerEnd++;
76
+ }
77
+
78
+ // Plan_75 D5: conflicting authorization is a label-presence error even if a value is invalid.
79
+ if (Object.hasOwn(fields, 'continue') && Object.hasOwn(fields, 'retry')) {
80
+ const conflict = entries.find(({ label }) => label === 'retry');
81
+ problems.push({
82
+ lineNo: conflict.lineNo,
83
+ line: conflict.line,
84
+ reason: 'continue and retry cannot both be present; keep exactly one grant label',
85
+ });
86
+ }
87
+
88
+ let grant = null;
89
+ let advice = null;
90
+ for (const entry of entries) {
91
+ const definition = LABELS.find(({ label }) => label === entry.label);
92
+ const reason = definition.validate(entry.value);
93
+ if (reason) {
94
+ problems.push({ lineNo: entry.lineNo, line: entry.line, reason: `${entry.label}: ${reason}` });
95
+ } else if (entry.value !== fields[entry.label]) {
96
+ continue;
97
+ } else if (entry.label === 'advice') {
98
+ advice ??= entry.value;
99
+ } else {
100
+ grant ??= { kind: entry.label, ...splitGrantValue(entry.value) };
101
+ }
102
+ }
103
+
104
+ // Plan_75 D5, 2026-10-03 20:42: fences are examples, not an escape from the misplaced guard.
105
+ for (let index = headerEnd; index < lines.length; index++) {
106
+ for (const definition of LABELS) {
107
+ const match = lines[index].match(definition.shape);
108
+ if (!match || !definition.misplaced(match[1])) continue;
109
+ const reason = match[1] ? `misplaced ${definition.label} metadata`
110
+ : `${definition.label} value must be non-empty and single line`;
111
+ problems.push({
112
+ lineNo: index + 1,
113
+ line: lines[index],
114
+ reason: `${reason}; ${definition.repairHint}`,
115
+ });
116
+ }
117
+ }
118
+
119
+ problems.sort((a, b) => a.lineNo - b.lineNo);
120
+ return { fields, grant, advice, body: lines.slice(headerEnd).join('\n'), problems };
121
+ }
122
+
123
+ export function taskHeaderRefusal(parsed) {
124
+ if (!parsed.problems.length) return null;
125
+ return parsed.problems.map(({ lineNo, line, reason }) => `line ${lineNo}: ${reason}: ${line}`)
126
+ .concat('The run folder was not created; quota was not spent.').join('\n');
127
+ }
@@ -19,6 +19,7 @@
19
19
  * run-state.mjs keeps status.json honest, verdict.mjs decides OK/FAIL/LIMIT, reply.mjs
20
20
  * renders the lines. Importers name this file and nothing below it.
21
21
  */
22
+ import { EXIT } from './runner/exit-codes.mjs';
22
23
  import fs from 'node:fs';
23
24
  import path from 'node:path';
24
25
  import { fileURLToPath } from 'node:url';
@@ -28,7 +29,7 @@ import { splitRunChanges } from './meta/environment.mjs';
28
29
  import { readEvents } from './meta/events.mjs';
29
30
  import { writeStatus } from './meta/run-state.mjs';
30
31
  import { resolveStatus } from './meta/verdict.mjs';
31
- import { AGENTS, failReply, limitReply, withProfileRow } from './meta/reply.mjs';
32
+ import { AGENTS, failReply, limitReply, unavailableReply, withProfileRow } from './meta/reply.mjs';
32
33
  import { withLaunchRows } from './meta/launch-rows.mjs';
33
34
 
34
35
  function readRunnerVersion() {
@@ -164,7 +165,13 @@ export function collect(runDir, agent, exitCode) {
164
165
  const reply =
165
166
  withProfileRow(
166
167
  withLaunchRows(
167
- status === 'OK' ? cfg.reply(ctx) : status === 'LIMIT' ? limitReply(ctx, meta) : failReply(ctx, meta),
168
+ status === 'OK'
169
+ ? cfg.reply(ctx)
170
+ : status === 'LIMIT'
171
+ ? limitReply(ctx, meta)
172
+ : status === 'UNAVAILABLE'
173
+ ? unavailableReply(ctx, meta)
174
+ : failReply(ctx, meta),
168
175
  runDir,
169
176
  ),
170
177
  meta,
@@ -173,8 +180,13 @@ export function collect(runDir, agent, exitCode) {
173
180
  return { meta, reply: reply.join('\n') };
174
181
  }
175
182
 
176
- /** Exit code mirrors the status so an orchestrator can branch without parsing text. */
177
- export const exitCodeFor = (status) => (status === 'OK' ? 0 : status === 'LIMIT' ? 3 : 1);
183
+ /**
184
+ * Exit code numbers live in runner/exit-codes.mjs.
185
+ * USAGE is the launcher's argument error and never comes from here.
186
+ */
187
+ export const exitCodeFor = (status) =>
188
+ (status === 'OK' ? EXIT.OK : status === 'LIMIT' ? EXIT.LIMIT :
189
+ status === 'UNAVAILABLE' ? EXIT.UNAVAILABLE : EXIT.FAIL);
178
190
 
179
191
  const invokedDirectly =
180
192
  process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
@@ -183,7 +195,7 @@ if (invokedDirectly) {
183
195
  const [runDir, agent, exitCode] = process.argv.slice(2);
184
196
  if (!runDir || !agent) {
185
197
  console.error('usage: node write-meta.mjs <runDir> <agent> <exitCode>');
186
- process.exit(1);
198
+ process.exit(EXIT.FAIL);
187
199
  }
188
200
  const { meta, reply } = collect(runDir, agent, exitCode);
189
201
  console.log(reply);