@lyupro/codex-bridge 0.6.15 → 0.6.16

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 +23 -0
  2. package/README.md +1 -1
  3. package/cli/command-registry.mjs +1 -1
  4. package/cli/guidance-render.mjs +57 -0
  5. package/cli/guidance-topics.mjs +8 -0
  6. package/cli/guidance.mjs +9 -10
  7. package/package.json +2 -2
  8. package/scripts/home-write-inventory.mjs +2 -0
  9. package/scripts/run-tests.mjs +44 -0
  10. package/src/claude/rules/core.md +5 -4
  11. package/src/home/guidance/order.md +12 -13
  12. package/src/home/guidance/records.md +3 -1
  13. package/src/home/hooks/dispatcher-gate.mjs +8 -2
  14. package/src/home/hooks/order-gate.mjs +16 -15
  15. package/src/home/lib/dispatcher-call.mjs +27 -24
  16. package/src/home/lib/dispatcher-command.mjs +12 -6
  17. package/src/home/lib/dispatcher-gate.mjs +20 -16
  18. package/src/home/lib/dispatcher-model.mjs +7 -8
  19. package/src/home/lib/dispatcher-pin.mjs +7 -6
  20. package/src/home/lib/dispatcher-protocol.mjs +76 -4
  21. package/src/home/lib/dispatcher-state.mjs +4 -3
  22. package/src/home/lib/install-owner-roots.mjs +57 -55
  23. package/src/home/lib/meta/advisor-task.mjs +21 -12
  24. package/src/home/lib/meta/reply.mjs +1 -10
  25. package/src/home/lib/meta/unavailable-rows.mjs +11 -0
  26. package/src/home/lib/order-diagnostics/cli-flags.mjs +56 -0
  27. package/src/home/lib/order-diagnostics/filesystem.mjs +18 -0
  28. package/src/home/lib/order-diagnostics/host-state.mjs +263 -0
  29. package/src/home/lib/order-diagnostics/internal.mjs +82 -0
  30. package/src/home/lib/order-diagnostics/run-state.mjs +58 -0
  31. package/src/home/lib/order-diagnostics.mjs +48 -0
  32. package/src/home/lib/order-rules/call.mjs +130 -0
  33. package/src/home/lib/order-rules/header-grants.mjs +175 -0
  34. package/src/home/lib/order-rules/header.mjs +206 -0
  35. package/src/home/lib/order-rules/task-document.mjs +80 -0
  36. package/src/home/lib/order-rules.mjs +77 -0
  37. package/src/home/lib/order-schema.mjs +206 -200
  38. package/src/home/lib/order-text-record.mjs +104 -0
  39. package/src/home/lib/order-values.mjs +46 -46
  40. package/src/home/lib/runner/advise-carry.mjs +9 -7
  41. package/src/home/lib/runner/args.mjs +23 -26
  42. package/src/home/lib/runner/codex-availability.mjs +10 -6
  43. package/src/home/lib/runner/continuation.mjs +158 -174
  44. package/src/home/lib/runner/launcher.mjs +19 -17
  45. package/src/home/lib/runner/order-claim.mjs +15 -9
  46. package/src/home/lib/runner/order-options.mjs +3 -2
  47. package/src/home/lib/runner/order-owner.mjs +9 -7
  48. package/src/home/lib/runner/pass-gate.mjs +14 -18
  49. package/src/home/lib/runner/preflight.mjs +25 -35
  50. package/src/home/lib/runner/question-kind.mjs +4 -2
  51. package/src/home/lib/runner/retired-roots.mjs +18 -9
  52. package/src/home/lib/runner/runs-root.mjs +3 -3
  53. package/src/home/lib/runner/sandbox-diagnosis.mjs +17 -17
  54. package/src/home/lib/runner/sandbox-probe.mjs +18 -27
  55. package/src/home/lib/runner/scope-check.mjs +30 -23
  56. package/src/home/lib/runner/task-file.mjs +6 -4
  57. package/src/home/lib/runner/task-input.mjs +12 -16
  58. package/src/home/lib/runner/worker-admission.mjs +5 -2
  59. package/src/home/lib/subagent-model-env.mjs +5 -6
  60. package/src/home/lib/task-header.mjs +159 -158
@@ -17,6 +17,8 @@ import {
17
17
  } from '../write-meta.mjs';
18
18
  import { TASK_HASH_SCHEME } from '../meta/chain.mjs';
19
19
  import { orderInputName } from '../order-schema.mjs';
20
+ import { orderLabelReason, orderRuleMessage } from '../order-rules.mjs';
21
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
20
22
  import { EXIT } from './exit-codes.mjs';
21
23
  import { passGate } from './pass-gate.mjs';
22
24
  import { setRun } from './run-context.mjs';
@@ -53,7 +55,10 @@ const RUNNER_ENTRY = fileURLToPath(new URL('../run-codex.mjs', import.meta.url))
53
55
  const stamp = () => {
54
56
  const d = new Date();
55
57
  const p = (n) => String(n).padStart(2, '0');
56
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
58
+ return [
59
+ [d.getFullYear(), p(d.getMonth() + 1), p(d.getDate())].join('-'),
60
+ [p(d.getHours()), p(d.getMinutes()), p(d.getSeconds())].join(''),
61
+ ].join('_');
57
62
  };
