@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
@@ -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,9 +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:`; it has no value. The runner accepts only `1/true/yes/0/false/no` as a
87
- grant value in the task text, 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
88
92
  there is no grant line, do not add the flag. Add `--effort "<value>"` only if the orchestrator
89
93
  named a depth, and only with one of `none|low|medium|high|xhigh|max`. Never add free text or any
90
94
  flags beyond those two conditional flags.
@@ -141,9 +145,13 @@ path.
141
145
  - `FAIL` — the result is empty, invalid, the return code is nonzero, or the run was abandoned.
142
146
  - `LIMIT` — the result is empty and the log signals a ChatGPT quota limit. The quota is spent and
143
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.
144
151
 
145
- 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
146
153
  retry, change the command, or investigate on your own.
154
+ Code `5` means `UNAVAILABLE`.
147
155
 
148
156
  For `scope`, `OK — scope: sufficient; …` means the listed paths cover the question; continue with
149
157
  `--phase advise` only when the orchestrator supplies that continuation. `OK — scope: insufficient;
@@ -158,12 +166,17 @@ verbatim; compose nothing.
158
166
  The run folder contains its status and artifacts. An abandoned run is not a reason to start over
159
167
  yourself: the orchestrator decides whether to continue, and without the `--continue` it issued,
160
168
  the runner will reject that continuation itself.
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:
171
+ the runner refuses a continued scope, because it would spend the continuation that advise needs.
161
172
 
162
173
  ## Codex is unavailable
163
174
 
164
- The runner checks `codex --version` before starting. If the binary is missing or authentication
165
- failed, it prints a ready-made `FAIL` with a verification command for the operator. Return that
166
- 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.
167
180
 
168
181
  ## What a violation looks like
169
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
 
@@ -38,13 +38,17 @@ files, do not run grep, do not retell the report, and do not reason about the ta
38
38
  hides. If the orchestrator did not give a required input, do not guess — start the runner without
39
39
  its flag and return the runner's refusal verbatim.
40
40
  - The sub-questions are NOT yours to pass. They live in the task file the orchestrator wrote, under
41
- a `Questions` heading, one Markdown list item each. You never read that file, never edit it and
42
- never put a question on the command line: a question is free prose, and free prose in an argument
41
+ a `Questions` heading, one Markdown list item each. A question may carry the orderer's
42
+ `[context-only]` marker; the dispatcher never adds, removes or repairs it. You never read that
43
+ file, never edit it and never put a question on the command line: a question is free prose, and
44
+ free prose in an argument
43
45
  makes the host stop applying the operator's permission rule, which is how a delegation dies on a
44
46
  refusal. If the file holds no question, start the runner anyway and return its refusal verbatim.
45
- - `continue` — when the task text contains a line beginning with the `continue:` label, pass the
46
- bare `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair,
47
- or swallow this grant; pass it through and let the runner issue the refusal. A continuation is
47
+ - `continue` — when the orchestrator's call carries a line beginning with `continue:` or `retry:`,
48
+ pass the bare `--continue` flag even when its run name or reason looks malformed. A `retry:`
49
+ grant lets the orchestrator repeat a failed pass (FAIL, LIMIT, UNAVAILABLE) of the same order
50
+ once; it is not the next pass. Do not inspect, repair, or swallow this grant; pass it through
51
+ and let the runner issue the refusal. A continuation is
48
52
  assigned by the orchestrator, never chosen by you. After the verdict, return the exact attaching
49
53
  output and stop; do not issue or invent another continuation. If no such grant line is present,
50
54
  the flag must not be present.
@@ -85,8 +89,9 @@ Add `--effort "<value>"` only when the orchestrator named a depth, and only with
85
89
  decides, which is the intended default — a placeholder copied from this template is refused before
86
90
  Codex starts.
87
91
 
88
- If the task text contains a line beginning with the `continue:` label, add the bare `--continue`
89
- flag even when its run name or reason looks malformed. Do not inspect, repair, or swallow the line;
92
+ If the orchestrator's call carries a line beginning with `continue:` or `retry:`, add the bare
93
+ `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair, or
94
+ swallow the line;
90
95
  the runner parses it and issues the refusal. If no such grant line is present, the flag must not be
91
96
  present in the command.
92
97
  No value
@@ -170,6 +175,7 @@ confidence), `unknowns[]`, and `report_markdown`. An uncovered subquestion or an
170
175
  evidence is FAIL;
