@lyupro/codex-bridge 0.6.14 → 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 (62) hide show
  1. package/CHANGELOG.md +33 -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 +15 -14
  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/paths.mjs +6 -1
  25. package/src/home/lib/meta/reply.mjs +1 -10
  26. package/src/home/lib/meta/unavailable-rows.mjs +11 -0
  27. package/src/home/lib/order-diagnostics/cli-flags.mjs +56 -0
  28. package/src/home/lib/order-diagnostics/filesystem.mjs +18 -0
  29. package/src/home/lib/order-diagnostics/host-state.mjs +263 -0
  30. package/src/home/lib/order-diagnostics/internal.mjs +82 -0
  31. package/src/home/lib/order-diagnostics/run-state.mjs +58 -0
  32. package/src/home/lib/order-diagnostics.mjs +48 -0
  33. package/src/home/lib/order-rules/call.mjs +130 -0
  34. package/src/home/lib/order-rules/header-grants.mjs +175 -0
  35. package/src/home/lib/order-rules/header.mjs +206 -0
  36. package/src/home/lib/order-rules/task-document.mjs +80 -0
  37. package/src/home/lib/order-rules.mjs +77 -0
  38. package/src/home/lib/order-schema.mjs +206 -200
  39. package/src/home/lib/order-text-record.mjs +104 -0
  40. package/src/home/lib/order-values.mjs +46 -46
  41. package/src/home/lib/runner/advise-carry.mjs +9 -7
  42. package/src/home/lib/runner/args.mjs +23 -26
  43. package/src/home/lib/runner/codex-availability.mjs +10 -6
  44. package/src/home/lib/runner/continuation.mjs +158 -174
  45. package/src/home/lib/runner/launcher.mjs +19 -17
  46. package/src/home/lib/runner/order-claim.mjs +15 -9
  47. package/src/home/lib/runner/order-options.mjs +3 -2
  48. package/src/home/lib/runner/order-owner.mjs +9 -7
  49. package/src/home/lib/runner/pass-gate.mjs +14 -18
  50. package/src/home/lib/runner/preflight.mjs +25 -35
  51. package/src/home/lib/runner/prompts.mjs +2 -2
  52. package/src/home/lib/runner/question-kind.mjs +4 -2
  53. package/src/home/lib/runner/retired-roots.mjs +18 -9
  54. package/src/home/lib/runner/runs-root.mjs +3 -3
  55. package/src/home/lib/runner/sandbox-diagnosis.mjs +17 -17
  56. package/src/home/lib/runner/sandbox-probe.mjs +18 -27
  57. package/src/home/lib/runner/scope-check.mjs +30 -23
  58. package/src/home/lib/runner/task-file.mjs +6 -4
  59. package/src/home/lib/runner/task-input.mjs +12 -16
  60. package/src/home/lib/runner/worker-admission.mjs +5 -2
  61. package/src/home/lib/subagent-model-env.mjs +5 -6
  62. package/src/home/lib/task-header.mjs +159 -158