58
63
 
59
64
  /**
@@ -69,7 +74,7 @@ export function makeRunDir(base) {
69
74
  return dir;
70
75
  } catch (err) {
71
76
  if (err.code !== 'EEXIST') throw err;
72
- dir = `${base}-${n}`;
77
+ dir = [base, n].join('-');
73
78
  }
74
79
  }
75
80
  }
@@ -108,16 +113,14 @@ export async function launcher(argv = process.argv.slice(2)) {
108
113
  // lists are empty and the check passes. Only paths named in `scope new:` may be absent.
109
114
  const scopeRefusal = validateScope(repoRoot, opts.scopePatterns, opts.scopeNewPatterns);
110
115
  if (scopeRefusal) {
111
- die(
112
- `${orderInputName(scopeRefusal.label)} pattern ${JSON.stringify(scopeRefusal.pattern)} refused: ` +
113
- `${scopeRefusal.reason}. ` +
114
- `Action: ${scopeRefusal.action}. The run folder was not created; quota was not spent.`,
115
- );
116
+ die([orderLabelReason(orderInputName(scopeRefusal.label), orderRuleMessage('scope-pattern-refusal', {
117
+ pattern: scopeRefusal.pattern, reason: scopeRefusal.reason, action: scopeRefusal.action,
118
+ })), orderDiagnosticMessage('run-not-created')].join(' '));
116
119
  }
117
120
  // Plan_77 D6: project-dir.mjs:84 recursively recreated a retired CODEX_RUNS_ROOT.
118
121
  // Refuse before that mkdir, using the same resolution that checked the advice header.
119
122
  const stale = staleOverrideRefusal(resolution);
120
- if (stale) die(`${stale}\nThe run folder was not created; quota was not spent.`, EXIT.FAIL);
123
+ if (stale) die([stale, orderDiagnosticMessage('run-not-created')].join('\n'), EXIT.FAIL);
121
124
  const projectRunsRoot = resolveProjectRunsDir(resolution.root, repoRoot).dir;
122
125
  // Plan_60 D4c, 2026-09-24: two launchers of one order both billed. The claim comes before markAbandoned, or a
123
126
  // contender could close a run whose worker is admitting itself and start a second paid one.
@@ -134,10 +137,9 @@ export async function launcher(argv = process.argv.slice(2)) {
134
137
  if (isGitRepo) {
135
138
  const drift = abandonedBranchDrift(projectRunsRoot, repoRoot, branchName(repoRoot));
136
139
  if (drift) {
137
- die(
138
- `repository is detached after abandoned run ${drift.run}, which recorded branch ${drift.branch}. ` +
139
- `Return with: git checkout ${drift.branch}. No run folder was created and no quota was spent.`,
140
- );
140
+ die(orderDiagnosticMessage('repository-detached-after-abandoned-run', {
141
+ run: drift.run, branch: drift.branch,
142
+ }));
141
143
  }
142
144
  }
143
145
 
@@ -302,8 +304,8 @@ export async function launcher(argv = process.argv.slice(2)) {
302
304
  const { reply } = writeFailure(
303
305
  runDir,
304
306
  opts.agent,
305
- `argument cannot be passed through cmd.exe (contains % or "): ${unsafe}`,
306
- ['Codex was not started; quota was not spent'],
307
+ orderDiagnosticMessage('cmd-argument-transport', { argument: unsafe }),
308
+ [orderDiagnosticMessage('codex-not-started')],
307
309
  true,
308
310
  );
309
311
  console.log(reply);
@@ -335,9 +337,9 @@ export async function launcher(argv = process.argv.slice(2)) {
335
337
  const started = await new Promise((resolve) => {
336
338
  worker.once('spawn', () => resolve(true));
337
339
  worker.once('error', (err) => {
338
- const { reply } = writeFailure(runDir, opts.agent, `run worker process failed to start: ${err.message}`, [
339
- 'Codex was not started; quota was not spent',
340
- ], true);
340
+ const { reply } = writeFailure(runDir, opts.agent,
341
+ orderDiagnosticMessage('run-worker-start-failure', { message: err.message }),
342
+ [orderDiagnosticMessage('codex-not-started')], true);
341
343
  console.log(reply);
342
344
  resolve(false);
343
345
  });
@@ -10,17 +10,19 @@ import {
10
10
  } from '../kernel-lock.mjs';
11
11
  import { parseJsonText } from '../json-file.mjs';
12
12
  import { chainRuns, readJson } from '../write-meta.mjs';
13
+ import { orderRuleMessage } from '../order-rules.mjs';
14
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
13
15
 
14
16
  // Plan_60 D4/D4c: preparation takes seconds; expiry must refuse rather than permit double billing.
15
17
  export const ORDER_CLAIM_TIMING = Object.freeze({ waitMs: 60_000, retryMs: 100, answerTimeoutMs: 1_000 });
16
18
 
17
19
  export function orderClaimAddress(projectRunsRoot, orderId, platform = process.platform) {
18
20
  if (typeof orderId !== 'string' || orderId.length === 0) {
19
- throw new TypeError('orderId must be a non-empty string');
21
+ throw new TypeError(orderDiagnosticMessage('order-claim-input-valid', { violation: 'order-id' }));
20
22
  }
21
23
  // Plan_60 A4 r5: owner matching is case-sensitive; retries must retain the exact order id.
22
24
  const digest = directoryDigest(projectRunsRoot, { suffix: orderId });
23
- if (digest === null) throw new Error(`project runs folder does not exist: ${projectRunsRoot}`);
25
+ if (digest === null) throw new Error(orderDiagnosticMessage('project-runs-folder-required', { projectRunsRoot }));
24
26
  if (kernelLockStrategy(platform) === null) return null;
25
27
  return kernelLockAddress('order', digest, platform);
26
28
  }
@@ -30,7 +32,7 @@ export async function acquireOrderClaim({
30
32
  timing = ORDER_CLAIM_TIMING, createServer,
31
33
  }) {
32
34
  if (role !== 'launcher' && role !== 'worker') {
33
- throw new TypeError('role must be launcher or worker');
35
+ throw new TypeError(orderDiagnosticMessage('order-claim-input-valid', { violation: 'role' }));
34
36
  }
35
37
  const address = orderClaimAddress(projectRunsRoot, orderId, platform);
36
38
  if (address === null) return { claimed: false, unsupported: true, release: async () => {} };
@@ -60,13 +62,16 @@ export function parseClaimHolder(line) {
60
62
  // Plan_60 D4/D4c: a holder answer diagnoses contention but cannot authorize anything.
61
63
  export function orderClaimHolderText(holder) {
62
64
  return holder === null
63
- ? 'a process that did not answer (unverified)'
64
- : `${holder.role} pid ${holder.pid} since ${holder.acquiredAt}`;
65
+ ? orderRuleMessage('order-launch-exclusive', { variant: 'holder-unverified' })
66
+ : orderRuleMessage('order-launch-exclusive', {
67
+ variant: 'holder', role: holder.role, pid: holder.pid, acquiredAt: holder.acquiredAt,
68
+ });
65
69
  }
66
70
 
67
71
  export function orderClaimBusyText(orderId, holder) {
68
- return `order id "${orderId}" is being launched by ${orderClaimHolderText(holder)}; repeat the same command later — `
69
- + 'it attaches to that run instead of starting another. The run folder was not created; quota was not spent.';
72
+ return [orderRuleMessage('order-launch-exclusive', {
73
+ variant: 'busy', orderId, holderDescription: orderClaimHolderText(holder),
74
+ }), orderDiagnosticMessage('run-not-created')].join(' ');
70
75
  }
71
76
 
72
77
  /**
@@ -79,6 +84,7 @@ export function unclaimedRaceRefusal({ projectRunsRoot, repoRoot, slug, taskHash
79
84
  const appeared = chain.find((run) => !before.has(run)
80
85
  && readJson(path.join(projectRunsRoot, run, 'status.json'))?.order_id === orderId);
81
86
  if (!appeared) return null;
82
- return `another launch of order id "${orderId}" registered ${appeared} while this one was preparing; `
83
- + 'repeat the same command — it attaches to that run. The run folder was not created; quota was not spent.';
87
+ return [orderRuleMessage('order-launch-exclusive', {
88
+ variant: 'race', orderId, run: appeared,
89
+ }), orderDiagnosticMessage('run-not-created')].join(' ');
84
90
  }
@@ -1,5 +1,6 @@
1
1
  /** Normalises one order's values into runner options, whatever channel carried them (Plan_63 D5). */