171
176
  the runner prints the line `Coverage: N/M subquestions`. The runner expands the last field into
172
177
  `report.md`.
178
+ A `[context-only]` subquestion is answered with `startup:<source>` evidence.
173
179
  An empty `answer` is FAIL with a path to the log, not a reason to choose different flags.
174
180
 
175
181
  ## The script determines status, not you
@@ -180,9 +186,13 @@ An empty `answer` is FAIL with a path to the log, not a reason to choose differe
180
186
  - `LIMIT` — the result is empty and the log signals a limit. The ChatGPT quota is exhausted, and the
181
187
  task was not
182
188
  completed; this is not a task failure and not a reason to restart.
189
+ - `UNAVAILABLE` — Codex is missing or signed out (checked before start, or recognised from the CLI's own
190
+ sign-in refusal after start). The task was not attempted or could not run; this is not a task failure and
191
+ not a reason to restart — return the block as is, the orchestrator hands the task to the next executor.
183
192
 
184
- The script return code mirrors the status: `0` / `1` / `3`. A nonzero code is not a reason to retry,
193
+ The script return code mirrors the status: `0` / `1` / `3` / `5`. A nonzero code is not a reason to retry,
185
194
  not a reason to change the command, and not a reason to investigate on your own.
195
+ Code `5` means `UNAVAILABLE`.
186
196
 
187
197
  The run folder contains `status.json` (`running` / `finished` / `failed` / `abandoned`) and the
188
198
  runner pid.
@@ -194,10 +204,10 @@ which is the runner telling you the answer is on disk already.
194
204
 
195
205
  ## Codex is unavailable
196
206
 
197
- The runner checks `codex --version` before starting. If the binary is missing or authentication
198
- failed,
199
- it prints a ready-made FAIL with a verification command for the operator. Your job is to return that
200
- output.
207
+ The runner looks `codex` up on PATH and asks `codex login status` before starting. A missing binary
208
+ or the measured "Not logged in" answer prints a ready-made `UNAVAILABLE` block (first line
209
+ `UNAVAILABLE — `, exit 5, no run folder). An unclear probe prints an ordinary refusal starting
210
+ `Codex CLI unavailable:` (exit 1). Return that output verbatim.
201
211
  Performing the task manually instead of Codex is prohibited for any reason it fails.
202
212
 
203
213
  ## What a violation looks like
@@ -42,3 +42,12 @@ boundary is needed.
42
42
  The 2026-08-05 step 2b acceptance found the abandoned-run check below the commit check. Test the
43
43
  ordering invariant itself, and leave the incident in the comment so a later cleanup does not
44
44
  silently undo it.
45
+
46
+ ## Failing tests are evidence - Plan_73 B3 fixture incident
47
+
48
+ - Never change a fixture or an expectation only to make a failing test pass.
49
+ - When an existing test fails for a reason the task does not describe, leave it failing and name
50
+ it under deviations.
51
+
52
+ The 2026-09-30 Plan_73 B3 run rewrote a witness fixture so that a snapshot codec defect stopped
53
+ showing; that defect would have failed every build started on a clean tree.
@@ -1,6 +1,6 @@
1
- import fs from 'node:fs';
2
1
  import path from 'node:path';
3
- import { BRAND_STATE_DIR } from '../lib/brand-home.mjs';
2
+ import { BRAND_HOME, BRAND_STATE_DIR } from '../lib/brand-home.mjs';
3
+ import { createHomeWriter } from '../lib/home-write.mjs';
4
4
  import { readJsonFileSync } from '../lib/json-file.mjs';
5
5
 
