@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
@@ -1,5 +1,6 @@
1
1
  /** Checks whether this host's Codex sandbox can start a process before a run spends quota. */
2
2
  import { performance } from 'node:perf_hooks';
3
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
3
4
  import { sandboxModeFor } from './codex-args.mjs';
4
5
  import { codexSpawnSpec, spawnCaptured } from './codex-cmd.mjs';
5
6
  import { platformSandboxArgs } from './sandbox-flags.mjs';
@@ -12,15 +13,6 @@ export const SANDBOX_PROBE_MARKER = 'codex-bridge-sandbox-ok';
12
13
  // on the same host after the AppArmor repair. macOS has never been observed.
13
14
  export const PROBED_PLATFORMS = new Set(['win32', 'linux']);
14
15
 
15
- // Ubuntu 23.10+ forbids unprivileged user namespaces unless an AppArmor profile allows bwrap; that
16
- // is the ordinary state of a fresh server, not an accident like the corrupted Windows state file.
17
- const LINUX_REPAIR = [
18
- 'On Ubuntu 23.10 and later this usually means AppArmor does not let bubblewrap create a user namespace.',
19
- 'Official repair: https://learn.chatgpt.com/docs/sandboxing — the bwrap-userns-restrict AppArmor profile',
20
- '(Ubuntu 24.04) or the bubblewrap package (25.04 and later). Do not set',
21
- 'kernel.apparmor_restrict_unprivileged_userns=0: it lifts the restriction for every program on the host.',
22
- ].join(' ');
23
-
24
16
  const echoArgs = (platform) => platform === 'win32'
25
17
  ? ['cmd', '/d', '/c', 'echo', SANDBOX_PROBE_MARKER]
26
18
  : ['echo', SANDBOX_PROBE_MARKER];
@@ -28,12 +20,12 @@ const echoArgs = (platform) => platform === 'win32'
28
20
  function inconclusiveReason(result, form) {
29
21
  if (result.error) {
30
22
  return result.error.code === 'ETIMEDOUT'
31
- ? `The ${form} sandbox probe timed out.`
32
- : `The ${form} sandbox probe could not start or complete.`;
23
+ ? orderDiagnosticMessage('sandbox-probe-reason', { variant: 'timeout', form })
24
+ : orderDiagnosticMessage('sandbox-probe-reason', { variant: 'incomplete', form });
33
25
  }
34
- if (result.signal) return `The ${form} sandbox probe was terminated by ${result.signal}.`;
35
- if (result.status === 2) return `Codex rejected the ${form} sandbox probe arguments.`;
36
- if (result.status == null) return `The ${form} sandbox probe returned no exit status.`;
26
+ if (result.signal) return orderDiagnosticMessage('sandbox-probe-reason', { variant: 'signal', form, signal: result.signal });
27
+ if (result.status === 2) return orderDiagnosticMessage('sandbox-probe-reason', { variant: 'arguments', form });
28
+ if (result.status == null) return orderDiagnosticMessage('sandbox-probe-reason', { variant: 'no-status', form });
37
29
  return null;
38
30
  }
39
31
 