2
2
  import path from 'node:path';
3
+ import { orderRuleMessage } from '../order-rules.mjs';
3
4
 
4
5
  export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
5
6
  const options = { orderId: (order.get('order id') ?? '').trim() };
@@ -10,7 +11,7 @@ export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
10
11
  options.effort = order.get('effort');
11
12
  if (!options.effort || /\s/.test(options.effort)) {
12
13
  problems.push({ label: 'effort',
13
- reason: `must be a non-empty single word with no whitespace; got ${JSON.stringify(options.effort)}` });
14
+ reason: orderRuleMessage('effort-value', { value: options.effort }) });
14
15
  }
15
16
  }
16
17
  options.repo = path.resolve(order.get('repository') || cwd);
@@ -18,7 +19,7 @@ export function orderOptions(agentType, order, { cwd = process.cwd() } = {}) {
18
19
  // Plan_29 incident: a generic slug let an honest order inherit a days-old chain.
19
20
  if (!/[A-Za-z0-9]/.test(options.slug)) {
20
21
  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
+ reason: orderRuleMessage('run-slug', { value: options.slug }) });
22
23
  }
23
24
  options.changeset = order.get('changeset') || 'uncommitted';
24
25
  const declaredScopePatterns = (order.get('scope') || '').split(',').map((p) => p.trim()).filter(Boolean);
@@ -1,6 +1,7 @@
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
3
  import { orderInputName } from '../order-schema.mjs';
4
+ import { orderRuleMessage } from '../order-rules.mjs';
4
5
 