6
6
  /**
@@ -49,8 +49,10 @@ export function takeTry(agentId, kind, maxBlocks = kind === STATE ? MAX_STATE_BL
49
49
  const ids = Object.keys(seen);
50
50
  if (ids.length > 200) ids.slice(0, ids.length - 200).forEach((id) => delete seen[id]);
51
51
  try {
52
- fs.mkdirSync(path.dirname(BLOCKED_FILE), { recursive: true });
53
- fs.writeFileSync(BLOCKED_FILE, `${JSON.stringify(seen)}\n`);
52
+ // Plan_65 B4: the budget is the registered guard-tries artifact, so purge can name and remove it.
53
+ const writer = createHomeWriter({ root: BRAND_HOME.root });
54
+ writer.mkdirSync('guard-tries', path.dirname(BLOCKED_FILE), { recursive: true });
55
+ writer.writeFileSync('guard-tries', BLOCKED_FILE, `${JSON.stringify(seen)}\n`);
54
56
  } catch {
55
57
  return 'untracked';
56
58
  }
@@ -14,23 +14,23 @@
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { AGENTS } from '../lib/agents.mjs';
17
- import { BRAND_STATE_DIR } from '../lib/brand-home.mjs';
17
+ import { recordHookDiagnostic } from '../lib/hook-diagnostics.mjs';
18
18
  import { readJsonFileSync } from '../lib/json-file.mjs';
19
19
  import {
20
20
  diagnoseInput,
21
21
  extractValue,
22
22
  missingInputs,
23
- parseContinuationGrant,
23
+ parseGrant,
24
24
  shellUnsafeInputs,
25
25
  } from '../lib/required-inputs.mjs';
26
26
  import { SUBAGENT_TOOLS } from '../lib/hook-definitions.mjs';
27
27
  import { taskFingerprint } from '../lib/meta/chain.mjs';
28
- import { conflictingOrderOwner } from '../lib/runner/order-owner.mjs';
28
+ import { conflictingOrderOwner, orderOwnerConflictText } from '../lib/runner/order-owner.mjs';
29
29
  import { resolveProjectRunsDir } from '../lib/runner/project-dir.mjs';
30
30
  import { runsRoot } from '../lib/runner/runs-root.mjs';
31
31
  import { parseTaskDocument } from '../lib/runner/task-file.mjs';
32
+ import { parseTaskHeader, taskHeaderRefusal } from '../lib/task-header.mjs';
32
33
 
33
- const DIAGNOSTICS_DIR = path.join(BRAND_STATE_DIR, 'diagnostics');
34
34
  const GUARDED = new Set(Object.keys(AGENTS));
35
35
  /**
36
36
  * Both spellings of the subagent-launching tool, from the same list the installer builds its
@@ -58,12 +58,7 @@ try {
58
58
  pass();
59
59
  }
60
60
 
61
- try {
62
- fs.mkdirSync(DIAGNOSTICS_DIR, { recursive: true });
63
- fs.writeFileSync(path.join(DIAGNOSTICS_DIR, 'order-gate.last.json'), `${JSON.stringify(input, null, 2)}\n`);
64
- } catch {
65
- // Diagnostics are a convenience, never a reason to fail the turn.
66
- }
61
+ recordHookDiagnostic('order-gate', input);
67
62
 
68
63
  if (!input || typeof input !== 'object' || Array.isArray(input)) pass();
69
64
  if (!SUBAGENT_TOOL_NAMES.has(input.tool_name)) pass();
@@ -121,11 +116,29 @@ const readStatus = (file) => {
121
116
 
122
117
  const orderId = extractValue(toolInput.prompt, 'order id');
123
118
  const taskFile = extractValue(toolInput.prompt, 'task file');
124
- if (!orderId || !taskFile || parseContinuationGrant(toolInput.prompt)) pass();
119
+ // Plan_75 D1, TradeForge capacity incident: retries keep their order; the runner owns conflicting-grant refusal.
120
+ const grant = parseGrant(toolInput.prompt);
121
+ if (!orderId || !taskFile || grant?.kind || grant?.error) pass();
122
+
123
+ let rawTask;
124
+ try {
125
+ rawTask = fs.readFileSync(taskFile, 'utf8');
126
+ } catch {
127
+ pass();
128
+ }
129
+ // Plan_75 D5, 2026-10-03 20:42: malformed metadata must not disappear into the disk-state guard.
130
+ const parsed = parseTaskHeader(rawTask);
131
+ const headerRefusal = taskHeaderRefusal(parsed);
132
+ if (headerRefusal) deny(headerRefusal);
133
+ let taskHash;
134
+ try {
135
+ taskHash = taskFingerprint(parseTaskDocument(parsed.body).task);
136
+ } catch {
137
+ // A malformed Questions or Verify section is the runner's refusal to issue, with its own text.
138
+ pass();
139
+ }
125
140
 
126
141
  try {
127
- const taskDocument = parseTaskDocument(fs.readFileSync(taskFile, 'utf8'));
128
- const taskHash = taskFingerprint(taskDocument.task);
129
142
  const projectRunsDir = resolveProjectRunsDir(runsRoot(), input.cwd, { create: false }).dir;
130
143
  // Per folder, not per directory: a run folder created a moment before its status.json, or any
131
144
  // leftover beside the runs, would otherwise throw out of the whole check and silently disarm the
@@ -137,12 +150,7 @@ try {
137
150
  const owner = conflictingOrderOwner(runs, orderId, taskHash);
138
151
  if (owner) {
139
152
  const ownerDir = path.join(projectRunsDir, owner.run);
140
- deny(
141
- `Order gate denied the Agent call because order id ${JSON.stringify(orderId)} already belongs ` +
142
- `to run folder ${ownerDir} (slug ${owner.status.slug}, started_at ${owner.status.started_at}) ` +
143
- 'with a different task. Use a new order id, or provide an explicit continuation grant if ' +
144
- 'this is another pass of that order.',
145
- );
153
+ deny(orderOwnerConflictText(owner, ownerDir, orderId));
146
154
  }
147
155
  } catch {
148
156
  // The 2026-08-15 collision guard is diagnostic: uncertain disk state must not block real work.
@@ -33,6 +33,7 @@ import fs from 'node:fs';
33
33
  import path from 'node:path';
34
34
  import { AGENTS } from '../lib/agents.mjs';
35
35
  import { BRAND_STATE_DIR } from '../lib/brand-home.mjs';
36
+ import { recordHookDiagnostic } from '../lib/hook-diagnostics.mjs';
36
37
  import { runOrderMismatch, transcriptOrderId } from '../lib/dispatcher-order.mjs';
37
38
  import { decideDispatcherStop, transcriptToolUses } from '../lib/dispatcher-stop.mjs';
38
39
  import { recognizeHostRefusal } from '../lib/host-refusal.mjs';
@@ -111,13 +112,7 @@ try {
111
112
  pass();
112
113
  }
113
114
 
114
- try {
115
- const diagnosticsDir = path.join(BRAND_STATE_DIR, 'diagnostics');
116
- fs.mkdirSync(diagnosticsDir, { recursive: true });
117
- fs.writeFileSync(path.join(diagnosticsDir, 'reply-guard.last.json'), `${JSON.stringify(input, null, 2)}\n`);
118
- } catch {
119
- // Diagnostics are a convenience, never a reason to fail the turn.
120
- }
115
+ recordHookDiagnostic('reply-guard', input);
121
116
 
122
117
  // A host that stops sending agent_type would let every dispatcher past the gate and this
123
118
  // audit without a word (Plan_66 D2 (c)); say so instead of passing silently.
@@ -1,5 +1,5 @@
1
1
  /** Parses dispatcher replies into guard facts. */