@@ -43,7 +35,7 @@ export async function probeSandbox({
43
35
  }) {
44
36
  if (!PROBED_PLATFORMS.has(platform)) return { outcome: 'skipped' };
45
37
  const sandbox = sandboxModeFor(agent);
46
- if (typeof repo !== 'string' || !repo) throw new Error('sandbox probe requires a repository path');
38
+ if (typeof repo !== 'string' || !repo) throw new Error(orderDiagnosticMessage('sandbox-probe-repository-required'));
47
39
  const echo = echoArgs(platform);
48
40
  const attempts = [];
49
41
  // The 2026-10-10 TradeForge refusals showed only "setup refresh had errors"; capture the
@@ -95,43 +87,42 @@ export async function probeSandbox({
95
87
  const flaggedReason = inconclusiveReason(flagged, 'flagged');
96
88
  if (flaggedReason) return finish('inconclusive', flaggedReason);
97
89
  if (flagged.status === 0 && flagged.marker) {
98
- return finish('alive', 'The Codex sandbox started a process.');
90
+ return finish('alive', orderDiagnosticMessage('sandbox-probe-reason', { variant: 'alive' }));
99
91
  }
100
92
  // D16: the marker proves a process started, so a nonzero exit cannot prove a dead sandbox.
101
93
  if (flagged.marker) {
102
- return finish('inconclusive', `The flagged sandbox probe printed the marker but exited ${flagged.status}.`);
94
+ return finish('inconclusive', orderDiagnosticMessage('sandbox-probe-reason', { variant: 'marker-exit', status: flagged.status }));
103
95
  }
104
96
 
105
97
  const control = await attempt('control', ['sandbox', '--', ...echo]);
106
98
  const controlReason = inconclusiveReason(control, 'control');
107
99
  if (controlReason) return finish('inconclusive', controlReason);
108
100
  if (control.marker) {
109
- return finish('inconclusive', 'This version of Codex no longer accepts the package sandbox flags.');
101
+ return finish('inconclusive', orderDiagnosticMessage('sandbox-probe-reason', { variant: 'flags' }));
110
102
  }
111
103
 
112
104
  const version = await attempt('version', ['--version']);
113
105
  if (version.error || version.signal || version.status !== 0) {
114
106
  // codexAvailabilityRefusal() owns the refusal for an unavailable CLI, earlier in the same pre-flight pass.
115
- return finish('inconclusive', 'Codex CLI is unavailable.');
107
+ return finish('inconclusive', orderDiagnosticMessage('sandbox-probe-reason', { variant: 'unavailable' }));
116
108
  }
117
- return finish('dead', 'The Codex sandbox on this host cannot start a process.');
109
+ return finish('dead', orderDiagnosticMessage('sandbox-probe-reason', { variant: 'dead' }));
118
110
  }
119
111
 
120
112
  export function sandboxRefusal(result, platform = process.platform) {
121
- const control = ['codex', 'sandbox', '--', ...echoArgs(platform)].join(' ');
113
+ const controlCommand = ['codex', 'sandbox', '--', ...echoArgs(platform)].join(' ');
122
114
  const stderr = result.attempts
123
115
  .filter(({ form }) => form === 'flagged' || form === 'control')
124
- .map(({ form, stderrTail }) =>
125
- `${form} stderr:\n> ${(stderrTail || '(empty)').replace(/\r\n?/g, '\n').replace(/\n/g, '\n> ')}`);
116
+ .map(({ form, stderrTail }) => orderDiagnosticMessage('sandbox-stderr-evidence', { form, stderrTail }));
126
117
  // Only a dead result is refused, and its reason already is the headline; printing both said it twice.
127
118
  return [
128
119
  result.reason,
129
- 'Repair the host sandbox; do not rewrite or retry the order.',
130
- ...(platform === 'linux' ? [LINUX_REPAIR] : []),
131
- `Operator check (run from the repository directory): ${control}`,
120
+ orderDiagnosticMessage('sandbox-refusal-repair'),
121
+ ...(platform === 'linux' ? [orderDiagnosticMessage('sandbox-linux-repair')] : []),
122
+ orderDiagnosticMessage('sandbox-operator-check', { command: controlCommand }),
132
123
  ...stderr,
133
124
  ...(result.diagnosis && typeof result.diagnosis === 'object'
134
125
  ? [formatSandboxDiagnosis(result.diagnosis).join('\n')] : []),
135
- 'The run folder was not created; quota was not spent.',
126
+ orderDiagnosticMessage('run-not-created'),
136
127
  ].join('\n\n');
137
128
  }
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
  import { listRepositoryPaths } from './git-paths.mjs';
10
10
  import { globToRegExp, normalizePath, SERVICE_RE } from '../meta/paths.mjs';
11
11
  import { orderInputName } from '../order-schema.mjs';
12
+ import { orderRuleMessage } from '../order-rules.mjs';
12
13
 
13
14
  // Directories a self-walk must never descend into. Only reached when the repository has no git:
14
15
  // with git present the file list comes from the repository itself, which already knows what is
@@ -29,41 +30,41 @@ function structuralRefusal(repoRoot, pattern) {
29
30
  // Plan_73 D7: scope lists store one pattern per line; literal line breaks cannot round-trip.
30
31
  if (/[\n\r]/.test(pattern)) {
31
32
  return {
32
- reason: 'contains a newline or carriage return',
33
- action: 'use a glob instead of a literal path containing a line break',
33
+ reason: orderRuleMessage('scope-pattern-shape', { violation: 'line-break', part: 'reason' }),
34
+ action: orderRuleMessage('scope-pattern-shape', { violation: 'line-break', part: 'action' }),
34
35
  };
35
36
  }
36
37
  if (absolutePattern(pattern)) {
37
38
  return {
38
- reason: 'is an absolute or drive-qualified path',
39
- action: 'use a path relative to the repository root with forward slashes',
39
+ reason: orderRuleMessage('scope-pattern-shape', { violation: 'absolute', part: 'reason' }),
40
+ action: orderRuleMessage('scope-pattern-shape', { violation: 'absolute', part: 'action' }),
40
41
  };
41
42
  }
42
43
  if (pattern.includes('\\')) {
43
44
  return {
44
- reason: 'uses backslash separators',
45
- action: 'replace backslashes with forward slashes',
45
+ reason: orderRuleMessage('scope-pattern-shape', { violation: 'backslash', part: 'reason' }),
46
+ action: orderRuleMessage('scope-pattern-shape', { violation: 'backslash', part: 'action' }),
46
47
  };
47
48
  }
48
49
  if (pattern.split('/').some((segment) => segment === '..')) {
49
50
  return {
50
- reason: 'contains a parent-directory (..) segment',
51
- action: 'remove the .. segment and keep the path relative to the repository root',
51
+ reason: orderRuleMessage('scope-pattern-shape', { violation: 'parent-directory', part: 'reason' }),
52
+ action: orderRuleMessage('scope-pattern-shape', { violation: 'parent-directory', part: 'action' }),
52
53
  };
53
54
  }
54
55
  // Plan_59 S1: 2026-09-22_235724_plan59-d1-advisor-docs-surface spent 22 minutes on a
55
56
  // service-directory scope the verdict could never authorise. Refuse it before a run exists.
56
57
  if (SERVICE_RE.test(normalizePath(pattern))) {
57
58
  return {
58
- reason: 'lies inside a service directory that no scope can authorise',
59
- action: 'name the file outside the service directory, or make that edit yourself',
59
+ reason: orderRuleMessage('scope-pattern-shape', { violation: 'service-directory', part: 'reason' }),
60
+ action: orderRuleMessage('scope-pattern-shape', { violation: 'service-directory', part: 'action' }),
60
61
  };
61
62
  }
62
63
  // Plan_58 D7: 2026-09-19_191402_ports-infra spent a run on a bare scope-new directory,
63
64
  // which the verdict could only match as a file. Refuse that intent before spending a run.
64
65
  if (!pattern || /[*?]/.test(pattern)) return null;
65
- const reason = 'names a directory rather than a file';
66
- const action = `write ${pattern.replace(/\/$/, '')}/** for everything inside it, or name the file itself`;
66
+ const reason = orderRuleMessage('scope-pattern-shape', { violation: 'directory', part: 'reason' });
67
+ const action = orderRuleMessage('scope-pattern-shape', { violation: 'directory', part: 'action', pattern });
67
68
  if (pattern.endsWith('/')) return { reason, action };
68
69
 
69
70
  let entry;
@@ -76,7 +77,7 @@ function structuralRefusal(repoRoot, pattern) {
76
77
  if (!entry && !pattern.split('/').at(-1).includes('.')) {
77
78
  return {
78
79
  reason,
79
- action: `write ${pattern}/** if it is a directory, or declare the new file by its own name with an extension; a new extensionless file is declared through its directory`,
80
+ action: orderRuleMessage('scope-pattern-shape', { violation: 'new-directory', part: 'action', pattern }),
80
81
  };
81
82
  }
82
83
  return null;
@@ -88,16 +89,18 @@ function noMatchRefusal(pattern, repoRoot, gitListed) {
88
89
  if (gitListed === false) {
89
90
  return {
90
91
  pattern,
91
- reason: `does not match any existing path under ${repoRoot}; ` +
92
- 'git could not list that folder as a repository, so it was walked instead',
93
- action: `start the run from the repository root (cd into it, or pass ${orderInputName('repository')} <repository root>); ` +
94
- 'if this folder is the intended one, correct the pattern',
92
+ reason: orderRuleMessage('scope-pattern-match', { violation: 'not-repository', part: 'reason', repoRoot }),
93
+ action: orderRuleMessage('scope-pattern-match', {
94
+ violation: 'not-repository', part: 'action', labelName: orderInputName('repository'),
95
+ }),
95
96
  };
96
97
  }
97
98
  return {
98
99
  pattern,
99
- reason: 'does not match any existing path in the repository',
100
- action: `correct the pattern or declare an intentionally new path with ${orderInputName('scope new')}`,
100
+ reason: orderRuleMessage('scope-pattern-match', { violation: 'missing', part: 'reason' }),
101
+ action: orderRuleMessage('scope-pattern-match', {
102
+ violation: 'missing', part: 'action', labelName: orderInputName('scope new'),
103
+ }),
101
104
  };
102
105
  }
103
106
 
@@ -193,8 +196,10 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
193
196
  return {
194
197
  pattern: required[0],
195
198
  label: 'scope',
196
- reason: `could not parse the pattern: ${error.message}`,
197
- action: 'correct the pattern and retry',
199
+ reason: orderRuleMessage('scope-pattern-match', {
200
+ violation: 'invalid-glob', part: 'reason', message: error.message,
201
+ }),
202
+ action: orderRuleMessage('scope-pattern-match', { violation: 'invalid-glob', part: 'action' }),
198
203
  };
199
204
  }
200
205
 
@@ -206,8 +211,10 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
206
211
  return {
207
212
  pattern: required[0],
208
213
  label: 'scope',
209
- reason: error.message,
210
- action: 'rename the non-UTF-8 file using a UTF-8 file name and retry',
214
+ reason: orderRuleMessage('scope-pattern-match', {
215
+ violation: 'non-utf8', part: 'reason', message: error.message,
216
+ }),
217
+ action: orderRuleMessage('scope-pattern-match', { violation: 'non-utf8', part: 'action' }),
211
218
  };
212
219
  }
213
220
  const missing = matchers.find((matcher) => !matches.matched.has(matcher.pattern));
@@ -6,6 +6,8 @@
6
6
  * text in the file while the headingless form preserves every existing task file.
7
7
  */
8
8
 
9
+ import { orderRuleMessage } from '../order-rules.mjs';
10
+
9
11
  const SECTION_HEADING = /^ {0,3}(#{1,6})[ \t]+(.+?)[ \t]*$/;
10
12
  const LIST_ITEM = /^\s*(?:[-*]|\d+\.)[ \t]+(.+?)\s*$/;
11
13
  const SECTION_NAMES = new Map([
@@ -72,16 +74,16 @@ export function parseTaskDocument(text) {
72
74
  if (!line.trim()) continue;
73
75
  const item = LIST_ITEM.exec(line);
74
76
  if (!item) {
75
- throw new Error(`Questions section contains a non-list line: ${JSON.stringify(line.trim())}`);
77
+ throw new Error(orderRuleMessage('scout-questions', { violation: 'non-list-line', line }));
76
78
  }
77
79
  questions.push(item[1].trim());
78
80
  }
79
- if (sawQuestions && !questions.length) throw new Error('Questions section is empty');
81
+ if (sawQuestions && !questions.length) throw new Error(orderRuleMessage('scout-questions', { violation: 'empty' }));
80
82
 
81
83
  const verifyContent = verifyLines.filter((line) => line.trim());
82
- if (sawVerify && !verifyContent.length) throw new Error('Verify section is empty');
84
+ if (sawVerify && !verifyContent.length) throw new Error(orderRuleMessage('verify-line', { violation: 'empty' }));
83
85
  if (verifyContent.length > 1) {
84
- throw new Error('Verify section must contain exactly one non-empty line');
86
+ throw new Error(orderRuleMessage('verify-line', { violation: 'multiple-lines' }));
85
87
  }
86
88
 
87
89
  return {
@@ -10,6 +10,8 @@ import { die, readTaskDocument } from './args.mjs';
10
10
  import { questionKindRefusal } from './question-kind.mjs';
11
11
  import { orderFromHeader, orderInputName, renderOrderProblems } from '../order-schema.mjs';
12
12
  import { orderOptions } from './order-options.mjs';
13
+ import { orderRuleMessage, orderLabelReason } from '../order-rules.mjs';
14
+ import { orderDiagnosticMessage } from '../order-diagnostics.mjs';
13
15
 
14
16
  /**
15
17
  * OW-061: outside git the launcher keeps `repository:` as given and the project folder takes its
@@ -21,18 +23,17 @@ import { orderOptions } from './order-options.mjs';
21
23
  */
22
24
  export function repositoryRefusal(value) {
23
25
  if (!path.isAbsolute(value)) {
24
- return `must be an absolute path to the repository; got ${JSON.stringify(value)}.`;
26
+ return orderRuleMessage('repository-path', { violation: 'relative', value });
25
27
  }
26
28
  const altered = value.split(/[\\/]/).find((segment) => /[. ]$/.test(segment) && segment !== '.' && segment !== '..');
27
29
  if (altered !== undefined) {
28
- return `has a path segment ending in a dot or a space (${JSON.stringify(altered)}), which Windows strips; ` +
29
- `got ${JSON.stringify(value)}.`;
30
+ return orderRuleMessage('repository-path', { violation: 'altered-segment', value, segment: altered });
30
31
  }
31
32
  let isDirectory = false;
32
33
  try {
33
34
  isDirectory = fs.statSync(value).isDirectory();
34
35
  } catch {}
35
- return isDirectory ? null : `must name an existing directory; ${JSON.stringify(value)} is not one.`;
36
+ return isDirectory ? null : orderRuleMessage('repository-path', { violation: 'not-directory', value });
36
37
  }
37
38
 
38
39
  export function settleTaskInput(opts, { cwd = process.cwd() } = {}) {
@@ -43,11 +44,13 @@ export function settleTaskInput(opts, { cwd = process.cwd() } = {}) {
43
44
  }
44
45
  const normalized = orderOptions(opts.agent, order, { cwd });
45
46
  if (normalized.problems.length) {
46
- die(normalized.problems.map(({ label, reason }) => `${orderInputName(label)} ${reason}`).join('\n'));
47
+ die(normalized.problems.map(({ label, reason }) => orderLabelReason(orderInputName(label), reason)).join('\n'));
47
48
  }
48
49
  const repositoryReason = order.has('repository') ? repositoryRefusal(order.get('repository')) : null;
49
50
  if (repositoryReason) {
50
- die(`${orderInputName('repository')} ${repositoryReason}\nThe run folder was not created; quota was not spent.`);
51
+ die(orderRuleMessage('repository-path', {
52
+ context: 'task-input', label: orderInputName('repository'), reason: repositoryReason,
53
+ }));
51
54
  }
52
55
  Object.assign(opts, normalized.options);
53
56
  opts.continue = Boolean(header.grant);
@@ -56,23 +59,16 @@ export function settleTaskInput(opts, { cwd = process.cwd() } = {}) {
56
59
  // Still before the run folder exists and before a token of someone else's quota is touched,
57
60
  // which is where every refusal of this kind belongs.
58
61
  if (opts.agent === 'codex-scout' && !opts.questions?.length) {
59
- die(
60
- 'a sub-question is required for codex-scout: put one Markdown list item per sub-question ' +
61
- 'under a `Questions` heading in the task document. ' +
62
- 'The runner will not infer questions from the task text; no quota was spent.',
63
- );
62
+ die(orderRuleMessage('scout-questions', { violation: 'missing' }));
64
63
  }
65
64
  if (opts.agent === 'codex-scout') {
66
65
  const refusal = questionKindRefusal(opts.questions);
67
66
  if (refusal) {
68
- die(
69
- `${refusal}; the marker is exactly [context-only] followed by a space and the question; no quota was spent.`,
70
- );
67
+ die(orderRuleMessage('question-context-marker', { context: 'task-input', refusal }));
71
68
  }
72
69
  }
73
70
  if (opts.noWait && opts.continue) {
74
- die(`--no-wait cannot be combined with a ${orderInputName(header.grant.kind)} grant: ` +
75
- 'checking an existing run must never authorize a new one.');
71
+ die(orderDiagnosticMessage('no-wait-grant-conflict', { grant: orderInputName(header.grant.kind) }));
76
72
  }
77
73
  return { task, header };
78
74
  }
@@ -9,6 +9,9 @@ import { readJsonFileSync } from '../json-file.mjs';
9
9
  import { writeStatus } from '../meta/run-state.mjs';
10
10
  import { acquireOrderClaim } from './order-claim.mjs';
11
11
 
12
+ // Plan_79 B4e: admission reasons are machine-readable codes, not refusal prose.
13
+ const REASONS = Object.freeze({ claimTimeout: 'claim-timeout', closed: 'closed' });
14
+
12
15
  export async function admitWorker({ runDir, orderId, timing, platform, createServer }) {
13
16
  const claim = await acquireOrderClaim({
14
17
  projectRunsRoot: path.dirname(runDir),
@@ -19,7 +22,7 @@ export async function admitWorker({ runDir, orderId, timing, platform, createSer
19
22
  ...(createServer !== undefined ? { createServer } : {}),
20
23
  });
21
24
  try {
22
- if (claim.busy) return { admitted: false, reason: 'claim-timeout', holder: claim.holder };
25
+ if (claim.busy) return { admitted: false, reason: REASONS.claimTimeout, holder: claim.holder };
23
26
  let status = null;
24
27
  try {
25
28
  status = readJsonFileSync(path.join(runDir, 'status.json'));
@@ -27,7 +30,7 @@ export async function admitWorker({ runDir, orderId, timing, platform, createSer
27
30
  // Plan_60 A4 r4: an unreadable run cannot authorize paid work after a launcher dies.
28
31
  }
29
32
  if (status?.state !== 'running' || fs.existsSync(path.join(runDir, 'meta.json'))) {
30
- return { admitted: false, reason: 'closed', state: status?.state ?? null };
33
+ return { admitted: false, reason: REASONS.closed, state: status?.state ?? null };
31
34
  }
32
35
  // The launcher cannot know this detached process's clock origin; takeover must record the
33
36
  // worker's own identity before it starts producing the run's artifacts.
@@ -4,10 +4,11 @@
4
4
  * Unknown host versions are not evidence for a pre-start refusal.
5
5
  */
6
6
  import { modelFamily } from './model-family.mjs';
7
+ import { orderDiagnosticMessage } from './order-diagnostics.mjs';
7
8
 
8
9
  // Host contract facts, not package versions or volatile model identifiers (Plan_67 D6 (b)).
9
- export const FRONTMATTER_PRECEDENCE_VERSION = '2.1.251';
10
- export const SUBAGENT_MODEL_FORCE_VERSION = '2.1.257';
10
+ export const FRONTMATTER_PRECEDENCE_VERSION = '2.1.251'; // order-guard: not-a-refusal — host version boundary compared as data
11
+ export const SUBAGENT_MODEL_FORCE_VERSION = '2.1.257'; // order-guard: not-a-refusal — host version boundary compared as data
11
12
 
12
13
  function versionParts(value) {
13
14
  if (typeof value !== 'string' || !/^\d+\.\d+\.\d+(?:\.\d+)*$/.test(value)) return null;
@@ -37,8 +38,6 @@ export function envModelProblem({ env, hostVersion, pinFamily }) {
37
38
  const model = env.CLAUDE_CODE_SUBAGENT_MODEL;
38
39
  const family = modelFamily(model);
39
40
  if (family === null || pinFamily === null || family === pinFamily) return null;
40
- return `Order gate denied the Agent call because CLAUDE_CODE_SUBAGENT_MODEL=${JSON.stringify(model)} `
41
- + `overrides the installed dispatcher pin ${pinFamily} on host ${hostVersion}. `
42
- + `Unset CLAUDE_CODE_SUBAGENT_MODEL or set it to the pinned family ${pinFamily}. `
43
- + 'The run folder was not created; quota was not spent.';
41
+ return [orderDiagnosticMessage('subagent-model-env-pin', { model, pinFamily, hostVersion }),
42
+ orderDiagnosticMessage('run-not-created')].join(' ');
44
43
  }