@lyupro/codex-bridge 0.6.10 → 0.6.12

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 (60) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +16 -4
  3. package/cli/active-hosts.mjs +44 -0
  4. package/cli/command-registry.mjs +18 -2
  5. package/cli/dispatcher-model-check.mjs +61 -0
  6. package/cli/doctor-host-side.mjs +117 -110
  7. package/cli/doctor-installation.mjs +111 -111
  8. package/cli/doctor.mjs +19 -4
  9. package/cli/handback-witness-check.mjs +30 -20
  10. package/cli/hosts.mjs +2 -10
  11. package/cli/install-owners.mjs +1 -4
  12. package/cli/install-record.mjs +7 -1
  13. package/cli/inventory-confirm.mjs +75 -0
  14. package/cli/inventory-transition.mjs +4 -2
  15. package/cli/manifest.mjs +6 -6
  16. package/package.json +2 -2
  17. package/src/agents/codex-advisor.md +25 -119
  18. package/src/agents/codex-build.md +15 -133
  19. package/src/agents/codex-review.md +10 -109
  20. package/src/agents/codex-scout.md +11 -130
  21. package/src/home/hooks/dispatcher-gate.mjs +14 -9
  22. package/src/home/hooks/live-runs.mjs +0 -41
  23. package/src/home/hooks/order-gate.mjs +59 -40
  24. package/src/home/hooks/reply-guard.mjs +52 -65
  25. package/src/home/hooks/reply-identity.mjs +64 -0
  26. package/src/home/hooks/reply-verdicts.mjs +31 -27
  27. package/src/home/hooks/reply-witness.mjs +48 -0
  28. package/src/home/lib/claude-layout.mjs +14 -0
  29. package/src/home/lib/dispatcher-call.mjs +171 -0
  30. package/src/home/lib/dispatcher-command.mjs +18 -56
  31. package/src/home/lib/dispatcher-gate.mjs +4 -0
  32. package/src/home/lib/dispatcher-model.mjs +112 -0
  33. package/src/home/lib/dispatcher-order.mjs +21 -22
  34. package/src/home/lib/dispatcher-pin.mjs +60 -0
  35. package/src/home/lib/dispatcher-protocol.mjs +91 -0
  36. package/src/home/lib/dispatcher-stop.mjs +11 -2
  37. package/src/home/lib/handback-witness.mjs +137 -48
  38. package/src/home/lib/home-registry.mjs +7 -0
  39. package/src/home/lib/install-owner-roots.mjs +55 -0
  40. package/src/home/lib/meta/paths.mjs +1 -1
  41. package/src/home/lib/meta/reply.mjs +2 -1
  42. package/src/home/lib/model-family.mjs +38 -0
  43. package/src/home/lib/observation-ledger.mjs +145 -0
  44. package/src/home/lib/order-schema.mjs +204 -0
  45. package/src/home/lib/order-values.mjs +46 -0
  46. package/src/home/lib/runner/args.mjs +140 -231
  47. package/src/home/lib/runner/attach.mjs +3 -3
  48. package/src/home/lib/runner/continuation.mjs +174 -192
  49. package/src/home/lib/runner/launcher.mjs +9 -9
  50. package/src/home/lib/runner/order-options.mjs +30 -0
  51. package/src/home/lib/runner/order-owner.mjs +2 -1
  52. package/src/home/lib/runner/pass-gate.mjs +11 -9
  53. package/src/home/lib/runner/preflight.mjs +4 -3
  54. package/src/home/lib/runner/scope-check.mjs +11 -10
  55. package/src/home/lib/runner/task-input.mjs +23 -10
  56. package/src/home/lib/subagent-model-env.mjs +44 -0
  57. package/src/home/lib/task-header.mjs +158 -127
  58. package/src/home/lib/untyped-stop.mjs +30 -0
  59. package/src/home/lib/required-inputs.mjs +0 -301
  60. /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