2
- const STATUSES = ['OK', 'FAIL', 'LIMIT'];
2
+ const STATUSES = ['OK', 'FAIL', 'LIMIT', 'UNAVAILABLE'];
3
3
 
4
4
  /**
5
5
  * Either line names the run folder. `RUN=` is printed by the call that starts a run, `ATTACH=`
@@ -15,7 +15,7 @@ import path from 'node:path';
15
15
  import { SHELL_TOOLS } from '../lib/hook-definitions.mjs';
16
16
  import { parseJsonText } from '../lib/json-file.mjs';
17
17
  import { splitRunChanges } from '../lib/meta/environment.mjs';
18
- import { changedPaths } from '../lib/meta/paths.mjs';
18
+ import { compareSnapshots } from '../lib/meta/snapshot-format.mjs';
19
19
  import { outOfScope } from '../lib/meta/verdict.mjs';
20
20
  import { git, worktreeSnapshot } from '../lib/runner/git-state.mjs';
21
21
  import { runsRoot } from '../lib/runner/runs-root.mjs';
@@ -64,16 +64,16 @@ if (beforeText === null || scopeText === null) pass();
64
64
 
65
65
  let outside;
66
66
  try {
67
- // changedPaths tolerates malformed rows; a hook must instead fail open on a damaged baseline.
68
- if (beforeText.split(/\r?\n/).filter(Boolean).some((row) =>
69
- !/^(?:\d+\t\d+|-\t-|U\t\d+)\t\S.*$/.test(row))) pass();
70
67
  const patterns = scopeText.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
71
68
  if (!patterns.length) pass();
72
69
  // The shared snapshot returns empty output on git errors; do not call that a restored tree.
73
70
  const repository = git(status.repo, ['rev-parse', '--is-inside-work-tree']);
74
71
  if (repository.error || repository.status !== 0 || repository.stdout?.trim() !== 'true') pass();
75
- const current = worktreeSnapshot(status.repo);
76
- const { work } = splitRunChanges(dir, changedPaths(beforeText, current));
72
+ // Plan_73 / 2026-09-30: a damaged or incompatible baseline proves neither a clean
73
+ // tree nor a stray edit. The advisory witness stays silent when the codec refuses it.
74
+ const comparison = compareSnapshots(beforeText, worktreeSnapshot(status.repo));
75
+ if (!comparison.ok) pass();
76
+ const { work } = splitRunChanges(dir, comparison.changed);
77
77
  outside = outOfScope(work, patterns);
78
78
  } catch {
79
79
  pass();
@@ -12,7 +12,7 @@ export const AGENTS = {
12
12
  'codex-build': { role: 'build', budget: 25, writes: true, result: 'result.json' },
13
13
  'codex-review': { role: 'review', budget: 20, writes: false, result: 'review.json' },
14
14
  // Plan_59: design advice needs a bounded scope pass before the continued decision pass.
15
- 'codex-advisor': { role: 'advisor', budget: { scope: 5, advise: 15 }, writes: false, result: 'result.json' },
15
+ 'codex-advisor': { role: 'advisor', budget: { scope: 10, advise: 25 }, writes: false, result: 'result.json' },
16
16
  };
17
17
 
18
18
  export const agentRole = (agent) => AGENTS[agent]?.role;
@@ -6,16 +6,34 @@
6
6
  * (`config-edit.mjs` keeps its own compare-and-swap publish for that reason).
7
7
  */