5
6
  const byStartThenName = (left, right) => {
6
7
  const leftAt = String(left.status.started_at || '');
@@ -34,14 +35,15 @@ export function ownerPredatesHashScheme(owner) {
34
35
  }
35
36
 
36
37
  export function orderOwnerConflictText(owner, ownerDir, orderId) {
37
- const identity = `Order id collision: ${JSON.stringify(String(orderId ?? ''))} already belongs to run folder ${ownerDir} ` +
38
- `(slug ${owner.status.slug}, started_at ${owner.status.started_at})`;
39
38
  // Plan_75 D5, 2026-10-03: an old hash cannot prove that a repeated order is a different task.
40
39
  if (ownerPredatesHashScheme(owner)) {
41
- return `${identity}, recorded before the task header (hash scheme ${TASK_HASH_SCHEME}), so its task cannot be compared. ` +
42
- `Its answer is already on disk in that folder (codex-bridge read "${ownerDir}"). ` +
43
- 'Another pass of that order needs a continue:/retry: header line; a new task needs a new order id.';
40
+ return orderRuleMessage('order-id-owner', {
41
+ violation: 'predates-hash-scheme', orderId, ownerDir,
42
+ slug: owner.status.slug, startedAt: owner.status.started_at, hashScheme: TASK_HASH_SCHEME,
43
+ });
44
44
  }
45
- return `${identity} with a different task. Use a new order id (${orderInputName('order id')}), or a continue:/retry: header line ` +
46
- 'if this is another pass of that order.';
45
+ return orderRuleMessage('order-id-owner', {
46
+ violation: 'different-task', orderId, ownerDir,
47
+ slug: owner.status.slug, startedAt: owner.status.started_at, labelName: orderInputName('order id'),
48
+ });
47
49
  }
@@ -15,6 +15,8 @@ import { continuationRefusal, retryRefusal, readyGrantLines } from './continuati
15
15
  import { advisorTaskOrRefuse } from './advise-carry.mjs';
16
16
  import { scopeRunRefusal } from './preflight.mjs';
17
17
  import { orderInputName } from '../order-schema.mjs';
18
+ import { orderRuleMessage } from '../order-rules.mjs';
19
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
18
20
 
19
21
  export async function passGate({ opts, taskText, header, projectRunsRoot, repoRoot, beforeWait }) {
20
22
  // A second pass at the same task is a real need — after a timeout or a LIMIT the work has
@@ -30,10 +32,9 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
30
32
  const grant = header.grant;
31
33
  // OW-040, 2026-09-30: only a retry repeats failed scope without spending advise's continuation.
32
34
  if (opts.agent === 'codex-advisor' && opts.phase === 'scope' && opts.continue && grant?.kind !== 'retry') {
33
- die(`codex-advisor ${orderInputName('phase')} scope refuses ${orderInputName('continue')}: a scope pass is never continued, and continuing one ` +
34
- "spends the order's single continuation that its advise phase needs. Action: repeat the scope under a new order id " +
35
- `without ${orderInputName('continue')} and without a continue: grant, then run advise as that order's continuation; a ` +
36
- 'scope run that failed is repeated with a `retry:` grant instead. The run folder was not created; quota was not spent.');
35
+ die([orderRuleMessage('advisor-scope-continuation', {
36
+ phaseLabel: orderInputName('phase'), continueLabel: orderInputName('continue'),
37
+ }), orderDiagnosticMessage('run-not-created')].join(' '));
37
38
  }
38
39
  let continuationGrant = grant?.kind === 'continue' ? grant : null;
39
40
  const retryOf = grant?.kind === 'retry' ? grant.run : null;
@@ -60,8 +61,9 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
60
61
  // A run written before phases existed has none; for every agent but the advisor that is 'default'.
61
62
  const phaseOf = (value) => String(value ?? '').trim() || 'default';
62
63
  if (namedStatus.agent !== opts.agent || phaseOf(namedStatus.phase) !== phaseOf(opts.phase)) {
63
- die(`a retry repeats the named run's own agent and phase: ${namedStatus.agent}/${namedStatus.phase}. ` +
64
- 'The run folder was not created; quota was not spent.');
64
+ die([orderRuleMessage('retry-pass-identity', {
65
+ agent: namedStatus.agent, phase: namedStatus.phase,
66
+ }), orderDiagnosticMessage('run-not-created')].join(' '));
65
67
  }
66
68
  // Plan_75 D4, TradeForge capacity incident: retry advise against its original successful scope.
67
69
  continuationGrant = String(namedStatus.continued_from ?? '').trim()
@@ -93,18 +95,12 @@ export async function passGate({ opts, taskText, header, projectRunsRoot, repoRo
93
95
  const last = startedChain[startedChain.length - 1];
94
96
  const lastSlug = String(readJson(path.join(projectRunsRoot, last, 'status.json'))?.slug || '');
95
97
  const renamed = lastSlug && lastSlug.toLowerCase() !== String(opts.slug).toLowerCase();
96
- die(
97
- `${orderInputName('continue')} is required: ${
98
- renamed
99
- ? `this task already ran in this repository under the name “${lastSlug}”`
100
- : `runs for task “${opts.slug}” already exist in this repository`
101
- } (${startedChain.length}), latest: ${path.join(projectRunsRoot, last)}. ` +
102
- 'A repeat run is allowed, but the orchestrator decides, not the runner: it read the ' +
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. ` +
105
- `${readyGrantLines(projectRunsRoot, startedChain)} ` +
106
- 'The run folder was not created; quota was not spent.',
107
- );
98
+ die([orderRuleMessage('repeat-task-grant-required', {
99
+ violation: renamed ? 'renamed' : 'existing',
100
+ continueLabel: orderInputName('continue'), slugLabel: orderInputName('slug'),
101
+ slug: renamed ? lastSlug : opts.slug, count: startedChain.length, latest: path.join(projectRunsRoot, last),
102
+ hints: readyGrantLines(projectRunsRoot, startedChain),
103
+ }), orderDiagnosticMessage('run-not-created')].join(' '));
108
104
  }
109
105
 
110
106
  return { taskHash, chain, startedChain, continuationGrant, advisorTask, retryOf };
@@ -6,36 +6,34 @@
6
6
  import path from 'node:path';
7
7
  import fs from 'node:fs';
8
8
  import { advisorTaskRefusal } from '../meta/advisor-task.mjs';
9
- import { unavailableRows } from '../meta/reply.mjs';
9
+ import { unavailableRows } from '../meta/unavailable-rows.mjs';
10
10
  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
14
  import { orderInputName } from '../order-schema.mjs';
15
15
  import { retiredPathRefusal, runsRootResolution } from './runs-root.mjs';
16
+ import { orderRuleMessage } from '../order-rules.mjs';
17
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
16
18
 
17
19
  /** Plan_60 D2: only conclusive evidence says UNAVAILABLE; a PATH miss is inconclusive (Plan_78 D5). */
18
20
  export async function codexAvailabilityRefusal({ probe = probeCodexAvailability } = {}) {
19
21
  const result = await probe();
20
22
  if (result.state === 'available') return null;
21
23
  const unavailable = result.state === 'logged-out';
22
- const rows = unavailable ? unavailableRows(result.detail) : result.pathMiss === true ? [
23
- `Codex CLI readiness unconfirmed: ${result.detail}`,
24
- 'Do not skip this bridge: check codex --version for up to about 2 minutes; when it answers, repeat the same order exactly once; if it does not answer, hand the order to the next executor and name that in the summary.',
25
- ] : [
26
- `Codex CLI unavailable: ${result.detail}`,
27
- 'Operator check: codex --version (and codex login if authorization is rejected)',
28
- ];
24
+ const rows = unavailable ? unavailableRows(result.detail) : [orderDiagnosticMessage(
25
+ result.pathMiss === true ? 'codex-readiness-unconfirmed' : 'codex-unavailable', { detail: result.detail },
26
+ )];
29
27
  return {
30
28
  unavailable,
31
- text: [...rows, 'The run folder was not created; quota was not spent.'].join('\n'),
29
+ text: [...rows, orderDiagnosticMessage('run-not-created')].join('\n'),
32
30
  };
33
31
  }
34
32
 
35
33
  /** Plan_59 D5/D6: settle design authority before any probe can spend quota. */
36
34
  export function taskPreflight({ agent, taskText, header, resolution = runsRootResolution() }) {
37
35
  const freeRefusal = (reason) => ({
38
- refusal: `${reason}\nThe run folder was not created; quota was not spent.`,
36
+ refusal: [reason, orderDiagnosticMessage('run-not-created')].join('\n'),
39
37
  });
40
38
  if (agent === 'codex-advisor') {
41
39
  const reason = advisorTaskRefusal(taskText);
@@ -64,11 +62,7 @@ export function taskPreflight({ agent, taskText, header, resolution = runsRootRe
64
62
  }
65
63
  }
66
64
  }
67
- return freeRefusal('codex-build requires the task file to start with the header line `advice: <value>`, where the ' +
68
- 'value is mechanical | revert | docs-only | test-only ' +
69
- 'or an absolute path to an existing advisor run directory whose meta.json says agent codex-advisor, ' +
70
- 'phase advise, status OK. ' +
71
- 'An order that invents a construction needs a second opinion first; only work with no design choice may skip it.');
65
+ return freeRefusal(orderRuleMessage('build-advice-required'));
72
66
  }
73
67
 
74
68
  /** Plan_59 D14: an advice pass may continue only a completed, successful advisor scope run. */
@@ -81,44 +75,40 @@ export function scopeRunRefusal({ agent, phase, runsRoot, grantRun }) {
81
75
  meta = null;
82
76
  }
83
77
  const failed = [
84
- meta?.agent === 'codex-advisor' ? null : "agent must be 'codex-advisor'",
85
- meta?.phase === 'scope' ? null : "phase must be 'scope'",
86
- meta?.status === 'OK' ? null : "status must be 'OK'",
78
+ meta?.agent === 'codex-advisor' ? null : orderRuleMessage('advise-scope-run-required', { violation: 'agent' }),
79
+ meta?.phase === 'scope' ? null : orderRuleMessage('advise-scope-run-required', { violation: 'phase' }),
80
+ meta?.status === 'OK' ? null : orderRuleMessage('advise-scope-run-required', { violation: 'status' }),
87
81
  ].filter(Boolean);
88
82
  if (!failed.length) return null;
89
- return `continued run meta.json failed ${failed.join(', ')}. The run folder was not created; quota was not spent.`;
83
+ return [orderRuleMessage('advise-scope-run-required', {
84
+ violation: 'meta', failed: failed.join(', '),
85
+ }), orderDiagnosticMessage('run-not-created')].join(' ');
90
86
  }
91
87
 
92
88
  /** D2: phase mistakes must refuse before even the sandbox probe can spend quota. */
93
89
  export function resolveRunPhase({ agent, phase, continue: isContinue }, budgets) {
94
90
  // Plan_59 D7: phase 2 must retain the scope run's predictions before quota is spent.
95
91
  if (agent === 'codex-advisor' && phase === 'advise' && !isContinue) {
96
- 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 ` +
97
- 'so it can settle the risks phase 1 predicted. The run folder was not created; quota was not spent.');
92
+ die([orderRuleMessage('advisor-advise-continuation', {
93
+ phaseLabel: orderInputName('phase'), continueLabel: orderInputName('continue'),
94
+ }), orderDiagnosticMessage('run-not-created')].join(' '));
98
95
  }
99
96
  const phases = budgets[agentRole(agent)];
100
97
  const names = Object.keys(phases);
101
- if (phase === undefined && names.length === 1 && names[0] === 'default') return 'default';
98
+ if (phase === undefined && names.length === 1 && names[0] === 'default') return names[0];
102
99
  if (phase !== undefined && Object.hasOwn(phases, phase)) return phase;
103
- die(`${phase === undefined ? `${orderInputName('phase')} is required` : `undeclared ${orderInputName('phase')} ${JSON.stringify(phase)}`} ` +
104
- `for ${agent}. Action: pass ${orderInputName('phase')} <name>; allowed phases: ${names.join(', ')}. ` +
105
- 'The run folder was not created; quota was not spent.');
100
+ die([orderRuleMessage('run-phase-required', {
101
+ violation: phase === undefined ? 'missing' : 'undeclared', phaseLabel: orderInputName('phase'), phase, agent, names,
102
+ }), orderDiagnosticMessage('run-not-created')].join(' '));
106
103
  }
107
104
 
108
105
  export function preflightRefusal({ agent, projectRunsRoot, repoRoot }) {
109
106
  // Two writing runs share one tree: the second snapshot would include the first one's edits.
110
107
  const busy = agent === 'codex-build' ? activeRunDetails(projectRunsRoot, repoRoot) : null;
111
108
  if (busy) {
112
- const identityNote = busy.identity === 'unverified'
113
- ? '; process identity could not be confirmed'
114
- : '';
115
- return [
116
- `run ${busy.run} is already active for this repository; two writing runs in one tree are prohibited${identityNote}`,
117
- `Active run: ${path.join(projectRunsRoot, busy.run)}`,
118
- // One sentence, the same one every other pre-flight refusal ends with. The older wording
119
- // said the identical thing twice, which is how two copies of a rule start to drift.
120
- 'The run folder was not created; quota was not spent.',
121
- ].join('\n');
109
+ return [orderRuleMessage('writing-run-exclusive', {
110
+ run: busy.run, activePath: path.join(projectRunsRoot, busy.run), identity: busy.identity,
111
+ }), orderDiagnosticMessage('run-not-created')].join('\n');
122
112
  }
123
113
 
124
114
  return null;
@@ -3,6 +3,8 @@
3
3
  * Plan_60 D1: only the orderer knows whether a question concerns handed startup context or code;
4
4
  * the scout cannot choose its own kind, and startup context currently fails without a command.
5
5
  */
6
+ import { orderRuleMessage } from '../order-rules.mjs';
7
+
6
8
  export const CONTEXT_ONLY_MARKER = '[context-only]';
7
9
  export const QUESTION_KIND = Object.freeze({
8
10
  STARTUP_CONTEXT: 'startup-context',
@@ -14,13 +16,13 @@ export function questionKindRefusal(texts = []) {
14
16
  const tokens = text.matchAll(/\[\s*context[\s_-]*only\s*\]/gi);
15
17
  for (const token of tokens) {
16
18
  if (token.index !== 0 || token[0] !== CONTEXT_ONLY_MARKER) {
17
- return `Q${index + 1} has an invalid context-only marker in ${JSON.stringify(text)}`;
19
+ return orderRuleMessage('question-context-marker', { index, text });
18
20
  }
19
21
  }
20
22
  if (text.startsWith(CONTEXT_ONLY_MARKER)) {
21
23
  const question = text.slice(CONTEXT_ONLY_MARKER.length);
22
24
  if (!/^\s/.test(question) || !question.trim()) {
23
- return `Q${index + 1} has an invalid context-only marker in ${JSON.stringify(text)}`;
25
+ return orderRuleMessage('question-context-marker', { index, text });
24
26
  }
25
27
  }
26
28
  }
@@ -11,6 +11,8 @@ import { writeHomeJsonAtomic } from '../atomic-json.mjs';
11
11
  import { stateDirWriter } from '../home-write.mjs';
12
12
  import { parseJsonText } from '../json-file.mjs';
13
13
  import { normalizeRepoPath } from './project-dir.mjs';
14
+ import { orderRuleMessage } from '../order-rules.mjs';
15
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
14
16
 
15
17
  export const RUNS_MOVE_RECORD = 'runs-root.json';
16
18
 
@@ -21,25 +23,31 @@ export function readRunsMoveRecord(stateDir) {
21
23
  text = fs.readFileSync(file, 'utf8');
22
24
  } catch (error) {
23
25
  if (error.code === 'ENOENT') return null;
24
- throw new Error(`Cannot read runs move record ${file}: ${error.message}`, { cause: error });
26
+ throw new Error(orderDiagnosticMessage('runs-move-record-readable', {
27
+ file, message: error.message,
28
+ }), { cause: error });
25
29
  }
26
30
  let record;
27
31
  try {
28
32
  record = parseJsonText(file, text);
29
33
  } catch (error) {
30
- throw new Error(`Invalid runs move record ${file}: ${error.message}`, { cause: error });
34
+ throw new Error(orderDiagnosticMessage('runs-move-record-invalid', {
35
+ file, violation: 'parse', detail: error.message,
36
+ }), { cause: error });
31
37
  }
32
- const invalid = (defect) => { throw new Error(`Invalid runs move record ${file}: ${defect}`); };
33
- if (record?.version !== 1) invalid('version must be 1');
38
+ const invalid = (violation, index) => {
39
+ throw new Error(orderDiagnosticMessage('runs-move-record-invalid', { file, violation, index }));
40
+ };
41
+ if (record?.version !== 1) invalid('version');
34
42
  if (!Array.isArray(record.retired) || record.retired.length === 0) {
35
- invalid('retired must be a non-empty array');
43
+ invalid('retired-array');
36
44
  }
37
45
  for (const [index, entry] of record.retired.entries()) {
38
46
  if (typeof entry?.root !== 'string' || !path.isAbsolute(entry.root)) {
39
- invalid(`retired[${index}].root must be an absolute path`);
47
+ invalid('retired-root', index);
40
48
  }
41
49
  if (typeof entry.movedAt !== 'string' || !Number.isFinite(Date.parse(entry.movedAt))) {
42
- invalid(`retired[${index}].movedAt must be a parseable ISO time`);
50
+ invalid('retired-time', index);
43
51
  }
44
52
  }
45
53
  return record;
@@ -63,8 +71,9 @@ export function retiredRootOf(candidate, retiredRoots) {
63
71
 
64
72
  export function retiredRootRefusal({ candidate, retired, destination }) {
65
73
  const equivalent = retired.suffix ? path.join(destination, retired.suffix) : destination;
66
- return `Run records moved from ${retired.root} to ${destination}. Equivalent path: ${equivalent}. ` +
67
- 'Update advice: or pass this new path explicitly. No path was remapped.';
74
+ return orderRuleMessage('advice-retired-root', {
75
+ candidate, retiredRoot: retired.root, destination, equivalent,
76
+ });
68
77
  }
69
78
 
70
79
  /**
@@ -13,6 +13,7 @@ import path from 'node:path';
13
13
  import { resolveBrandHome } from '../brand-home.mjs';
14
14
  import { RUN_RECORDS_DIR } from '../home-registry.mjs';
15
15
  import { readRunsMoveRecord, retiredRootOf, retiredRootRefusal } from './retired-roots.mjs';
16
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
16
17
 
17
18
  export function runsRootResolution({ env = process.env, homedir = os.homedir() } = {}) {
18
19
  const brandHome = resolveBrandHome({ env, homedir });
@@ -50,7 +51,6 @@ export function retiredPathRefusal(candidate, resolution = runsRootResolution())
50
51
 
51
52
  export function staleOverrideRefusal(resolution = runsRootResolution()) {
52
53
  if (!resolution.staleOverride) return null;
53
- return retiredRootRefusal({ candidate: resolution.root, retired: resolution.staleOverride,
54
- destination: resolution.homeRoot }) + '\n' +
55
- 'CODEX_RUNS_ROOT points under a retired runs root; remove it or set it to the new location.';
54
+ return [retiredRootRefusal({ candidate: resolution.root, retired: resolution.staleOverride,
55
+ destination: resolution.homeRoot }), orderDiagnosticMessage('stale-runs-root-override')].join('\n');
56
56
  }
@@ -1,13 +1,13 @@
1
1
  /** Owns read-only sandbox log evidence and repair formatting after 2026-10-10 TradeForge orders exposed only "setup refresh had errors". */
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
4
5
 
5
6
  const LOG_LIMIT = 64 * 1024;
6
7
  const SETUP_LIMIT = 8 * 1024;
7
8
  const ERROR_LINE = /setup error|setup refresh completed with errors|failed|error/i;
8
9
  // A successful refresh logs `errors=[]`; on the 2026-10-10 log five of them pushed the real error out.
9
10
  const CLEAN_REFRESH = /errors=\[\]/;
10
- const NO_REPAIR = 'No verified repair signature; inspect this log.';
11
11
 
12
12
  // Plan_78 A1: a repair needs all incident fragments on one fresh line, not a generic error word.
13
13
  const SIGNATURES = Object.freeze([
@@ -16,16 +16,18 @@ const SIGNATURES = Object.freeze([
16
16
  observed: Object.freeze({ date: '2026-10-10', codexCli: '0.162.1' }),
17
17
  predicate: (line) => ['runtime read/execute validation failed',
18
18
  'open ACL target for root-only update', 'os error 32'].every((fragment) => line.includes(fragment)),
19
- summary: 'A file under the Codex runtimes folder is held open by another process, so sandbox setup cannot update its access rights (openai/codex #51634).',
20
- repair: () => 'Find and close the process holding the file named in the line (on 2026-10-10, the Codex desktop app\'s computer-use helper), then rerun codex-bridge doctor; on Codex CLI 0.162.x the proven workaround is npm install -g @openai/codex@0.160.1.',
19
+ summary: orderDiagnosticMessage('sandbox-known-failure-runtime-file-locked', { part: 'summary' }),
20
+ repair: () => orderDiagnosticMessage('sandbox-known-failure-runtime-file-locked', { part: 'repair' }),
21
21
  }),
22
22
  Object.freeze({
23
23
  id: 'deny-read-state-corrupt',
24
24
  observed: Object.freeze({ date: '2026-09-16' }),
25
25
  predicate: (line) => ['deny_read_acl_state.json', 'expected value at line 1 column 1']
26
26
  .every((fragment) => line.includes(fragment)),
27
- summary: 'The sandbox state file is empty or corrupt.',
28
- repair: (codexHome) => `Back up and rename ${path.join(codexHome, '.sandbox', 'deny_read_acl_state.json')}, then rerun codex-bridge doctor; the helper recreates it.`,
27
+ summary: orderDiagnosticMessage('sandbox-known-failure-deny-read-state-corrupt', { part: 'summary' }),
28
+ repair: (codexHome) => orderDiagnosticMessage('sandbox-known-failure-deny-read-state-corrupt', {
29
+ part: 'repair', file: path.join(codexHome, '.sandbox', 'deny_read_acl_state.json'),
30
+ }),
29
31
  }),
30
32
  ]);
31
33
 
@@ -94,7 +96,7 @@ function readSetupError(file, folderState) {
94
96
  return {
95
97
  state: result.state,
96
98
  text: result.state === 'read' ? result.text : result.state === 'missing' ? null
97
- : `Unknown: unable to read ${file}.`,
99
+ : orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'unreadable', file }),
98
100
  };
99
101
  }
100
102
 
@@ -127,7 +129,7 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
127
129
  if (result.state === 'missing') continue;
128
130
  sources.push(file);
129
131
  if (result.state === 'unknown') {
130
- unknown.push(`Unknown: unable to read ${file}.`);
132
+ unknown.push(orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'unreadable', file }));
131
133
  continue;
132
134
  }
133
135
  if (baseline !== null && result.bytes > 0) {
@@ -135,7 +137,7 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
135
137
  appended.push(result.text);
136
138
  }
137
139
  recent.push({ file, mtime: result.mtime });
138
- if (baseline === null) unknown.push(`Unknown: pre-probe size unavailable for ${file}.`);
140
+ if (baseline === null) unknown.push(orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'baseline', file }));
139
141
  }