@@ -1,192 +1,174 @@
1
- /**
2
- * Owns the zero-quota decision about whether a requested continuation was ordered.
3
- *
4
- * The 2026-08-05 incident showed why the runner must not infer a second pass from a verdict:
5
- * only the orchestrator read that verdict and can name the run and reason it is willing to fund.
6
- */
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
- import { CONTINUATION_INPUT, parseContinuationGrant } from '../required-inputs.mjs';
10
- import { readJson } from '../write-meta.mjs';
11
- import { workerMayBeAlive } from '../meta/run-liveness.mjs';
12
-
13
- // One comparison of run names for the grant gate and for attach, so a repeat and a grant can never
14
- // disagree about whether two spellings name the same folder.
15
- export const sameRun = (runsRootPath, left, right) => {
16
- const a = path.resolve(runsRootPath, left);
17
- const b = path.resolve(runsRootPath, right);
18
- return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
19
- };
20
-
21
- function namedRunDirectory(runsRootPath, run) {
22
- if (!run || run === '.' || run === '..' || path.basename(run) !== run) return null;
23
- const directory = path.join(runsRootPath, run);
24
- try {
25
- return fs.statSync(directory).isDirectory() ? directory : null;
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
- function normalizeGrant(grant) {
32
- if (typeof grant === 'string') return parseContinuationGrant(grant);
33
- if (!grant || typeof grant !== 'object') return null;
34
- const run = String(grant.run ?? '').trim();
35
- const reason = String(grant.reason ?? '').trim();
36
- return run && reason ? { run, reason } : null;
37
- }
38
-
39
- const grantExample = `Example: \`continue: ${CONTINUATION_INPUT.example}\`.`;
40
- const grantAction =
41
- 'Action: ask the orchestrator for an explicit grant naming the run and reason; do not invent or reuse a continuation.';
42
-
43
- // The 2026-08-10_220535_plan25-2-install-table-two-roots incident showed that a refusal must
44
- // carry the repair line; otherwise a typo forces an unnecessary directory listing and retry.
45
- function lastRunOutcome(runsRootPath, chain) {
46
- const run = chain[chain.length - 1];
47
- if (!run) return null;
48
- const status = readJson(path.join(runsRootPath, run, 'status.json')) || {};
49
- const meta = readJson(path.join(runsRootPath, run, 'meta.json')) || {};
50
- const outcomeStatus = String(meta.status ?? status.status ?? status.state ?? 'unknown').trim() || 'unknown';
51
- const outcomeReason = String(meta.reason ?? status.reason ?? 'reason not recorded').trim() || 'reason not recorded';
52
- return { run, status: outcomeStatus, reason: outcomeReason, verdict: meta.status, orderId: status.order_id };
53
- }
54
-
55
- export function readyGrantLines(runsRootPath, chain, grantReason) {
56
- const last = lastRunOutcome(runsRootPath, chain);
57
- if (!last) return 'Ready grant line: none (there is no run to continue).';
58
- const reason = String(grantReason ?? last.reason).trim() || last.reason;
59
- const continuation = `Ready grant line: continue: ${last.run} — ${reason}.`;
60
- // Plan_75 D1: the TradeForge failed advise spent its continuation, so show the same-pass repair.
61
- // A run without order_id cannot be retried (retryRefusal step d), so it gets no retry line.
62
- const orderId = String(last.orderId ?? '').trim();
63
- if (!last.verdict || last.verdict === 'OK' || !orderId) return continuation;
64
- return continuation +
65
- ` Ready retry line: retry: ${last.run} — ${last.reason} (repeats that pass under order ${orderId}).`;
66
- }
67
-
68
- function lastRunHints(runsRootPath, chain, grantReason) {
69
- const last = lastRunOutcome(runsRootPath, chain);
70
- if (!last) {
71
- return 'Last run: none. Outcome: none. Ready grant line: none (there is no run to continue).';
72
- }
73
- return `Last run: ${last.run}. Outcome: ${last.status} — ${last.reason}. ` +
74
- readyGrantLines(runsRootPath, chain, grantReason);
75
- }
76
-
77
- /**
78
- * Applies the one-continuation limit and all continuation safety gates before a folder is created.
79
- *
80
- * The limit is counted over the runs carrying THIS order id, not over the whole chain. The chain
81
- * also ties runs together by slug and by the fingerprint of the task text, which is what catches a
82
- * repeat that renamed itself — but counting continuations that way makes the escape hatch
83
- * unreachable: the operator's rule is "more passes than one need a new order id", and a new order
84
- * id lands in the same chain through the task hash. A task would then be refused with --continue
85
- * for having spent its continuation and refused without it for already having runs — permanently
86
- * unrunnable, which is a worse failure than the retry storm this limit exists to stop.
87
- */
88
- export function continuationRefusal(runsRootPath, chain, isContinue, orderId, grant) {
89
- const continuation = normalizeGrant(grant);
90
- if (!isContinue) {
91
- if (!continuation) return null;
92
- return (
93
- '--continue is required when the task text contains a `continue:` grant. ' +
94
- `${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
95
- 'The submitted grant was not rewritten. The run folder was not created; quota was not spent.'
96
- );
97
- }
98
-
99
- if (!continuation) {
100
- return (
101
- '--continue is refused: the orchestrator did not provide a `continue:` grant in the task text. ' +
102
- `${lastRunHints(runsRootPath, chain)} ` +
103
- `${grantExample} ${grantAction} The run folder was not created; quota was not spent.`
104
- );
105
- }
106
-
107
- const namedDirectory = namedRunDirectory(runsRootPath, continuation.run);
108
- if (!namedDirectory) {
109
- return (
110
- `--continue is refused: the orchestrator named run “${continuation.run}”, but it does not ` +
111
- `exist as a run folder in this project's runs directory ${runsRootPath}. ` +
112
- `${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
113
- `${grantExample} ${grantAction} The submitted grant was not rewritten. ` +
114
- 'The run folder was not created; quota was not spent.'
115
- );
116
- }
117
-
118
- const last = chain[chain.length - 1];
119
- // Continuing the last run appends a later run, so the old grant stops matching by itself; this
120
- // incident needs no counter or new state to make an orchestrator grant single-use.
121
- if (!last || !sameRun(runsRootPath, continuation.run, last)) {
122
- return (
123
- `--continue is refused: grant ${continuation.run} is not the LAST run of this task's chain; ` +
124
- `the current last run is ${last || 'none'}. A continuation is single-use: continuing the ` +
125
- 'last run appends a later run, so the old grant stops matching by itself — no counter or new ' +
126
- `state is used. ${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
127
- `${grantExample} ${grantAction} The run folder was not created; quota was not spent.`
128
- );
129
- }
130
-
131
- const wanted = String(orderId ?? '').trim();
132
- // Plan_75 D1, TradeForge capacity incident: a retry spends no new pass, even after an OK verdict.
133
- const ofThisOrder = chain.filter((run) => {
134
- const status = readJson(path.join(runsRootPath, run, 'status.json'));
135
- return String(status?.order_id ?? '').trim() === wanted && !String(status?.retry_of ?? '').trim();
136
- });
137
- if (ofThisOrder.length === 0) return null;
138
- if (ofThisOrder.length > 1) {
139
- const spent = ofThisOrder.map((run) => path.join(runsRootPath, run)).join(', ');
140
- return (
141
- `--continue is refused: order “${wanted}” already spent its allowed continuation on ${spent}. ` +
142
- `${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
143
- 'A further pass needs a new order id from the orchestrator. The run folder was not ' +
144
- 'created; quota was not spent.'
145
- );
146
- }
147
- const previous = path.join(runsRootPath, ofThisOrder[0]);
148
- const status = readJson(path.join(previous, 'status.json'));
149
- const meta = readJson(path.join(previous, 'meta.json'));
150
- if (!meta?.status || status?.state === 'running') {
151
- return (
152
- `--continue is refused: previous run ${previous} has no finished verdict and may still ` +
153
- 'be editing the worktree. Repeat without --continue to attach to it. ' +
154
- `${lastRunHints(runsRootPath, chain, continuation.reason)} The run folder was ` +
155
- 'not created; quota was not spent.'
156
- );
157
- }
158
- return null;
159
- }
160
-
161
- export function retryRefusal(runsRootPath, chain, isContinue, orderId, grant, liveness = workerMayBeAlive) {
162
- const refuse = (message) => `${message} The run folder was not created; quota was not spent.`;
163
- if (!isContinue) {
164
- return refuse('--continue is required for a `retry:` grant: the orchestrator deliberately repeats the same pass.');
165
- }
166
- const runDir = namedRunDirectory(runsRootPath, grant?.run);
167
- if (!runDir) {
168
- return refuse(`Retry is refused: ${grant?.run || 'none'} is not a bare existing run folder in ${runsRootPath}.`);
169
- }
170
- const last = chain[chain.length - 1];
171
- // Plan_75 D1: appending the TradeForge retry makes this grant single-use without a new counter.
172
- if (!last || !sameRun(runsRootPath, grant.run, last)) {
173
- return refuse(`Retry is refused: grant ${grant.run} is not the LAST run of this task's chain; a retry is single-use.`);
174
- }
175
- const status = readJson(path.join(runDir, 'status.json'));
176
- const wanted = String(orderId ?? '').trim();
177
- const previousOrder = String(status?.order_id ?? '').trim();
178
- if (!previousOrder || previousOrder !== wanted) {
179
- return refuse(`Retry is refused: run ${grant.run} order_id ${previousOrder || '(missing)'} differs from order ${wanted}. ` +
180
- 'A retry repeats its own order; a run without order_id predates orders — start a new order.');
181
- }
182
- const meta = readJson(path.join(runDir, 'meta.json'));
183
- // Plan_75 D1: a verdict alone must not let a retry overlap the failed advise's live writer.
184
- if (!meta?.status || liveness({ runDir, status })) {
185
- return refuse(`Retry is refused: run ${grant.run} has no finished verdict or its worker may still be alive and writing. ` +
186
- 'Repeat without --continue to attach to it.');
187
- }
188
- if (meta.status === 'OK') {
189
- return refuse(`Retry is refused: run ${grant.run} ended OK. A retry repeats a failed pass; use continue: for the next pass.`);
190
- }
191
- return null;
192
- }
1
+ /**
2
+ * Owns the zero-quota decision about whether a requested continuation was ordered.
3
+ *
4
+ * The 2026-08-05 incident showed why the runner must not infer a second pass from a verdict:
5
+ * only the orchestrator read that verdict and can name the run and reason it is willing to fund.
6
+ */
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { CONTINUATION_ORDER_INPUT, orderInputName } from '../order-schema.mjs';
10
+ import { readJson } from '../write-meta.mjs';
11
+ import { workerMayBeAlive } from '../meta/run-liveness.mjs';
12
+
13
+ // One comparison of run names for the grant gate and for attach, so a repeat and a grant can never
14
+ // disagree about whether two spellings name the same folder.
15
+ export const sameRun = (runsRootPath, left, right) => {
16
+ const a = path.resolve(runsRootPath, left);
17
+ const b = path.resolve(runsRootPath, right);
18
+ return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
19
+ };
20
+
21
+ function namedRunDirectory(runsRootPath, run) {
22
+ if (!run || run === '.' || run === '..' || path.basename(run) !== run) return null;
23
+ const directory = path.join(runsRootPath, run);
24
+ try {
25
+ return fs.statSync(directory).isDirectory() ? directory : null;
26
+ } catch {
27
+ return null;
28
+ }
29
+ }
30
+
31
+ function normalizeGrant(grant) {
32
+ if (!grant || typeof grant !== 'object') return null;
33
+ const run = String(grant.run ?? '').trim();
34
+ const reason = String(grant.reason ?? '').trim();
35
+ return run && reason ? { run, reason } : null;
36
+ }
37
+
38
+ const grantExample = `Example: \`continue: ${CONTINUATION_ORDER_INPUT.example}\`.`;
39
+ const grantAction =
40
+ 'Action: ask the orchestrator for an explicit grant naming the run and reason; do not invent or reuse a continuation.';
41
+
42
+ // The 2026-08-10_220535_plan25-2-install-table-two-roots incident showed that a refusal must
43
+ // carry the repair line; otherwise a typo forces an unnecessary directory listing and retry.
44
+ function lastRunOutcome(runsRootPath, chain) {
45
+ const run = chain[chain.length - 1];
46
+ if (!run) return null;
47
+ const status = readJson(path.join(runsRootPath, run, 'status.json')) || {};
48
+ const meta = readJson(path.join(runsRootPath, run, 'meta.json')) || {};
49
+ const outcomeStatus = String(meta.status ?? status.status ?? status.state ?? 'unknown').trim() || 'unknown';
50
+ const outcomeReason = String(meta.reason ?? status.reason ?? 'reason not recorded').trim() || 'reason not recorded';
51
+ return { run, status: outcomeStatus, reason: outcomeReason, verdict: meta.status, orderId: status.order_id };
52
+ }
53
+
54
+ export function readyGrantLines(runsRootPath, chain, grantReason) {
55
+ const last = lastRunOutcome(runsRootPath, chain);
56
+ if (!last) return 'Ready grant line: none (there is no run to continue).';
57
+ const reason = String(grantReason ?? last.reason).trim() || last.reason;
58
+ const continuation = `Ready grant line: continue: ${last.run} — ${reason}.`;
59
+ // Plan_75 D1: the TradeForge failed advise spent its continuation, so show the same-pass repair.
60
+ // A run without order_id cannot be retried (retryRefusal step d), so it gets no retry line.
61
+ const orderId = String(last.orderId ?? '').trim();
62
+ if (!last.verdict || last.verdict === 'OK' || !orderId) return continuation;
63
+ return continuation +
64
+ ` Ready retry line: retry: ${last.run} — ${last.reason} (repeats that pass under order ${orderId}).`;
65
+ }
66
+
67
+ function lastRunHints(runsRootPath, chain, grantReason) {
68
+ const last = lastRunOutcome(runsRootPath, chain);
69
+ if (!last) {
70
+ return 'Last run: none. Outcome: none. Ready grant line: none (there is no run to continue).';
71
+ }
72
+ return `Last run: ${last.run}. Outcome: ${last.status} — ${last.reason}. ` +
73
+ readyGrantLines(runsRootPath, chain, grantReason);
74
+ }
75
+
76
+ /**
77
+ * Applies the one-continuation limit and all continuation safety gates before a folder is created.
78
+ *
79
+ * The limit is counted over the runs carrying THIS order id, not over the whole chain. The chain
80
+ * also ties runs together by slug and by the fingerprint of the task text, which is what catches a
81
+ * repeat that renamed itself — but counting continuations that way makes the escape hatch
82
+ * unreachable: the operator's rule is "more passes than one need a new order id", and a new order
83
+ * id lands in the same chain through the task hash. A task would then be refused with a grant
84
+ * for having spent its continuation and refused without one for already having runs — permanently
85
+ * unrunnable, which is a worse failure than the retry storm this limit exists to stop.
86
+ */
87
+ export function continuationRefusal(runsRootPath, chain, isContinue, orderId, grant) {
88
+ const continuation = normalizeGrant(grant);
89
+ // Plan_63 D9: only header grant objects authorize a pass; a string is no grant.
90
+ if (!isContinue || !continuation) return null;
91
+
92
+ const namedDirectory = namedRunDirectory(runsRootPath, continuation.run);
93
+ if (!namedDirectory) {
94
+ return (
95
+ `${orderInputName('continue')} is refused: the orchestrator named run “${continuation.run}”, but it does not ` +
96
+ `exist as a run folder in this project's runs directory ${runsRootPath}. ` +
97
+ `${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
98
+ `${grantExample} ${grantAction} The submitted grant was not rewritten. ` +
99
+ 'The run folder was not created; quota was not spent.'
100
+ );
101
+ }
102
+
103
+ const last = chain[chain.length - 1];
104
+ // Continuing the last run appends a later run, so the old grant stops matching by itself; this
105
+ // incident needs no counter or new state to make an orchestrator grant single-use.
106
+ if (!last || !sameRun(runsRootPath, continuation.run, last)) {
107
+ return (
108
+ `${orderInputName('continue')} is refused: grant ${continuation.run} is not the LAST run of this task's chain; ` +
109
+ `the current last run is ${last || 'none'}. A continuation is single-use: continuing the ` +
110
+ 'last run appends a later run, so the old grant stops matching by itself — no counter or new ' +
111
+ `state is used. ${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
112
+ `${grantExample} ${grantAction} The run folder was not created; quota was not spent.`
113
+ );
114
+ }
115
+
116
+ const wanted = String(orderId ?? '').trim();
117
+ // Plan_75 D1, TradeForge capacity incident: a retry spends no new pass, even after an OK verdict.
118
+ const ofThisOrder = chain.filter((run) => {
119
+ const status = readJson(path.join(runsRootPath, run, 'status.json'));
120
+ return String(status?.order_id ?? '').trim() === wanted && !String(status?.retry_of ?? '').trim();
121
+ });
122
+ if (ofThisOrder.length === 0) return null;
123
+ if (ofThisOrder.length > 1) {
124
+ const spent = ofThisOrder.map((run) => path.join(runsRootPath, run)).join(', ');
125
+ return (
126
+ `${orderInputName('continue')} is refused: order “${wanted}” already spent its allowed continuation on ${spent}. ` +
127
+ `${lastRunHints(runsRootPath, chain, continuation.reason)} ` +
128
+ 'A further pass needs a new order id from the orchestrator. The run folder was not ' +
129
+ 'created; quota was not spent.'
130
+ );
131
+ }
132
+ const previous = path.join(runsRootPath, ofThisOrder[0]);
133
+ const status = readJson(path.join(previous, 'status.json'));
134
+ const meta = readJson(path.join(previous, 'meta.json'));
135
+ if (!meta?.status || status?.state === 'running') {
136
+ return (
137
+ `${orderInputName('continue')} is refused: previous run ${previous} has no finished verdict and may still ` +
138
+ `be editing the worktree. Repeat without ${orderInputName('continue')} to attach to it. ` +
139
+ `${lastRunHints(runsRootPath, chain, continuation.reason)} The run folder was ` +
140
+ 'not created; quota was not spent.'
141
+ );
142
+ }
143
+ return null;
144
+ }
145
+
146
+ export function retryRefusal(runsRootPath, chain, isContinue, orderId, grant, liveness = workerMayBeAlive) {
147
+ const refuse = (message) => `${message} The run folder was not created; quota was not spent.`;
148
+ const runDir = namedRunDirectory(runsRootPath, grant?.run);
149
+ if (!runDir) {
150
+ return refuse(`Retry is refused: ${grant?.run || 'none'} is not a bare existing run folder in ${runsRootPath}.`);
151
+ }
152
+ const last = chain[chain.length - 1];
153
+ // Plan_75 D1: appending the TradeForge retry makes this grant single-use without a new counter.
154
+ if (!last || !sameRun(runsRootPath, grant.run, last)) {
155
+ return refuse(`Retry is refused: grant ${grant.run} is not the LAST run of this task's chain; a retry is single-use.`);
156
+ }
157
+ const status = readJson(path.join(runDir, 'status.json'));
158
+ const wanted = String(orderId ?? '').trim();
159
+ const previousOrder = String(status?.order_id ?? '').trim();
160
+ if (!previousOrder || previousOrder !== wanted) {
161
+ return refuse(`Retry is refused: run ${grant.run} order_id ${previousOrder || '(missing)'} differs from order ${wanted}. ` +
162
+ 'A retry repeats its own order; a run without order_id predates orders — start a new order.');
163
+ }
164
+ const meta = readJson(path.join(runDir, 'meta.json'));
165
+ // Plan_75 D1: a verdict alone must not let a retry overlap the failed advise's live writer.
166
+ if (!meta?.status || liveness({ runDir, status })) {
167
+ return refuse(`Retry is refused: run ${grant.run} has no finished verdict or its worker may still be alive and writing. ` +
168
+ `Repeat without ${orderInputName('retry')} to attach to it.`);
169
+ }
170
+ if (meta.status === 'OK') {
171
+ return refuse(`Retry is refused: run ${grant.run} ended OK. A retry repeats a failed pass; use continue: for the next pass.`);
172
+ }
173
+ return null;
174
+ }
@@ -16,6 +16,7 @@ import {
16
16
  abandonedBranchDrift,
17
17
  } from '../write-meta.mjs';
18
18
  import { TASK_HASH_SCHEME } from '../meta/chain.mjs';
19
+ import { orderInputName } from '../order-schema.mjs';
19
20
  import { EXIT } from './exit-codes.mjs';
20
21
  import { passGate } from './pass-gate.mjs';
21
22
  import { setRun } from './run-context.mjs';
@@ -85,15 +86,15 @@ export const runDirPath = (root, slug, runStamp = stamp()) => {
85
86
 
86
87
  /**
87
88
  * Everything a run needs before a single token of someone else's quota is spent, and every
88
- * refusal that costs nothing: bad arguments, an open chain without --continue, a busy
89
+ * refusal that costs nothing: bad arguments, an open chain without a `continue:` grant, a busy
89
90
  * worktree, a dead Codex sandbox, a missing Codex CLI. All in the process the caller is free
90
91
  * to kill — so a killed caller can only ever interrupt a run that was already paid for.
91
92
  */
92
93
  export async function launcher(argv = process.argv.slice(2)) {
93
94
  loadRunEnv();
94
95
  const opts = parseArgs(argv);
95
- opts.phase = resolveRunPhase(opts, RUN_ENV.budgets);
96
96
  const { task: taskText, header } = settleTaskInput(opts);
97
+ opts.phase = resolveRunPhase(opts, RUN_ENV.budgets);
97
98
  // Plan_59 D5/D6: blind choices and design authority must be checked before the paid probe.
98
99
  const taskGate = taskPreflight({ agent: opts.agent, taskText, header });
99
100
  if (taskGate.refusal) die(taskGate.refusal, EXIT.FAIL);
@@ -102,13 +103,12 @@ export async function launcher(argv = process.argv.slice(2)) {
102
103
  const repoRoot = isGitRepo ? topLevel.stdout.trim() : opts.repo;
103
104
  // Plan_27 moved impossible scope failures ahead of the run directory: an absolute pattern had
104
105
  // already cost 18 minutes before the verdict could prove it matched nothing.
105
- // Every agent, not only the writing one: a pattern that cannot match gives a scout empty coverage
106
- // instead of an answer. Only codex-build may declare a not-yet-existing path, so only its scope
107
- // carries --scope-new; for the other two the list is empty and every pattern must match.
106
+ // Plan_63 D6: only codex-build's header accepts `scope:` and `scope new:`; for the other agents both
107
+ // lists are empty and the check passes. Only paths named in `scope new:` may be absent.
108
108
  const scopeRefusal = validateScope(repoRoot, opts.scopePatterns, opts.scopeNewPatterns);
109
109
  if (scopeRefusal) {
110
110
  die(
111
- `${scopeRefusal.flag} pattern ${JSON.stringify(scopeRefusal.pattern)} refused: ` +
111
+ `${orderInputName(scopeRefusal.label)} pattern ${JSON.stringify(scopeRefusal.pattern)} refused: ` +
112
112
  `${scopeRefusal.reason}. ` +
113
113
  `Action: ${scopeRefusal.action}. The run folder was not created; quota was not spent.`,
114
114
  );
@@ -224,8 +224,8 @@ export async function launcher(argv = process.argv.slice(2)) {
224
224
  );
225
225
  }
226
226
 
227
- // The sub-questions this run will be graded against come only from the orchestrator's
228
- // repeatable flags. They are written before the task is assembled so the prompt and verdict
227
+ // The sub-questions this run will be graded against come only from the task document's
228
+ // `Questions` section. They are written before the task is assembled so the prompt and verdict
229
229
  // read the same ordered list, including a valid one-question order.
230
230
  const questions = opts.agent === 'codex-scout' ? questionsFromTexts(opts.questions) : [];
231
231
  if (opts.agent === 'codex-scout') {
@@ -344,7 +344,7 @@ export async function launcher(argv = process.argv.slice(2)) {
344
344
  `STARTED agent=${opts.agent} slug=${opts.slug} order-id=${opts.orderId} worker-pid=${worker.pid}`,
345
345
  );
346
346
  console.log(
347
- 'To get the verdict, repeat the identical command with the same --order-id; it will attach to this run and will not start a second run.',
347
+ `To get the verdict, repeat the identical command with the same ${orderInputName('order id')}; it will attach to this run and will not start a second run.`,
348
348
  );
349
349
  return EXIT.OK;
350
350
  } finally {
@@ -0,0 +1,30 @@
1
+ /** Normalises one order's values into runner options, whatever channel carried them (Plan_63 D5). */
2
+ import path from 'node:path';
3
+
4
+ export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
5
+ const options = { orderId: (order.get('order id') ?? '').trim() };
6
+ const problems = [];
7
+ // Left unset when absent: the role's configured profile fills the gap, and a default here would always win.
8
+ // Plan_56 step 3 leaves effort support to the live catalogue and Codex at run start.
9
+ if (order.has('effort')) {
10
+ options.effort = order.get('effort');
11
+ if (!options.effort || /\s/.test(options.effort)) {
12
+ problems.push({ label: 'effort',
13
+ reason: `must be a non-empty single word with no whitespace; got ${JSON.stringify(options.effort)}` });
14
+ }
15
+ }
16
+ options.repo = path.resolve(order.get('repository') || cwd);
17
+ options.slug = (order.get('slug') || options.orderId).replace(/[^A-Za-z0-9._-]+/g, '-');
18
+ // Plan_29 incident: a generic slug let an honest order inherit a days-old chain.
19
+ if (!/[A-Za-z0-9]/.test(options.slug)) {
20
+ problems.push({ label: order.has('slug') ? 'slug' : 'order id',
21
+ reason: `produces an unusable run folder name after sanitization: ${JSON.stringify(options.slug)} must contain a letter or digit.` });
22
+ }
23
+ options.changeset = order.get('changeset') || 'uncommitted';
24
+ const declaredScopePatterns = (order.get('scope') || '').split(',').map((p) => p.trim()).filter(Boolean);
25
+ options.scopeNewPatterns = (order.get('scope new') || '').split(',').map((p) => p.trim()).filter(Boolean);
26
+ // Plan_27 keeps ordinary patterns strict: only paths named explicitly as new may be absent.
27
+ options.scopePatterns = [...declaredScopePatterns, ...options.scopeNewPatterns];
28
+ if (order.has('phase')) options.phase = order.get('phase');
29
+ return { options, problems };
30
+ }
@@ -1,5 +1,6 @@
1
1
  /** Keeps the runner and producer hook on one definition of which task owns an order id. */
2
2
  import { TASK_HASH_SCHEME } from '../meta/chain.mjs';
3
+ import { orderInputName } from '../order-schema.mjs';
3
4
 
4
5
  const byStartThenName = (left, right) => {
5
6
  const leftAt = String(left.status.started_at || '');
@@ -41,6 +42,6 @@ export function orderOwnerConflictText(owner, ownerDir, orderId) {
41
42
  `Its answer is already on disk in that folder (codex-bridge read "${ownerDir}"). ` +
42
43
  'Another pass of that order needs a continue:/retry: header line; a new task needs a new order id.';
43
44
  }
44
- return `${identity} with a different task. Use a new order id (--order-id), or a continue:/retry: header line ` +
45
+ return `${identity} with a different task. Use a new order id (${orderInputName('order id')}), or a continue:/retry: header line ` +
45
46
  'if this is another pass of that order.';
46
47
  }
@@ -14,6 +14,7 @@ import { attach } from './attach.mjs';
14
14
  import { continuationRefusal, retryRefusal, readyGrantLines } from './continuation.mjs';
15
15
  import { advisorTaskOrRefuse } from './advise-carry.mjs';
16
16
  import { scopeRunRefusal } from './preflight.mjs';
17
+ import { orderInputName } from '../order-schema.mjs';
17
18
 
18
19
  export async function passGate({ opts, taskText, header, projectRunsRoot, repoRoot, beforeWait }) {
19
20
  // A second pass at the same task is a real need — after a timeout or a LIMIT the work has
@@ -24,14 +25,14 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
24
25
  // The task is identified by its text as well as by its slug, because the slug is chosen by
25
26
  // whoever repeats the run: on 2026-08-02 a dispatcher whose launcher was killed restarted
26
27
  // the identical order as `<slug>-v2` and spent 46k on it. Refused before the folder exists,
27
- // like --scope.
28
+ // like an impossible scope.
28
29
  const taskHash = taskFingerprint(taskText);
29
30
  const grant = header.grant;
30
31
  // OW-040, 2026-09-30: only a retry repeats failed scope without spending advise's continuation.
31
32
  if (opts.agent === 'codex-advisor' && opts.phase === 'scope' && opts.continue && grant?.kind !== 'retry') {
32
- die('codex-advisor --phase scope refuses --continue: a scope pass is never continued, and continuing one ' +
33
+ die(`codex-advisor ${orderInputName('phase')} scope refuses ${orderInputName('continue')}: a scope pass is never continued, and continuing one ` +
33
34
  "spends the order's single continuation that its advise phase needs. Action: repeat the scope under a new order id " +
34
- "without --continue and without a continue: grant, then run advise as that order's continuation; a " +
35
+ `without ${orderInputName('continue')} and without a continue: grant, then run advise as that order's continuation; a ` +
35
36
  'scope run that failed is repeated with a `retry:` grant instead. The run folder was not created; quota was not spent.');
36
37
  }
37
38
  let continuationGrant = grant?.kind === 'continue' ? grant : null;
@@ -46,11 +47,12 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
46
47
  let attachedExitCode = opts.continue ? await attachExistingRun() : null;
47
48
  if (attachedExitCode !== null) return { exitCode: attachedExitCode };
48
49
 
49
- // The 2026-08-10_220535_plan25-2-install-table-two-roots incident exposed this ordering:
50
- // a grant without its flag must refuse before attach can print an older run's verdict.
50
+ // The 2026-08-10_220535_plan25-2-install-table-two-roots incident exposed this ordering: an
51
+ // invalid grant must refuse before attach can print an older run's verdict (Plan_63 D8: the
52
+ // grant alone means continuation, so "grant without flag" no longer exists).
51
53
  // markAbandoned() ran before this gate, so a dead runner already has meta.json with FAIL.
52
54
  // A run without a verdict can therefore only still be in flight; ordinary repeats go through
53
- // attach() after this gate. Repeated --continue calls attach first because their grant is spent.
55
+ // attach() after this gate. Repeated continuation calls attach first because their grant is spent.
54
56
  if (grant?.kind === 'retry') {
55
57
  const retryError = retryRefusal(projectRunsRoot, startedChain, opts.continue, opts.orderId, grant);
56
58
  if (retryError) die(retryError);
@@ -92,14 +94,14 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
92
94
  const lastSlug = String(readJson(path.join(projectRunsRoot, last, 'status.json'))?.slug || '');
93
95
  const renamed = lastSlug && lastSlug.toLowerCase() !== String(opts.slug).toLowerCase();
94
96
  die(
95
- `--continue is required: ${
97
+ `${orderInputName('continue')} is required: ${
96
98
  renamed
97
99
  ? `this task already ran in this repository under the name “${lastSlug}”`
98
100
  : `runs for task “${opts.slug}” already exist in this repository`
99
101
  } (${startedChain.length}), latest: ${path.join(projectRunsRoot, last)}. ` +
100
102
  'A repeat run is allowed, but the orchestrator decides, not the runner: it read the ' +
101
- 'previous response and knows whether work remains. Add --continue if you are finishing ' +
102
- 'the same task; changing --slug with the same task text does not stop it being a repeat. ' +
103
+ `previous response and knows whether work remains. Add ${orderInputName('continue')} if you are finishing ` +
104
+ `the same task; changing ${orderInputName('slug')} with the same task text does not stop it being a repeat. ` +
103
105
  `${readyGrantLines(projectRunsRoot, startedChain)} ` +
104
106
  'The run folder was not created; quota was not spent.',
105
107
  );
@@ -11,6 +11,7 @@ import { activeRunDetails, readJson } from '../write-meta.mjs';
11
11
  import { probeCodexAvailability } from './codex-availability.mjs';
12
12
  import { agentRole } from '../agents.mjs';
13
13
  import { die } from './args.mjs';
14
+ import { orderInputName } from '../order-schema.mjs';
14
15
 
15
16
  /** Plan_60 D2: only conclusive evidence says UNAVAILABLE; an unclear probe stays an ordinary refusal. */
16
17
  export async function codexAvailabilityRefusal({ probe = probeCodexAvailability } = {}) {
@@ -84,15 +85,15 @@ export function scopeRunRefusal({ agent, phase, runsRoot, grantRun }) {
84
85
  export function resolveRunPhase({ agent, phase, continue: isContinue }, budgets) {
85
86
  // Plan_59 D7: phase 2 must retain the scope run's predictions before quota is spent.
86
87
  if (agent === 'codex-advisor' && phase === 'advise' && !isContinue) {
87
- die('codex-advisor --phase advise requires --continue: phase 2 continues the scope run of the same order ' +
88
+ die(`codex-advisor ${orderInputName('phase')} advise requires a ${orderInputName('continue')} grant naming the scope run: phase 2 continues the scope run of the same order ` +
88
89
  'so it can settle the risks phase 1 predicted. The run folder was not created; quota was not spent.');
89
90
  }
90
91
  const phases = budgets[agentRole(agent)];
91
92
  const names = Object.keys(phases);
92
93
  if (phase === undefined && names.length === 1 && names[0] === 'default') return 'default';
93
94
  if (phase !== undefined && Object.hasOwn(phases, phase)) return phase;
94
- die(`${phase === undefined ? '--phase is required' : `undeclared --phase ${JSON.stringify(phase)}`} ` +
95
- `for ${agent}. Action: pass --phase <name>; allowed phases: ${names.join(', ')}. ` +
95
+ die(`${phase === undefined ? `${orderInputName('phase')} is required` : `undeclared ${orderInputName('phase')} ${JSON.stringify(phase)}`} ` +
96
+ `for ${agent}. Action: pass ${orderInputName('phase')} <name>; allowed phases: ${names.join(', ')}. ` +
96
97
  'The run folder was not created; quota was not spent.');
97
98
  }
98
99