8
8
  import { randomUUID } from 'node:crypto';
9
- import fs from 'node:fs';
10
9
  import path from 'node:path';
11
10
 
12
- export function writeJsonAtomic(file, record) {
13
- fs.mkdirSync(path.dirname(file), { recursive: true });
11
+ function removeIfPresent(remove, temporary) {
12
+ try {
13
+ remove(temporary);
14
+ } catch (error) {
15
+ if (error?.code !== 'ENOENT') throw error;
16
+ }
17
+ }
18
+
19
+ function publishJsonAtomic(file, record, operations) {
14
20
  const temporary = `${file}.${randomUUID()}.tmp`;
15
21
  try {
16
- fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx' });
17
- fs.renameSync(temporary, file);
22
+ operations.mkdir(path.dirname(file), { recursive: true });
23
+ operations.write(temporary, `${JSON.stringify(record, null, 2)}\n`, { flag: 'wx' });
24
+ operations.rename(temporary, file);
18
25
  } finally {
19
- fs.rmSync(temporary, { force: true });
26
+ removeIfPresent(operations.unlink, temporary);
20
27
  }
21
28
  }
29
+
30
+ export function writeHomeJsonAtomic(writer, id, file, record) {
31
+ // Plan_65 B2: `state/` is shared by artifacts, so validate the file before mkdir can create it.
32
+ writer.assertArtifact(id, file);
33
+ return publishJsonAtomic(file, record, {
34
+ mkdir(directory, options) { return writer.mkdirSync(id, directory, options); },
35
+ write(temporary, contents, options) { return writer.writeFileSync(id, temporary, contents, options); },
36
+ rename(temporary, target) { return writer.renameSync(id, temporary, target); },
37
+ unlink(temporary) { return writer.unlinkSync(id, temporary); },
38
+ });
39
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Finds a command on PATH the way the host shell would, without starting it.
3
+ * The runner and CLI share this home module because the home never imports cli/ (Plan_60 D2).
4
+ */
5
+ import { existsSync, statSync } from 'node:fs';
6
+ import path from 'node:path';
7
+
8
+ export function envValue(env, name) {
9
+ const key = Object.keys(env).find((entry) => entry.toLowerCase() === name);
10
+ return key ? env[key] : '';
11
+ }
12
+
13
+ function pathValue(env) {
14
+ return envValue(env, 'path');
15
+ }
16
+
17
+ export function resolveCommandOnPath(name, env = process.env) {
18
+ const extensions = process.platform === 'win32'
19
+ ? (env.PATHEXT || '.COM;.EXE;.BAT;.CMD').split(';')
20
+ : [''];
21
+ for (const directory of pathValue(env).split(path.delimiter).filter(Boolean)) {
22
+ for (const extension of extensions) {
23
+ const candidate = path.join(path.resolve(directory), `${name}${extension}`);
24
+ try {
25
+ if (existsSync(candidate) && statSync(candidate).isFile()) return candidate;
26
+ } catch {
27
+ continue;
28
+ }
29
+ }
30
+ }
31
+ return null;
32
+ }