140
142
 
141
143
  let text = appended.join('\n');
@@ -143,7 +145,7 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
143
145
  const newest = recent.sort((a, b) => b.mtime - a.mtime || b.file.localeCompare(a.file))[0];
144
146
  const fallback = readRange(newest.file, 0, LOG_LIMIT, true, folderState);
145
147
  if (fallback.state === 'read') text = fallback.text;
146
- else unknown.push(`Unknown: unable to read ${newest.file}.`);
148
+ else unknown.push(orderDiagnosticMessage('sandbox-evidence-unknown', { variant: 'unreadable', file: newest.file }));
147
149
  }
148
150
  const errors = text.split(/\r?\n/)
149
151
  .filter((line) => ERROR_LINE.test(line) && !CLEAN_REFRESH.test(line)).slice(-5);
@@ -166,15 +168,13 @@ export function diagnoseSandbox({ codexHome, before, now = new Date() }) {
166
168
 
167
169
  export function formatSandboxDiagnosis(diagnosis) {
168
170
  return [
169
- ...diagnosis.sources.map((source) => `Sandbox diagnosis source: ${source}`),
170
- diagnosis.attributed ? 'Sandbox log lines attributed to this probe:'
171
- : 'Recent sandbox log lines only; not attributed to this probe:',
172
- ...diagnosis.lines.map((line) => `> ${line}`),
171
+ ...diagnosis.sources.map((source) => orderDiagnosticMessage('sandbox-diagnosis-source', { source })),
172
+ orderDiagnosticMessage('sandbox-diagnosis-attribution', { attributed: diagnosis.attributed }),
173
+ ...diagnosis.lines.map((line) => orderDiagnosticMessage('sandbox-evidence-line', { line })),
173
174
  ...(diagnosis.setupError.text === null ? [] : [
174
- `setup_error.json changed since snapshot: ${diagnosis.setupError.changed === null
175
- ? 'unknown' : diagnosis.setupError.changed ? 'yes' : 'no'}`,
176
- ...diagnosis.setupError.text.split(/\r?\n/).map((line) => `> ${line}`),
175
+ orderDiagnosticMessage('sandbox-setup-change', { changed: diagnosis.setupError.changed }),
176
+ ...diagnosis.setupError.text.split(/\r?\n/).map((line) => orderDiagnosticMessage('sandbox-evidence-line', { line })),
177
177
  ]),
178
- ...(diagnosis.signature ? [diagnosis.signature.summary, diagnosis.signature.repair] : [NO_REPAIR]),
178
+ ...(diagnosis.signature ? [diagnosis.signature.summary, diagnosis.signature.repair] : [orderDiagnosticMessage('sandbox-no-repair')]),
179
179
  ];
180
180
  }