@@ -0,0 +1,175 @@
1
+ /** Plan_79 D4: owns rules for the grant labels of the task header: continue, retry and advice. */
2
+ export const RULES = Object.freeze({
3
+ 'grant-label-exclusive': Object.freeze({
4
+ data: Object.freeze([]),
5
+ topic: 'order',
6
+ rule: 'Keep exactly one grant label: continue and retry cannot both appear, even with invalid values.',
7
+ message: () => 'continue and retry cannot both be present; keep exactly one grant label',
8
+ }),
9
+ 'advice-value-shape': Object.freeze({
10
+ data: Object.freeze([]),
11
+ topic: 'order',
12
+ rule: 'Give advice a non-empty single-line value in the task header.',
13
+ message: () => 'advice value must be non-empty and single line',
14
+ }),
15
+ 'grant-value-shape': Object.freeze({
16
+ data: Object.freeze([]),
17
+ topic: 'order',
18
+ rule: 'Give continue or retry a bare run folder other than . or .. and a non-placeholder reason, ' +
19
+ 'separated by an em dash, hyphen or colon.',
20
+ message: () => 'grant must name a bare run folder (not . or ..) and a non-placeholder reason',
21
+ }),
22
+ 'continuation-grant-usable': Object.freeze({
23
+ data: Object.freeze(['violation', 'labelName', 'run', 'runsRoot', 'lastRun', 'orderId', 'spent', 'previous', 'example']),
24
+ topic: 'order',
25
+ rule: 'Grant continue to an existing last run of the task chain only when this order has not spent its ' +
26
+ 'one continuation and its previous pass has a finished verdict with no running worker; ' +
27
+ 'ask the orchestrator for the run and reason, and use a new order id for any further pass.',
28
+ message: ({ violation, labelName, run, runsRoot, lastRun, orderId, spent, previous, example, hints }) => {
29
+ if (violation === 'missing-folder' || violation === 'not-last') {
30
+ const repair = `Example: \`continue: ${example}\`. ` +
31
+ 'Action: ask the orchestrator for an explicit grant naming the run and reason; ' +
32
+ 'do not invent or reuse a continuation.';
33
+ if (violation === 'missing-folder') {
34
+ return `${labelName} is refused: the orchestrator named run “${run}”, but it does not ` +
35
+ `exist as a run folder in this project's runs directory ${runsRoot}. ` +
36
+ `${hints} ${repair} The submitted grant was not rewritten.`;
37
+ }
38
+ return `${labelName} is refused: grant ${run} is not the LAST run of this task's chain; ` +
39
+ `the current last run is ${lastRun}. A continuation is single-use: continuing the ` +
40
+ 'last run appends a later run, so the old grant stops matching by itself — no counter or new ' +
41
+ `state is used. ${hints} ${repair}`;
42
+ }
43
+ if (violation === 'spent') {
44
+ return `${labelName} is refused: order “${orderId}” already spent its allowed continuation on ${spent}. ` +
45
+ `${hints} A further pass needs a new order id from the orchestrator.`;
46
+ }
47
+ if (violation === 'unfinished') {
48
+ return `${labelName} is refused: previous run ${previous} has no finished verdict and may still ` +
49
+ `be editing the worktree. Repeat without ${labelName} to attach to it. ${hints}`;
50
+ }
51
+ throw new Error(`unknown continuation-grant-usable violation: ${violation}`);
52
+ },
53
+ }),
54
+ 'retry-grant-usable': Object.freeze({
55
+ data: Object.freeze(['violation', 'run', 'runsRoot', 'previousOrder', 'orderId', 'labelName']),
56
+ topic: 'order',
57
+ rule: 'Grant retry to a bare existing last run of the task chain under its own recorded order id, ' +
58
+ 'only after a non-OK finished verdict and after its worker has stopped; retry repeats that pass, ' +
59
+ 'while continue grants the next pass.',
60
+ message: ({ violation, run, runsRoot, previousOrder, orderId, labelName }) => {
61
+ if (violation === 'missing-folder') {
62
+ return `Retry is refused: ${run} is not a bare existing run folder in ${runsRoot}.`;
63
+ }
64
+ if (violation === 'not-last') {
65
+ return `Retry is refused: grant ${run} is not the LAST run of this task's chain; a retry is single-use.`;
66
+ }
67
+ if (violation === 'order-mismatch') {
68
+ return `Retry is refused: run ${run} order_id ${previousOrder} differs from order ${orderId}. ` +
69
+ 'A retry repeats its own order; a run without order_id predates orders — start a new order.';
70
+ }
71
+ if (violation === 'unfinished') {
72
+ return `Retry is refused: run ${run} has no finished verdict or its worker may still be alive and writing. ` +
73
+ `Repeat without ${labelName} to attach to it.`;
74
+ }
75
+ if (violation === 'ended-ok') {
76
+ return `Retry is refused: run ${run} ended OK. A retry repeats a failed pass; use continue: for the next pass.`;
77
+ }
78
+ throw new Error(`unknown retry-grant-usable violation: ${violation}`);
79
+ },
80
+ }),
81
+ 'ready-grant-line': Object.freeze({
82
+ data: Object.freeze(['variant', 'run', 'reason', 'orderId']),
83
+ topic: 'order',
84
+ rule: 'Write a ready continue grant with the latest run and the orchestrator\'s reason, or a retry grant ' +
85
+ 'with that failed pass\'s recorded reason and order id; without a run there is no ready grant.',
86
+ message: ({ variant, run, reason, orderId }) => {
87
+ if (variant === 'none') return 'Ready grant line: none (there is no run to continue).';
88
+ if (variant === 'continue') return `Ready grant line: continue: ${run} — ${reason}.`;
89
+ if (variant === 'retry') {
90
+ return `Ready retry line: retry: ${run} — ${reason} (repeats that pass under order ${orderId}).`;
91
+ }
92
+ throw new Error(`unknown ready-grant-line variant: ${variant}`);
93
+ },
94
+ }),
95
+ 'grant-last-run-hint': Object.freeze({
96
+ data: Object.freeze(['variant', 'run', 'status', 'reason']),
97
+ topic: 'order',
98
+ rule: 'Use the latest recorded run and its outcome when repairing a continue or retry header grant.',
99
+ message: ({ variant, run, status, reason }) => {
100
+ if (variant === 'none') return 'Last run: none. Outcome: none.';
101
+ if (variant === 'last') return `Last run: ${run}. Outcome: ${status} — ${reason}.`;
102
+ throw new Error(`unknown grant-last-run-hint variant: ${variant}`);
103
+ },
104
+ }),
105
+ 'advisor-scope-continuation': Object.freeze({
106
+ data: Object.freeze(['phaseLabel', 'continueLabel']),
107
+ topic: 'order',
108
+ rule: 'Never continue an advisor scope pass; repeat failed scope with retry, or use a new order id without continue.',
109
+ message: ({ phaseLabel, continueLabel }) =>
110
+ `codex-advisor ${phaseLabel} scope refuses ${continueLabel}: a scope pass is never continued, and continuing one ` +
111
+ "spends the order's single continuation that its advise phase needs. Action: repeat the scope under a new order id " +
112
+ `without ${continueLabel} and without a continue: grant, then run advise as that order's continuation; a ` +
113
+ 'scope run that failed is repeated with a `retry:` grant instead.',
114
+ }),
115
+ 'retry-pass-identity': Object.freeze({
116
+ data: Object.freeze(['agent', 'phase']),
117
+ topic: 'order',
118
+ rule: 'Retry the named run with its own agent and phase.',
119
+ message: ({ agent, phase }) => `a retry repeats the named run's own agent and phase: ${agent}/${phase}.`,
120
+ }),
121
+ 'repeat-task-grant-required': Object.freeze({
122
+ data: Object.freeze(['violation', 'continueLabel', 'slugLabel', 'slug', 'count', 'latest']),
123
+ topic: 'order',
124
+ rule: 'Ask the orchestrator for a continue grant before repeating an existing task, even under a different slug.',
125
+ message: ({ violation, continueLabel, slugLabel, slug, count, latest, hints }) => {
126
+ let previous;
127
+ if (violation === 'renamed') previous = `this task already ran in this repository under the name “${slug}”`;
128
+ else if (violation === 'existing') previous = `runs for task “${slug}” already exist in this repository`;
129
+ else throw new Error(`unknown repeat-task-grant-required violation: ${violation}`);
130
+ return `${continueLabel} is required: ${previous} (${count}), latest: ${latest}. ` +
131
+ 'A repeat run is allowed, but the orchestrator decides, not the runner: it read the ' +
132
+ `previous response and knows whether work remains. Add ${continueLabel} if you are finishing ` +
133
+ `the same task; changing ${slugLabel} with the same task text does not stop it being a repeat. ${hints}`;
134
+ },
135
+ }),
136
+ 'advisor-advise-continuation': Object.freeze({
137
+ data: Object.freeze(['phaseLabel', 'continueLabel']),
138
+ topic: 'order',
139
+ rule: 'Grant advise as the continuation of the scope run of the same order so it can settle the predicted risks.',
140
+ message: ({ phaseLabel, continueLabel }) =>
141
+ `codex-advisor ${phaseLabel} advise requires a ${continueLabel} grant naming the scope run: phase 2 continues the scope run of the same order ` +
142
+ 'so it can settle the risks phase 1 predicted.',
143
+ }),
144
+ 'build-advice-required': Object.freeze({
145
+ data: Object.freeze([]),
146
+ topic: 'order',
147
+ rule: 'Start a build order with advice naming an OK advisor advise run, or mechanical, revert, docs-only or test-only work without design choices.',
148
+ message: () => 'codex-build requires the task file to start with the header line `advice: <value>`, where the ' +
149
+ 'value is mechanical | revert | docs-only | test-only ' +
150
+ 'or an absolute path to an existing advisor run directory whose meta.json says agent codex-advisor, ' +
151
+ 'phase advise, status OK. ' +
152
+ 'An order that invents a construction needs a second opinion first; only work with no design choice may skip it.',
153
+ }),
154
+ 'advise-scope-run-required': Object.freeze({
155
+ data: Object.freeze(['violation']),
156
+ topic: 'order',
157
+ rule: 'Name an OK codex-advisor scope run in the continue grant for advise.',
158
+ message: ({ violation, failed }) => {
159
+ if (violation === 'agent') return "agent must be 'codex-advisor'";
160
+ if (violation === 'phase') return "phase must be 'scope'";
161
+ if (violation === 'status') return "status must be 'OK'";
162
+ if (violation === 'meta') return `continued run meta.json failed ${failed}.`;
163
+ throw new Error(`unknown advise-scope-run-required violation: ${violation}`);
164
+ },
165
+ }),
166
+ 'advice-retired-root': Object.freeze({
167
+ data: Object.freeze(['candidate', 'retiredRoot', 'destination', 'equivalent']),
168
+ topic: 'order',
169
+ rule: 'When advice points under a retired runs root, update advice to the equivalent path ' +
170
+ 'under the current destination or pass that path explicitly; retired paths are never silently remapped.',
171
+ message: ({ retiredRoot, destination, equivalent }) =>
172
+ `Run records moved from ${retiredRoot} to ${destination}. Equivalent path: ${equivalent}. ` +
173
+ 'Update advice: or pass this new path explicitly. No path was remapped.',
174
+ }),
175
+ });
@@ -0,0 +1,206 @@
1
+ /** Plan_79 D4: owns rules for values and placement of task-header labels. */
2
+ export const RULES = Object.freeze({
3
+ 'order-header-label-shape': Object.freeze({
4
+ data: Object.freeze(['label', 'violation']),
5
+ topic: 'order',
6
+ rule: 'Write header labels at column one with their exact spelling and a non-empty value.',
7
+ message: ({ label, violation }) => {
8
+ if (violation === 'empty') return `header label "${label}" has an empty value`;
9
+ if (violation === 'spelling') return `write the header label exactly as "${label}:"`;
10
+ throw new Error(`unknown order-header-label-shape violation: ${violation}`);
11
+ },
12
+ }),
13
+ 'unknown-header-label': Object.freeze({
14
+ data: Object.freeze(['label', 'knownLabels']),
15
+ topic: 'order',
16
+ rule: 'Use only known header labels; an unknown lowercase label after a header line is refused.',
17
+ message: ({ label, knownLabels }) =>
18
+ `unknown header label "${label}"; known labels: ${knownLabels.join(', ')}`,
19
+ }),
20
+ 'duplicate-order-label': Object.freeze({
21
+ data: Object.freeze(['label']),
22
+ topic: 'order',
23
+ rule: 'Write each header label at most once, even when the repeated values are identical.',
24
+ message: ({ label }) => `duplicate ${label} label`,
25
+ }),
26
+ 'header-metadata-placement': Object.freeze({
27
+ data: Object.freeze(['label', 'violation']),
28
+ topic: 'order',
29
+ rule: 'Put grants and advice only in the header at the top of the task file, with a non-empty value; ' +
30
+ 'quote examples with > or reword them inside a sentence, including examples in code fences.',
31
+ message: ({ label, violation }) => {
32
+ const repairHint = `move it into the header at the top of the file, spelled ${label}: <value>; ` +
33
+ 'if it is an example, quote it with > or reword it inside a sentence';
34
+ if (violation === 'misplaced') return `misplaced ${label} metadata; ${repairHint}`;
35
+ if (violation === 'empty') return `${label} value must be non-empty and single line; ${repairHint}`;
36
+ throw new Error(`unknown header-metadata-placement violation: ${violation}`);
37
+ },
38
+ }),
39
+ 'effort-value': Object.freeze({
40
+ data: Object.freeze(['value']),
41
+ topic: 'order',
42
+ rule: 'When effort is present, use a non-empty single word with no whitespace.',
43
+ message: ({ value }) =>
44
+ `must be a non-empty single word with no whitespace; got ${JSON.stringify(value)}`,
45
+ }),
46
+ 'run-slug': Object.freeze({
47
+ data: Object.freeze(['value']),
48
+ topic: 'order',
49
+ rule: 'The slug, or order id when slug is absent or empty, must contain a letter or digit ' +
50
+ 'after replacing characters outside A-Z, a-z, 0-9, dot, underscore and hyphen with hyphens.',
51
+ message: ({ value }) =>
52
+ `produces an unusable run folder name after sanitization: ${JSON.stringify(value)} must contain a letter or digit.`,
53
+ }),
54
+ 'repository-path': Object.freeze({
55
+ data: Object.freeze(['violation', 'value', 'segment', 'context', 'label']),
56
+ topic: 'order',
57
+ rule: 'When repository is present, use an absolute path to an existing directory; ' +
58
+ 'no path segment other than . or .. may end in a dot or a space, which Windows strips.',
59
+ message: ({ violation, value, segment, context, label, reason }) => {
60
+ if (context === 'task-input') {
61
+ return `${label} ${reason}\nThe run folder was not created; quota was not spent.`;
62
+ }
63
+ if (context !== undefined) throw new Error(`unknown repository-path context: ${context}`);
64
+ if (violation === 'relative') {
65
+ return `must be an absolute path to the repository; got ${JSON.stringify(value)}.`;
66
+ }
67
+ if (violation === 'altered-segment') {
68
+ return `has a path segment ending in a dot or a space (${JSON.stringify(segment)}), which Windows strips; ` +
69
+ `got ${JSON.stringify(value)}.`;
70
+ }
71
+ if (violation === 'not-directory') {
72
+ return `must name an existing directory; ${JSON.stringify(value)} is not one.`;
73
+ }
74
+ throw new Error(`unknown repository-path violation: ${violation}`);
75
+ },
76
+ }),
77
+ 'agent-accepted-labels': Object.freeze({
78
+ data: Object.freeze(['label', 'agent']),
79
+ topic: 'order',
80
+ rule: 'Use only the header labels accepted by the agent assigned to this order.',
81
+ message: ({ label, agent }) => `label "${label}" is not accepted by ${agent}`,
82
+ }),
83
+ 'order-value-placeholder': Object.freeze({
84
+ data: Object.freeze(['label']),
85
+ topic: 'order',
86
+ rule: 'Replace placeholder header values with the actual values assigned to this order.',
87
+ message: ({ label }) => `label "${label}" is still a placeholder`,
88
+ }),
89
+ 'header-shell-safety': Object.freeze({
90
+ data: Object.freeze(['labelName', 'sequence']),
91
+ topic: 'order',
92
+ rule: 'Keep header values short and free of forbidden shell sequences; put free text in the task file body.',
93
+ message: ({ labelName, sequence }) =>
94
+ `${labelName} contains forbidden shell sequence ${JSON.stringify(sequence)}; ` +
95
+ 'put free text in the task file body and keep header values short',
96
+ }),
97
+ 'build-existing-scope-required': Object.freeze({
98
+ data: Object.freeze(['labelName', 'newLabelName']),
99
+ topic: 'order',
100
+ rule: 'For codex-build, declare at least one existing path pattern in scope; scope new does not replace it.',
101
+ message: ({ labelName, newLabelName }) =>
102
+ `${labelName} is required for codex-build: declare at least one existing path pattern; ` +
103
+ `${newLabelName} does not replace it.`,
104
+ }),
105
+ 'required-order-labels': Object.freeze({
106
+ data: Object.freeze(['label', 'example']),
107
+ topic: 'order',
108
+ rule: 'Include every required header label for the assigned agent, using its example as a guide.',
109
+ message: ({ label, example }) =>
110
+ `missing required header label "${label}:"; example: ${label}: ${example}`,
111
+ }),
112
+ 'scope-pattern-shape': Object.freeze({
113
+ data: Object.freeze(['violation', 'part', 'pattern']),
114
+ topic: 'order',
115
+ rule: 'Write scope and scope new as repository-relative file patterns with forward slashes, ' +
116
+ 'no line breaks, parent-directory segments or service directories; name a file or use a glob for a directory.',
117
+ message: ({ violation, part, pattern }) => {
118
+ if (part !== 'reason' && part !== 'action') throw new Error(`unknown scope-pattern-shape part: ${part}`);
119
+ if (violation === 'line-break') return part === 'reason'
120
+ ? 'contains a newline or carriage return'
121
+ : 'use a glob instead of a literal path containing a line break';
122
+ if (violation === 'absolute') return part === 'reason'
123
+ ? 'is an absolute or drive-qualified path'
124
+ : 'use a path relative to the repository root with forward slashes';
125
+ if (violation === 'backslash') return part === 'reason'
126
+ ? 'uses backslash separators'
127
+ : 'replace backslashes with forward slashes';
128
+ if (violation === 'parent-directory') return part === 'reason'
129
+ ? 'contains a parent-directory (..) segment'
130
+ : 'remove the .. segment and keep the path relative to the repository root';
131
+ if (violation === 'service-directory') return part === 'reason'
132
+ ? 'lies inside a service directory that no scope can authorise'
133
+ : 'name the file outside the service directory, or make that edit yourself';
134
+ if (violation === 'directory' || violation === 'new-directory') {
135
+ if (part === 'reason') return 'names a directory rather than a file';
136
+ if (violation === 'directory') {
137
+ return `write ${pattern.replace(/\/$/, '')}/** for everything inside it, or name the file itself`;
138
+ }
139
+ return `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`;
140
+ }
141
+ throw new Error(`unknown scope-pattern-shape violation: ${violation}`);
142
+ },
143
+ }),
144
+ 'scope-pattern-match': Object.freeze({
145
+ data: Object.freeze(['violation', 'part', 'repoRoot', 'labelName', 'message']),
146
+ topic: 'order',
147
+ rule: 'Each ordinary scope pattern must parse and match an existing repository path; ' +
148
+ 'start from the git repository root, use UTF-8 file names, and declare intentionally new paths with scope new.',
149
+ message: ({ violation, part, repoRoot, labelName, message }) => {
150
+ if (part !== 'reason' && part !== 'action') throw new Error(`unknown scope-pattern-match part: ${part}`);
151
+ if (violation === 'missing') return part === 'reason'
152
+ ? 'does not match any existing path in the repository'
153
+ : `correct the pattern or declare an intentionally new path with ${labelName}`;
154
+ if (violation === 'not-repository') return part === 'reason'
155
+ ? `does not match any existing path under ${repoRoot}; ` +
156
+ 'git could not list that folder as a repository, so it was walked instead'
157
+ : `start the run from the repository root (cd into it, or pass ${labelName} <repository root>); ` +
158
+ 'if this folder is the intended one, correct the pattern';
159
+ if (violation === 'invalid-glob') return part === 'reason'
160
+ ? `could not parse the pattern: ${message}`
161
+ : 'correct the pattern and retry';
162
+ if (violation === 'non-utf8') return part === 'reason'
163
+ ? message
164
+ : 'rename the non-UTF-8 file using a UTF-8 file name and retry';
165
+ throw new Error(`unknown scope-pattern-match violation: ${violation}`);
166
+ },
167
+ }),
168
+ 'run-phase-required': Object.freeze({
169
+ data: Object.freeze(['violation', 'phaseLabel', 'phase', 'agent', 'names']),
170
+ topic: 'order',
171
+ rule: 'Declare an allowed phase for the assigned agent; phase may be omitted only when default is its sole phase.',
172
+ message: ({ violation, phaseLabel, phase, agent, names }) => {
173
+ let reason;
174
+ if (violation === 'missing') reason = `${phaseLabel} is required`;
175
+ else if (violation === 'undeclared') reason = `undeclared ${phaseLabel} ${JSON.stringify(phase)}`;
176
+ else throw new Error(`unknown run-phase-required violation: ${violation}`);
177
+ return `${reason} for ${agent}. Action: pass ${phaseLabel} <name>; allowed phases: ${names.join(', ')}.`;
178
+ },
179
+ }),
180
+ 'order-id-owner': Object.freeze({
181
+ data: Object.freeze(['violation', 'orderId', 'ownerDir', 'slug', 'startedAt', 'hashScheme', 'labelName']),
182
+ topic: 'order',
183
+ rule: 'Use a new order id for a new task; another pass of an owned order needs a continue or retry header grant.',
184
+ message: ({ violation, orderId, ownerDir, slug, startedAt, hashScheme, labelName }) => {
185
+ const identity = `Order id collision: ${JSON.stringify(String(orderId ?? ''))} already belongs to run folder ${ownerDir} ` +
186
+ `(slug ${slug}, started_at ${startedAt})`;
187
+ if (violation === 'predates-hash-scheme') {
188
+ return `${identity}, recorded before the task header (hash scheme ${hashScheme}), so its task cannot be compared. ` +
189
+ `Its answer is already on disk in that folder (codex-bridge read "${ownerDir}"). ` +
190
+ 'Another pass of that order needs a continue:/retry: header line; a new task needs a new order id.';
191
+ }
192
+ if (violation === 'different-task') {
193
+ return `${identity} with a different task. Use a new order id (${labelName}), or a continue:/retry: header line ` +
194
+ 'if this is another pass of that order.';
195
+ }
196
+ throw new Error(`unknown order-id-owner violation: ${violation}`);
197
+ },
198
+ }),
199
+ 'scope-pattern-refusal': Object.freeze({
200
+ data: Object.freeze(['pattern']),
201
+ topic: 'order',
202
+ rule: 'Correct the refused scope pattern using its scope-check reason and action before starting a run.',
203
+ message: ({ pattern, reason, action }) =>
204
+ `pattern ${JSON.stringify(pattern)} refused: ${reason}. Action: ${action}.`,
205
+ }),
206
+ });
@@ -0,0 +1,80 @@
1
+ /** Plan_79 D4: owns rules for task-body sections. */
2
+ export const RULES = Object.freeze({
3
+ 'scout-questions': Object.freeze({
4
+ data: Object.freeze(['violation', 'line']),
5
+ topic: 'order',
6
+ rule: 'For codex-scout, include a non-empty Questions section in the task document. ' +
7
+ 'Whenever Questions is present, put each question on its own line as a Markdown list item ' +
8
+ 'using -, *, or a number followed by a dot.',
9
+ message: ({ violation, line }) => {
10
+ if (violation === 'non-list-line') {
11
+ return `Questions section contains a non-list line: ${JSON.stringify(line.trim())}`;
12
+ }
13
+ if (violation === 'empty') return 'Questions section is empty';
14
+ if (violation === 'missing') {
15
+ return 'a sub-question is required for codex-scout: put one Markdown list item per sub-question ' +
16
+ 'under a `Questions` heading in the task document. ' +
17
+ 'The runner will not infer questions from the task text; no quota was spent.';
18
+ }
19
+ throw new Error(`unknown scout-questions violation: ${violation}`);
20
+ },
21
+ }),
22
+ 'question-context-marker': Object.freeze({
23
+ data: Object.freeze(['index', 'text', 'context']),
24
+ topic: 'order',
25
+ rule: 'If a question uses a context-only marker, spell it exactly [context-only] and place it ' +
26
+ 'only once, at the start, followed by whitespace (such as a space) and non-empty question text.',
27
+ message: ({ index, text, context, refusal }) => {
28
+ if (context === 'task-input') {
29
+ return `${refusal}; the marker is exactly [context-only] followed by a space and the question; no quota was spent.`;
30
+ }
31
+ if (context !== undefined) throw new Error(`unknown question-context-marker context: ${context}`);
32
+ return `Q${index + 1} has an invalid context-only marker in ${JSON.stringify(text)}`;
33
+ },
34
+ }),
35
+ 'verify-line': Object.freeze({
36
+ data: Object.freeze(['violation']),
37
+ topic: 'order',
38
+ rule: 'When a Verify section is present, put exactly one non-empty command line in it; ' +
39
+ 'blank lines are ignored. The section runs until the next heading of the same or a higher level, ' +
40
+ 'so any non-empty line after the command, including a paragraph or a second command, counts as another line. ' +
41
+ 'Put ## Verify last or follow it with another heading.',
42
+ message: ({ violation }) => {
43
+ if (violation === 'empty') return 'Verify section is empty';
44
+ if (violation === 'multiple-lines') return 'Verify section must contain exactly one non-empty line';
45
+ throw new Error(`unknown verify-line violation: ${violation}`);
46
+ },
47
+ }),
48
+ 'advisor-task-options': Object.freeze({
49
+ data: Object.freeze(['violation', 'lineNo', 'line', 'preference', 'optionId']),
50
+ topic: 'order',
51
+ rule: 'For an advisor task, write a ## Options section with at least two distinct options ' +
52
+ 'as - option-id: description, using ids matching [a-z0-9][a-z0-9-]*; ' +
53
+ 'remove preference markers from every line in that section (Plan_59 D5).',
54
+ message: ({ violation, lineNo, line, preference, optionId }) => {
55
+ if (violation === 'missing') return 'Options: add a "## Options" section with at least two options.';
56
+ const location = `Options line ${lineNo} ${JSON.stringify(line)}`;
57
+ if (violation === 'preference') {
58
+ return `${location}: remove preference marker ${JSON.stringify(preference)} (D5).`;
59
+ }
60
+ if (violation === 'option-shape') {
61
+ return `${location}: use "- option-id: description" with id matching [a-z0-9][a-z0-9-]*.`;
62
+ }
63
+ if (violation === 'duplicate') return `${location}: replace duplicate option id "${optionId}".`;
64
+ if (violation === 'too-few') return `${location}: provide at least two distinct options.`;
65
+ throw new Error(`unknown advisor-task-options violation: ${violation}`);
66
+ },
67
+ }),
68
+ 'advisor-task-paths': Object.freeze({
69
+ data: Object.freeze(['violation', 'lineNo', 'line']),
70
+ topic: 'order',
71
+ rule: 'For an advisor task, write a ## Paths section with at least one repository-relative path as a list item.',
72
+ message: ({ violation, lineNo, line }) => {
73
+ if (violation === 'missing') return 'Paths: add a "## Paths" section with repository-relative paths.';
74
+ if (violation === 'empty') {
75
+ return `Paths line ${lineNo} ${JSON.stringify(line)}: add at least one repository-relative path as a list item.`;
76
+ }
77
+ throw new Error(`unknown advisor-task-paths violation: ${violation}`);
78
+ },
79
+ }),
80
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Plan_79 D4 places each rule where the orchestrator writes what it governs:
3
+ * the call line or task file → call.mjs; a grant label (continue, retry,
4
+ * advice) → header-grants.mjs; any other header label → header.mjs;
5
+ * a body section → task-document.mjs (all under order-rules/).
6
+ * A file outgrowing the gate splits by the next level of the same place
7
+ * (header → label families), never by number.
8
+ */
9
+ import { RULES as HEADER_RULES } from './order-rules/header.mjs';
10
+ import { RULES as HEADER_GRANT_RULES } from './order-rules/header-grants.mjs';
11
+ import { RULES as TASK_DOCUMENT_RULES } from './order-rules/task-document.mjs';
12
+ import { RULES as CALL_RULES } from './order-rules/call.mjs';
13
+ import { recordOrderText, checkOrderTextParameters } from './order-text-record.mjs';
14
+
15
+ const places = [HEADER_RULES, HEADER_GRANT_RULES, TASK_DOCUMENT_RULES, CALL_RULES];
16
+ const keys = new Set();
17
+ for (const rules of places) {
18
+ for (const key of Object.keys(rules)) {
19
+ if (keys.has(key)) throw new Error(`duplicate order rule key: ${key}`);
20
+ keys.add(key);
21
+ }
22
+ }
23
+ export const ORDER_RULES = Object.freeze(Object.assign({}, ...places));
24
+
25
+ export function orderRuleMessage(key, values = {}) {
26
+ if (!Object.hasOwn(ORDER_RULES, key)) throw new Error(`unknown order rule: ${key}`);
27
+ checkOrderTextParameters('orderRuleMessage', key, values, ORDER_RULES[key].data);
28
+ return recordOrderText(ORDER_RULES[key].message(values));
29
+ }
30
+
31
+ // Plan_79 B3b: adds a location to text built by the registry and owns no rule.
32
+ export function orderRefusalAt(location, message) {
33
+ checkOrderTextParameters('orderRefusalAt', null, { location, message }, ['location']);
34
+ return recordOrderText(`${location}: ${message}`);
35
+ }
36
+
37
+ // Plan_79 B3c1: adds a source location to text built by the registry and owns no rule.
38
+ export function orderProblemLine({ lineNo, reason, line }) {
39
+ checkOrderTextParameters('orderProblemLine', null, { lineNo, reason, line }, ['lineNo', 'line']);
40
+ return recordOrderText(lineNo === null ? reason : `line ${lineNo}: ${reason}: ${line}`);
41
+ }
42
+
43
+ // Plan_79 B3c2: adds a label name to text built by the registry and owns no rule.
44
+ export function orderLabelReason(labelName, reason) {
45
+ checkOrderTextParameters('orderLabelReason', null, { labelName, reason }, ['labelName']);
46
+ return recordOrderText(`${labelName} ${reason}`);
47
+ }
48
+
49
+ // Plan_79 B3c2: renders the label schema owned by order-schema.mjs and owns no rule.
50
+ export function orderHeaderHelp(labels) {
51
+ checkOrderTextParameters('orderHeaderHelp', null, { labels }, ['labels']);
52
+ return recordOrderText(labels.map(({ label, example }) => `${label}: ${example}`).join('\n'));
53
+ }
54
+
55
+ // Plan_79 D3: callers supply the topic catalogue so installed rules do not depend on the CLI tree.
56
+ export function validateOrderRules(rules, topics, diagnostics) {
57
+ for (const [key, entry] of Object.entries(rules)) {
58
+ if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(key)) {
59
+ throw new Error(`invalid order rule key: ${key}`);
60
+ }
61
+ if (Object.hasOwn(diagnostics, key)) throw new Error(`duplicate order key: ${key}`);
62
+ if (!entry || Object.keys(entry).length !== 4 ||
63
+ !['topic', 'rule', 'message', 'data'].every((field) => Object.hasOwn(entry, field))) {
64
+ throw new Error(`invalid order rule entry: ${key}`);
65
+ }
66
+ if (!Object.hasOwn(topics, entry.topic)) throw new Error(`unknown order rule topic: ${key}`);
67
+ if (typeof entry.rule !== 'string' || !entry.rule.trim() || entry.rule.includes('{{')) {
68
+ throw new Error(`invalid order rule text: ${key}`);
69
+ }
70
+ if (typeof entry.message !== 'function') throw new Error(`invalid order rule message: ${key}`);
71
+ if (!Array.isArray(entry.data) ||
72
+ ![...entry.data].every((name) => typeof name === 'string' && name.trim()) ||
73
+ new Set(entry.data).size !== entry.data.length) {
74
+ throw new Error(`invalid order rule data: ${key}`);
75
+ }
76
+ }
77
+ }