@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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -1
- package/cli/command-registry.mjs +1 -1
- package/cli/guidance-render.mjs +57 -0
- package/cli/guidance-topics.mjs +8 -0
- package/cli/guidance.mjs +9 -10
- package/package.json +2 -2
- package/scripts/home-write-inventory.mjs +2 -0
- package/scripts/run-tests.mjs +44 -0
- package/src/claude/rules/core.md +5 -4
- package/src/home/guidance/order.md +15 -14
- package/src/home/guidance/records.md +3 -1
- package/src/home/hooks/dispatcher-gate.mjs +8 -2
- package/src/home/hooks/order-gate.mjs +16 -15
- package/src/home/lib/dispatcher-call.mjs +27 -24
- package/src/home/lib/dispatcher-command.mjs +12 -6
- package/src/home/lib/dispatcher-gate.mjs +20 -16
- package/src/home/lib/dispatcher-model.mjs +7 -8
- package/src/home/lib/dispatcher-pin.mjs +7 -6
- package/src/home/lib/dispatcher-protocol.mjs +76 -4
- package/src/home/lib/dispatcher-state.mjs +4 -3
- package/src/home/lib/install-owner-roots.mjs +57 -55
- package/src/home/lib/meta/advisor-task.mjs +21 -12
- package/src/home/lib/meta/paths.mjs +6 -1
- package/src/home/lib/meta/reply.mjs +1 -10
- package/src/home/lib/meta/unavailable-rows.mjs +11 -0
- package/src/home/lib/order-diagnostics/cli-flags.mjs +56 -0
- package/src/home/lib/order-diagnostics/filesystem.mjs +18 -0
- package/src/home/lib/order-diagnostics/host-state.mjs +263 -0
- package/src/home/lib/order-diagnostics/internal.mjs +82 -0
- package/src/home/lib/order-diagnostics/run-state.mjs +58 -0
- package/src/home/lib/order-diagnostics.mjs +48 -0
- package/src/home/lib/order-rules/call.mjs +130 -0
- package/src/home/lib/order-rules/header-grants.mjs +175 -0
- package/src/home/lib/order-rules/header.mjs +206 -0
- package/src/home/lib/order-rules/task-document.mjs +80 -0
- package/src/home/lib/order-rules.mjs +77 -0
- package/src/home/lib/order-schema.mjs +206 -200
- package/src/home/lib/order-text-record.mjs +104 -0
- package/src/home/lib/order-values.mjs +46 -46
- package/src/home/lib/runner/advise-carry.mjs +9 -7
- package/src/home/lib/runner/args.mjs +23 -26
- package/src/home/lib/runner/codex-availability.mjs +10 -6
- package/src/home/lib/runner/continuation.mjs +158 -174
- package/src/home/lib/runner/launcher.mjs +19 -17
- package/src/home/lib/runner/order-claim.mjs +15 -9
- package/src/home/lib/runner/order-options.mjs +3 -2
- package/src/home/lib/runner/order-owner.mjs +9 -7
- package/src/home/lib/runner/pass-gate.mjs +14 -18
- package/src/home/lib/runner/preflight.mjs +25 -35
- package/src/home/lib/runner/prompts.mjs +2 -2
- package/src/home/lib/runner/question-kind.mjs +4 -2
- package/src/home/lib/runner/retired-roots.mjs +18 -9
- package/src/home/lib/runner/runs-root.mjs +3 -3
- package/src/home/lib/runner/sandbox-diagnosis.mjs +17 -17
- package/src/home/lib/runner/sandbox-probe.mjs +18 -27
- package/src/home/lib/runner/scope-check.mjs +30 -23
- package/src/home/lib/runner/task-file.mjs +6 -4
- package/src/home/lib/runner/task-input.mjs +12 -16
- package/src/home/lib/runner/worker-admission.mjs +5 -2
- package/src/home/lib/subagent-model-env.mjs +5 -6
- package/src/home/lib/task-header.mjs +159 -158
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** Plan_79 D4: owns diagnostics for the command-line flags the dispatcher passes to the runner. */
|
|
2
|
+
export const DIAGNOSTICS = Object.freeze({
|
|
3
|
+
'no-wait-grant-conflict': Object.freeze({
|
|
4
|
+
data: Object.freeze(['grant']),
|
|
5
|
+
category: 'cli-flags',
|
|
6
|
+
message: ({ grant }) =>
|
|
7
|
+
`--no-wait cannot be combined with a ${grant} grant: ` +
|
|
8
|
+
'checking an existing run must never authorize a new one.',
|
|
9
|
+
}),
|
|
10
|
+
'task-channel-conflict': Object.freeze({
|
|
11
|
+
data: Object.freeze([]),
|
|
12
|
+
category: 'cli-flags',
|
|
13
|
+
message: () => 'task text was supplied through both stdin and --task-file; choose exactly one channel',
|
|
14
|
+
}),
|
|
15
|
+
'unexpected-argument': Object.freeze({
|
|
16
|
+
data: Object.freeze(['value']),
|
|
17
|
+
category: 'cli-flags',
|
|
18
|
+
message: ({ value }) => `unexpected argument: ${value}`,
|
|
19
|
+
}),
|
|
20
|
+
'unknown-flag': Object.freeze({
|
|
21
|
+
data: Object.freeze(['name']),
|
|
22
|
+
category: 'cli-flags',
|
|
23
|
+
message: ({ name, help }) =>
|
|
24
|
+
`unknown flag --${name}: codex-bridge run takes only --agent, --task-file and --no-wait; ` +
|
|
25
|
+
'the order belongs in the task-file header' + (help ? `\n${help}` : ''),
|
|
26
|
+
}),
|
|
27
|
+
'boolean-flag-value': Object.freeze({
|
|
28
|
+
data: Object.freeze(['name', 'value']),
|
|
29
|
+
category: 'cli-flags',
|
|
30
|
+
message: ({ name, value }) =>
|
|
31
|
+
`--${name} takes no value, or one of 1/true/yes/0/false/no; got ${JSON.stringify(value)}. ` +
|
|
32
|
+
'A placeholder left in from the prompt template is not consent.',
|
|
33
|
+
}),
|
|
34
|
+
'missing-flag-value': Object.freeze({
|
|
35
|
+
data: Object.freeze(['key']),
|
|
36
|
+
category: 'cli-flags',
|
|
37
|
+
message: ({ key }) => `missing value for ${key}`,
|
|
38
|
+
}),
|
|
39
|
+
'flag-shell-safety': Object.freeze({
|
|
40
|
+
data: Object.freeze(['name', 'sequence']),
|
|
41
|
+
category: 'cli-flags',
|
|
42
|
+
message: ({ name, sequence }) =>
|
|
43
|
+
`--${name} contains forbidden shell sequence ${JSON.stringify(sequence)}; ` +
|
|
44
|
+
'put free text in the task file and pass only a short command-line value.',
|
|
45
|
+
}),
|
|
46
|
+
'agent-required': Object.freeze({
|
|
47
|
+
data: Object.freeze([]),
|
|
48
|
+
category: 'cli-flags',
|
|
49
|
+
message: () => '--agent is required',
|
|
50
|
+
}),
|
|
51
|
+
'agent-known': Object.freeze({
|
|
52
|
+
data: Object.freeze(['agent']),
|
|
53
|
+
category: 'cli-flags',
|
|
54
|
+
message: ({ agent }) => `unknown --agent ${agent}`,
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Plan_79 D4: owns diagnostics for filesystem access. */
|
|
2
|
+
export const DIAGNOSTICS = Object.freeze({
|
|
3
|
+
'task-file-readable': Object.freeze({
|
|
4
|
+
data: Object.freeze(['message']),
|
|
5
|
+
category: 'filesystem',
|
|
6
|
+
message: ({ message }) => `task file from --task-file could not be read: ${message}`,
|
|
7
|
+
}),
|
|
8
|
+
'project-runs-folder-required': Object.freeze({
|
|
9
|
+
data: Object.freeze(['projectRunsRoot']),
|
|
10
|
+
category: 'filesystem',
|
|
11
|
+
message: ({ projectRunsRoot }) => `project runs folder does not exist: ${projectRunsRoot}`,
|
|
12
|
+
}),
|
|
13
|
+
'runs-move-record-readable': Object.freeze({
|
|
14
|
+
data: Object.freeze(['file', 'message']),
|
|
15
|
+
category: 'filesystem',
|
|
16
|
+
message: ({ file, message }) => `Cannot read runs move record ${file}: ${message}`,
|
|
17
|
+
}),
|
|
18
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/** Plan_79 D4: owns diagnostics for the host's Codex CLI readiness and availability. */
|
|
2
|
+
import { DIAGNOSTICS as INTERNAL_DIAGNOSTICS } from './internal.mjs';
|
|
3
|
+
|
|
4
|
+
export const DIAGNOSTICS = Object.freeze({
|
|
5
|
+
'codex-readiness-unconfirmed': Object.freeze({
|
|
6
|
+
data: Object.freeze(['detail']),
|
|
7
|
+
category: 'host-state',
|
|
8
|
+
message: ({ detail }) => [
|
|
9
|
+
`Codex CLI readiness unconfirmed: ${detail}`,
|
|
10
|
+
'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.',
|
|
11
|
+
].join('\n'),
|
|
12
|
+
}),
|
|
13
|
+
'codex-unavailable': Object.freeze({
|
|
14
|
+
data: Object.freeze(['detail']),
|
|
15
|
+
category: 'host-state',
|
|
16
|
+
message: ({ detail }) => [
|
|
17
|
+
`Codex CLI unavailable: ${detail}`,
|
|
18
|
+
'Operator check: codex --version (and codex login if authorization is rejected)',
|
|
19
|
+
].join('\n'),
|
|
20
|
+
}),
|
|
21
|
+
'codex-availability-detail': Object.freeze({
|
|
22
|
+
data: Object.freeze(['variant', 'command', 'status', 'message']),
|
|
23
|
+
category: 'host-state',
|
|
24
|
+
message: ({ variant, command, status, message }) => {
|
|
25
|
+
if (variant === 'path-miss') return 'codex could not be resolved in this process PATH; readiness is unconfirmed';
|
|
26
|
+
if (variant === 'exit-status') return `${command} exited ${status}`;
|
|
27
|
+
if (variant === 'failed') return 'codex availability probe failed';
|
|
28
|
+
if (variant === 'logged-out') return 'codex login status: Not logged in';
|
|
29
|
+
if (variant === 'message') return message;
|
|
30
|
+
throw new Error(`unknown codex-availability-detail variant: ${variant}`);
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
'codex-unavailable-handoff': Object.freeze({
|
|
34
|
+
data: Object.freeze(['variant', 'signal']),
|
|
35
|
+
category: 'host-state',
|
|
36
|
+
message: ({ variant, signal }) => {
|
|
37
|
+
if (variant === 'handoff') {
|
|
38
|
+
return 'UNAVAILABLE — Codex is not available (signed out); ' +
|
|
39
|
+
'hand the task to the next executor, do not retry';
|
|
40
|
+
}
|
|
41
|
+
if (variant === 'signal') return `Signal: ${signal}`;
|
|
42
|
+
if (variant === 'operator-check') {
|
|
43
|
+
return 'Operator check: codex --version; codex login status (sign in with codex login)';
|
|
44
|
+
}
|
|
45
|
+
throw new Error(`unknown codex-unavailable-handoff variant: ${variant}`);
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
'stale-runs-root-override': Object.freeze({
|
|
49
|
+
data: Object.freeze([]),
|
|
50
|
+
category: 'host-state',
|
|
51
|
+
message: () => 'CODEX_RUNS_ROOT points under a retired runs root; remove it or set it to the new location.',
|
|
52
|
+
}),
|
|
53
|
+
'runs-move-record-invalid': Object.freeze({
|
|
54
|
+
data: Object.freeze(['file', 'violation', 'detail', 'index']),
|
|
55
|
+
category: 'host-state',
|
|
56
|
+
message: ({ file, violation, detail, index }) => {
|
|
57
|
+
let defect;
|
|
58
|
+
if (violation === 'parse') defect = detail;
|
|
59
|
+
else if (violation === 'version') defect = 'version must be 1';
|
|
60
|
+
else if (violation === 'retired-array') defect = 'retired must be a non-empty array';
|
|
61
|
+
else if (violation === 'retired-root') defect = `retired[${index}].root must be an absolute path`;
|
|
62
|
+
else if (violation === 'retired-time') defect = `retired[${index}].movedAt must be a parseable ISO time`;
|
|
63
|
+
else throw new Error(`unknown runs-move-record-invalid violation: ${violation}`);
|
|
64
|
+
return `Invalid runs move record ${file}: ${defect}`;
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
'sandbox-probe-reason': Object.freeze({
|
|
68
|
+
data: Object.freeze(['variant', 'form', 'status', 'signal']),
|
|
69
|
+
category: 'host-state',
|
|
70
|
+
message: ({ variant, form, status, signal }) => {
|
|
71
|
+
if (variant === 'timeout') return `The ${form} sandbox probe timed out.`;
|
|
72
|
+
if (variant === 'incomplete') return `The ${form} sandbox probe could not start or complete.`;
|
|
73
|
+
if (variant === 'signal') return `The ${form} sandbox probe was terminated by ${signal}.`;
|
|
74
|
+
if (variant === 'arguments') return `Codex rejected the ${form} sandbox probe arguments.`;
|
|
75
|
+
if (variant === 'no-status') return `The ${form} sandbox probe returned no exit status.`;
|
|
76
|
+
if (variant === 'alive') return 'The Codex sandbox started a process.';
|
|
77
|
+
if (variant === 'marker-exit') return `The flagged sandbox probe printed the marker but exited ${status}.`;
|
|
78
|
+
if (variant === 'flags') return 'This version of Codex no longer accepts the package sandbox flags.';
|
|
79
|
+
if (variant === 'unavailable') return 'Codex CLI is unavailable.';
|
|
80
|
+
if (variant === 'dead') return 'The Codex sandbox on this host cannot start a process.';
|
|
81
|
+
throw new Error(`unknown sandbox-probe-reason variant: ${variant}`);
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
'sandbox-refusal-repair': Object.freeze({
|
|
85
|
+
data: Object.freeze([]),
|
|
86
|
+
category: 'host-state',
|
|
87
|
+
message: () => 'Repair the host sandbox; do not rewrite or retry the order.',
|
|
88
|
+
}),
|
|
89
|
+
// Ubuntu 23.10+ forbids unprivileged user namespaces unless an AppArmor profile allows bwrap; that
|
|
90
|
+
// is the ordinary state of a fresh server, not an accident like the corrupted Windows state file.
|
|
91
|
+
'sandbox-linux-repair': Object.freeze({
|
|
92
|
+
data: Object.freeze([]),
|
|
93
|
+
category: 'host-state',
|
|
94
|
+
message: () => [
|
|
95
|
+
'On Ubuntu 23.10 and later this usually means AppArmor does not let bubblewrap create a user namespace.',
|
|
96
|
+
'Official repair: https://learn.chatgpt.com/docs/sandboxing — the bwrap-userns-restrict AppArmor profile',
|
|
97
|
+
'(Ubuntu 24.04) or the bubblewrap package (25.04 and later). Do not set',
|
|
98
|
+
'kernel.apparmor_restrict_unprivileged_userns=0: it lifts the restriction for every program on the host.',
|
|
99
|
+
].join(' '),
|
|
100
|
+
}),
|
|
101
|
+
'sandbox-operator-check': Object.freeze({
|
|
102
|
+
data: Object.freeze(['command']),
|
|
103
|
+
category: 'host-state',
|
|
104
|
+
message: ({ command }) => `Operator check (run from the repository directory): ${command}`,
|
|
105
|
+
}),
|
|
106
|
+
'sandbox-stderr-evidence': Object.freeze({
|
|
107
|
+
data: Object.freeze(['form', 'stderrTail']),
|
|
108
|
+
category: 'host-state',
|
|
109
|
+
message: ({ form, stderrTail }) =>
|
|
110
|
+
`${form} stderr:\n> ${(stderrTail || '(empty)').replace(/\r\n?/g, '\n').replace(/\n/g, '\n> ')}`,
|
|
111
|
+
}),
|
|
112
|
+
'sandbox-diagnosis-source': Object.freeze({
|
|
113
|
+
data: Object.freeze(['source']),
|
|
114
|
+
category: 'host-state',
|
|
115
|
+
message: ({ source }) => `Sandbox diagnosis source: ${source}`,
|
|
116
|
+
}),
|
|
117
|
+
'sandbox-diagnosis-attribution': Object.freeze({
|
|
118
|
+
data: Object.freeze(['attributed']),
|
|
119
|
+
category: 'host-state',
|
|
120
|
+
message: ({ attributed }) => attributed ? 'Sandbox log lines attributed to this probe:'
|
|
121
|
+
: 'Recent sandbox log lines only; not attributed to this probe:',
|
|
122
|
+
}),
|
|
123
|
+
'sandbox-setup-change': Object.freeze({
|
|
124
|
+
data: Object.freeze(['changed']),
|
|
125
|
+
category: 'host-state',
|
|
126
|
+
message: ({ changed }) =>
|
|
127
|
+
`setup_error.json changed since snapshot: ${changed === null ? 'unknown' : changed ? 'yes' : 'no'}`,
|
|
128
|
+
}),
|
|
129
|
+
'sandbox-evidence-line': Object.freeze({
|
|
130
|
+
data: Object.freeze(['line']),
|
|
131
|
+
category: 'host-state',
|
|
132
|
+
message: ({ line }) => `> ${line}`,
|
|
133
|
+
}),
|
|
134
|
+
'sandbox-evidence-unknown': Object.freeze({
|
|
135
|
+
data: Object.freeze(['variant', 'file']),
|
|
136
|
+
category: 'host-state',
|
|
137
|
+
message: ({ variant, file }) => {
|
|
138
|
+
if (variant === 'unreadable') return `Unknown: unable to read ${file}.`;
|
|
139
|
+
if (variant === 'baseline') return `Unknown: pre-probe size unavailable for ${file}.`;
|
|
140
|
+
throw new Error(`unknown sandbox-evidence-unknown variant: ${variant}`);
|
|
141
|
+
},
|
|
142
|
+
}),
|
|
143
|
+
'sandbox-no-repair': Object.freeze({
|
|
144
|
+
data: Object.freeze([]),
|
|
145
|
+
category: 'host-state',
|
|
146
|
+
message: () => 'No verified repair signature; inspect this log.',
|
|
147
|
+
}),
|
|
148
|
+
'sandbox-known-failure-runtime-file-locked': Object.freeze({
|
|
149
|
+
data: Object.freeze(['part']),
|
|
150
|
+
category: 'host-state',
|
|
151
|
+
message: ({ part }) => {
|
|
152
|
+
if (part === 'summary') {
|
|
153
|
+
return 'A file under the Codex runtimes folder is held open by another process, so sandbox setup cannot update its access rights (openai/codex #51634).';
|
|
154
|
+
}
|
|
155
|
+
if (part === 'repair') {
|
|
156
|
+
return '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.';
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`unknown sandbox-known-failure-runtime-file-locked part: ${part}`);
|
|
159
|
+
},
|
|
160
|
+
}),
|
|
161
|
+
'sandbox-known-failure-deny-read-state-corrupt': Object.freeze({
|
|
162
|
+
data: Object.freeze(['part', 'file']),
|
|
163
|
+
category: 'host-state',
|
|
164
|
+
message: ({ part, file }) => {
|
|
165
|
+
if (part === 'summary') return 'The sandbox state file is empty or corrupt.';
|
|
166
|
+
if (part === 'repair') {
|
|
167
|
+
return `Back up and rename ${file}, then rerun codex-bridge doctor; the helper recreates it.`;
|
|
168
|
+
}
|
|
169
|
+
throw new Error(`unknown sandbox-known-failure-deny-read-state-corrupt part: ${part}`);
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
'cmd-argument-transport': Object.freeze({
|
|
173
|
+
data: Object.freeze(['argument']),
|
|
174
|
+
category: 'host-state',
|
|
175
|
+
message: ({ argument }) => `argument cannot be passed through cmd.exe (contains % or "): ${argument}`,
|
|
176
|
+
}),
|
|
177
|
+
'run-worker-start-failure': Object.freeze({
|
|
178
|
+
data: Object.freeze(['message']),
|
|
179
|
+
category: 'host-state',
|
|
180
|
+
message: ({ message }) => `run worker process failed to start: ${message}`,
|
|
181
|
+
}),
|
|
182
|
+
'subagent-model-env-pin': Object.freeze({
|
|
183
|
+
data: Object.freeze(['model', 'pinFamily', 'hostVersion']),
|
|
184
|
+
category: 'host-state',
|
|
185
|
+
message: ({ model, pinFamily, hostVersion }) =>
|
|
186
|
+
`Order gate denied the Agent call because CLAUDE_CODE_SUBAGENT_MODEL=${JSON.stringify(model)} `
|
|
187
|
+
+ `overrides the installed dispatcher pin ${pinFamily} on host ${hostVersion}. `
|
|
188
|
+
+ `Unset CLAUDE_CODE_SUBAGENT_MODEL or set it to the pinned family ${pinFamily}.`,
|
|
189
|
+
}),
|
|
190
|
+
'dispatcher-pin-file-readable': Object.freeze({
|
|
191
|
+
data: Object.freeze(['file', 'problem', 'cli', 'root']),
|
|
192
|
+
category: 'host-state',
|
|
193
|
+
message: ({ file, problem, cli, root }) => {
|
|
194
|
+
if (problem === 'missing') return `Dispatcher file "${file}" is missing; repair with ${cli} install --host "${root}".`;
|
|
195
|
+
if (problem === 'unreadable') return `Dispatcher file "${file}" cannot be read; repair with ${cli} install --host "${root}".`;
|
|
196
|
+
throw new Error(`unknown dispatcher-pin-file-readable problem: ${problem}`);
|
|
197
|
+
},
|
|
198
|
+
}),
|
|
199
|
+
'dispatcher-pin-frontmatter-readable': Object.freeze({
|
|
200
|
+
data: Object.freeze(['file']),
|
|
201
|
+
category: 'host-state',
|
|
202
|
+
message: ({ file }) => `The frontmatter in "${file}" is unreadable.`,
|
|
203
|
+
}),
|
|
204
|
+
'dispatcher-pin-agent-name': Object.freeze({
|
|
205
|
+
data: Object.freeze(['file', 'agentType']),
|
|
206
|
+
category: 'host-state',
|
|
207
|
+
message: ({ file, agentType }) => `Dispatcher file "${file}" does not declare the required frontmatter name "${agentType}".`,
|
|
208
|
+
}),
|
|
209
|
+
'dispatcher-pin-model-family': Object.freeze({
|
|
210
|
+
data: Object.freeze(['file']),
|
|
211
|
+
category: 'host-state',
|
|
212
|
+
message: ({ file }) => `Dispatcher file "${file}" has no parseable model family in its frontmatter model.`,
|
|
213
|
+
}),
|
|
214
|
+
'dispatcher-pin-owner-agreement': Object.freeze({
|
|
215
|
+
data: Object.freeze(['disagreement']),
|
|
216
|
+
category: 'host-state',
|
|
217
|
+
message: ({ disagreement }) => `Installed dispatcher model families disagree across owners: ${disagreement}.`,
|
|
218
|
+
}),
|
|
219
|
+
'dispatcher-model-observation': Object.freeze({
|
|
220
|
+
data: Object.freeze(['verdict', 'agentType', 'models', 'pinFamily', 'incompleteMatch']),
|
|
221
|
+
category: 'host-state',
|
|
222
|
+
message: ({ verdict, agentType, models, pinFamily, incompleteMatch }) => {
|
|
223
|
+
if (verdict === 'violation') return `${agentType} ran on ${models} while the installed contract pins ${pinFamily}`;
|
|
224
|
+
if (verdict === 'match') return `${agentType} ran on ${models} matching the installed contract pin ${pinFamily}`;
|
|
225
|
+
if (verdict !== 'undetermined') throw new Error(`unknown dispatcher-model-observation verdict: ${verdict}`);
|
|
226
|
+
return incompleteMatch
|
|
227
|
+
? `${agentType} model comparison is undetermined: the transcript has unreadable records`
|
|
228
|
+
: `${agentType} model comparison against the installed contract is undetermined`;
|
|
229
|
+
},
|
|
230
|
+
}),
|
|
231
|
+
'dispatcher-model-stop-warning': Object.freeze({
|
|
232
|
+
data: Object.freeze(['agentType', 'models', 'pinFamily']),
|
|
233
|
+
category: 'host-state',
|
|
234
|
+
message: ({ agentType, models, pinFamily }) =>
|
|
235
|
+
`codex-bridge: dispatcher ${agentType} ran on ${models} while the installed contract pins ${pinFamily} — Claude quota was spent on it; run codex-bridge doctor.`,
|
|
236
|
+
}),
|
|
237
|
+
'install-record-owners-readable': Object.freeze({
|
|
238
|
+
data: Object.freeze(['file', 'problem', 'cli']),
|
|
239
|
+
category: 'host-state',
|
|
240
|
+
message: ({ file, problem, cli }) => {
|
|
241
|
+
if (problem === 'missing') return `Installation record at ${file} is missing.`;
|
|
242
|
+
if (problem === 'unreadable') return `Installation record at ${file} cannot be read.`;
|
|
243
|
+
if (problem === 'invalid-json') return `Installation record at ${file} contains invalid JSON.`;
|
|
244
|
+
if (problem === 'not-format-2') return `Installation record at ${file} is not format 2.`;
|
|
245
|
+
if (problem === 'no-owners') return `Installation record at ${file} has no owners.`;
|
|
246
|
+
if (problem === 'inventory-incomplete') {
|
|
247
|
+
// Plan_67 D11: match cli/inventory-confirm.mjs INVENTORY_CONFIRM_COMMAND; hooks cannot import cli/.
|
|
248
|
+
return `Installation record at ${file} has an incomplete inventory; if the recorded hosts are all the hosts using this home, run ${cli} inventory confirm.`;
|
|
249
|
+
}
|
|
250
|
+
if (problem === 'legacy-partition') return `Installation record at ${file} contains a legacy partition.`;
|
|
251
|
+
if (problem === 'malformed-owner') return `Installation record at ${file} has an owner without a non-empty root.`;
|
|
252
|
+
throw new Error(INTERNAL_DIAGNOSTICS['install-record-owners-problem-known'].message({ problem }));
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
'dispatcher-model-latch': Object.freeze({
|
|
256
|
+
data: Object.freeze(['agentType', 'models', 'pinFamily', 'hostVersion', 'at']),
|
|
257
|
+
category: 'host-state',
|
|
258
|
+
message: ({ agentType, models, pinFamily, hostVersion, at }) =>
|
|
259
|
+
`codex-bridge: the last ${agentType} ran on ${models} `
|
|
260
|
+
+ `while the installed contract pins ${pinFamily} (${hostVersion ?? 'unknown'}, ${at}); `
|
|
261
|
+
+ 'the cause is outside this call — another hook or the host; run codex-bridge doctor.',
|
|
262
|
+
}),
|
|
263
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Plan_79 D4: owns diagnostics for internal order contracts. */
|
|
2
|
+
export const DIAGNOSTICS = Object.freeze({
|
|
3
|
+
'dispatcher-agent-known': Object.freeze({
|
|
4
|
+
data: Object.freeze(['agent']),
|
|
5
|
+
category: 'internal',
|
|
6
|
+
message: ({ agent }) => `unknown dispatcher agent "${agent}"`,
|
|
7
|
+
}),
|
|
8
|
+
'order-input-name-known': Object.freeze({
|
|
9
|
+
data: Object.freeze(['label']),
|
|
10
|
+
category: 'internal',
|
|
11
|
+
message: ({ label }) => `Unknown order input label "${label}"`,
|
|
12
|
+
}),
|
|
13
|
+
'advise-scope-input-required': Object.freeze({
|
|
14
|
+
data: Object.freeze([]),
|
|
15
|
+
category: 'internal',
|
|
16
|
+
message: () => 'Cannot carry advise scope result: runsRoot and grantRun are required.',
|
|
17
|
+
}),
|
|
18
|
+
'order-claim-input-valid': Object.freeze({
|
|
19
|
+
data: Object.freeze(['violation']),
|
|
20
|
+
category: 'internal',
|
|
21
|
+
message: ({ violation }) => {
|
|
22
|
+
if (violation === 'order-id') return 'orderId must be a non-empty string';
|
|
23
|
+
if (violation === 'role') return 'role must be launcher or worker';
|
|
24
|
+
throw new Error(`unknown order-claim-input-valid violation: ${violation}`);
|
|
25
|
+
},
|
|
26
|
+
}),
|
|
27
|
+
'advisor-task-text-required': Object.freeze({
|
|
28
|
+
data: Object.freeze([]),
|
|
29
|
+
category: 'internal',
|
|
30
|
+
message: () => 'Advisor task text must be a string.',
|
|
31
|
+
}),
|
|
32
|
+
'sandbox-probe-repository-required': Object.freeze({
|
|
33
|
+
data: Object.freeze([]),
|
|
34
|
+
category: 'internal',
|
|
35
|
+
message: () => 'sandbox probe requires a repository path',
|
|
36
|
+
}),
|
|
37
|
+
'dispatcher-agent-input-known': Object.freeze({
|
|
38
|
+
data: Object.freeze(['agentType']),
|
|
39
|
+
category: 'internal',
|
|
40
|
+
message: ({ agentType }) => `unknown dispatcher agent ${JSON.stringify(agentType)}`,
|
|
41
|
+
}),
|
|
42
|
+
'dispatcher-prompt-text-required': Object.freeze({
|
|
43
|
+
data: Object.freeze([]),
|
|
44
|
+
category: 'internal',
|
|
45
|
+
message: () => 'prompt text is missing or is not text',
|
|
46
|
+
}),
|
|
47
|
+
'dispatcher-refusal': Object.freeze({
|
|
48
|
+
data: Object.freeze([]),
|
|
49
|
+
category: 'internal',
|
|
50
|
+
message: ({ reason }) => `Refused: ${reason}.`,
|
|
51
|
+
}),
|
|
52
|
+
'dispatcher-gate-internal-error': Object.freeze({
|
|
53
|
+
data: Object.freeze(['message']),
|
|
54
|
+
category: 'internal',
|
|
55
|
+
message: ({ message }) => `codex-bridge dispatcher gate: internal error: ${message}`,
|
|
56
|
+
}),
|
|
57
|
+
'dispatcher-protocol-key-known': Object.freeze({
|
|
58
|
+
data: Object.freeze(['key']),
|
|
59
|
+
category: 'internal',
|
|
60
|
+
message: ({ key }) => `unknown dispatcher protocol key: ${key}`,
|
|
61
|
+
}),
|
|
62
|
+
'dispatcher-state-identity-required': Object.freeze({
|
|
63
|
+
data: Object.freeze([]),
|
|
64
|
+
category: 'internal',
|
|
65
|
+
message: () => 'sessionId and agentId must be non-empty strings',
|
|
66
|
+
}),
|
|
67
|
+
'dispatcher-state-mutator-required': Object.freeze({
|
|
68
|
+
data: Object.freeze([]),
|
|
69
|
+
category: 'internal',
|
|
70
|
+
message: () => 'mutate must be a function',
|
|
71
|
+
}),
|
|
72
|
+
'dispatcher-state-record-required': Object.freeze({
|
|
73
|
+
data: Object.freeze([]),
|
|
74
|
+
category: 'internal',
|
|
75
|
+
message: () => 'mutate must produce a JSON record object',
|
|
76
|
+
}),
|
|
77
|
+
'install-record-owners-problem-known': Object.freeze({
|
|
78
|
+
data: Object.freeze(['problem']),
|
|
79
|
+
category: 'internal',
|
|
80
|
+
message: ({ problem }) => `unknown dispatcher-pin-owner-record problem: ${problem}`,
|
|
81
|
+
}),
|
|
82
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** Plan_79 D4: owns diagnostics for the run's state at refusal. */
|
|
2
|
+
export const DIAGNOSTICS = Object.freeze({
|
|
3
|
+
'run-not-created': Object.freeze({
|
|
4
|
+
data: Object.freeze([]),
|
|
5
|
+
category: 'run-state',
|
|
6
|
+
message: () => 'The run folder was not created; quota was not spent.',
|
|
7
|
+
}),
|
|
8
|
+
'advise-scope-result-readable': Object.freeze({
|
|
9
|
+
data: Object.freeze(['scopeFile', 'message']),
|
|
10
|
+
category: 'run-state',
|
|
11
|
+
message: ({ scopeFile, message }) => `Cannot read advise scope result at ${scopeFile}: ${message}`,
|
|
12
|
+
}),
|
|
13
|
+
'advise-scope-result-shape': Object.freeze({
|
|
14
|
+
data: Object.freeze(['scopeFile']),
|
|
15
|
+
category: 'run-state',
|
|
16
|
+
message: ({ scopeFile }) =>
|
|
17
|
+
`Malformed advise scope result at ${scopeFile}: expected predicted_risks and missing_paths arrays.`,
|
|
18
|
+
}),
|
|
19
|
+
'repository-detached-after-abandoned-run': Object.freeze({
|
|
20
|
+
data: Object.freeze(['run', 'branch']),
|
|
21
|
+
category: 'run-state',
|
|
22
|
+
message: ({ run, branch }) =>
|
|
23
|
+
`repository is detached after abandoned run ${run}, which recorded branch ${branch}. ` +
|
|
24
|
+
`Return with: git checkout ${branch}. No run folder was created and no quota was spent.`,
|
|
25
|
+
}),
|
|
26
|
+
'codex-not-started': Object.freeze({
|
|
27
|
+
data: Object.freeze([]),
|
|
28
|
+
category: 'run-state',
|
|
29
|
+
message: () => 'Codex was not started; quota was not spent',
|
|
30
|
+
}),
|
|
31
|
+
'dispatcher-order-transcript-readable': Object.freeze({
|
|
32
|
+
data: Object.freeze(['transcriptPath']),
|
|
33
|
+
category: 'run-state',
|
|
34
|
+
message: ({ transcriptPath }) => `the order could not be read from the agent transcript ${transcriptPath}`,
|
|
35
|
+
}),
|
|
36
|
+
'dispatcher-order-refusal': Object.freeze({
|
|
37
|
+
data: Object.freeze([]),
|
|
38
|
+
category: 'run-state',
|
|
39
|
+
message: ({ refusal }) => `FAIL — dispatcher gate: ${refusal}`,
|
|
40
|
+
}),
|
|
41
|
+
'dispatcher-receipt-conflict': Object.freeze({
|
|
42
|
+
data: Object.freeze(['runReceipt', 'runReceiptConflict']),
|
|
43
|
+
category: 'run-state',
|
|
44
|
+
message: ({ runReceipt, runReceiptConflict }) =>
|
|
45
|
+
'FAIL — dispatcher gate: the runner reported two different run folders for one dispatcher: ' +
|
|
46
|
+
`${runReceipt} and ${runReceiptConflict}; the answer is not delivered.`,
|
|
47
|
+
}),
|
|
48
|
+
'dispatcher-did-not-delegate': Object.freeze({
|
|
49
|
+
data: Object.freeze(['command']),
|
|
50
|
+
category: 'run-state',
|
|
51
|
+
message: ({ command }) => 'FAIL — dispatcher did not delegate: it handed back without running `' + command + '`',
|
|
52
|
+
}),
|
|
53
|
+
'dispatcher-runner-output': Object.freeze({
|
|
54
|
+
data: Object.freeze(['output']),
|
|
55
|
+
category: 'run-state',
|
|
56
|
+
message: ({ output }) => output,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Plan_79 D4: each diagnostic category owns its own file named after it under order-diagnostics/. */
|
|
2
|
+
import { DIAGNOSTICS as CLI_FLAGS_DIAGNOSTICS } from './order-diagnostics/cli-flags.mjs';
|
|
3
|
+
import { DIAGNOSTICS as FILESYSTEM_ACCESS_DIAGNOSTICS } from './order-diagnostics/filesystem.mjs';
|
|
4
|
+
import { DIAGNOSTICS as HOST_STATE_DIAGNOSTICS } from './order-diagnostics/host-state.mjs';
|
|
5
|
+
import { DIAGNOSTICS as RUN_STATE_DIAGNOSTICS } from './order-diagnostics/run-state.mjs';
|
|
6
|
+
import { DIAGNOSTICS as INTERNAL_DIAGNOSTICS } from './order-diagnostics/internal.mjs';
|
|
7
|
+
import { recordOrderText, checkOrderTextParameters } from './order-text-record.mjs';
|
|
8
|
+
|
|
9
|
+
export const DIAGNOSTIC_CATEGORIES = Object.freeze([
|
|
10
|
+
'cli-flags', 'filesystem', 'host-state', 'run-state', 'internal',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const categories = [CLI_FLAGS_DIAGNOSTICS, FILESYSTEM_ACCESS_DIAGNOSTICS, HOST_STATE_DIAGNOSTICS, RUN_STATE_DIAGNOSTICS, INTERNAL_DIAGNOSTICS];
|
|
14
|
+
const keys = new Set();
|
|
15
|
+
for (const diagnostics of categories) {
|
|
16
|
+
for (const key of Object.keys(diagnostics)) {
|
|
17
|
+
if (keys.has(key)) throw new Error(`duplicate order diagnostic key: ${key}`);
|
|
18
|
+
keys.add(key);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const ORDER_DIAGNOSTICS = Object.freeze(Object.assign({}, ...categories));
|
|
22
|
+
|
|
23
|
+
export function orderDiagnosticMessage(key, values = {}) {
|
|
24
|
+
if (!Object.hasOwn(ORDER_DIAGNOSTICS, key)) throw new Error(`unknown order diagnostic: ${key}`);
|
|
25
|
+
checkOrderTextParameters('orderDiagnosticMessage', key, values, ORDER_DIAGNOSTICS[key].data);
|
|
26
|
+
return recordOrderText(ORDER_DIAGNOSTICS[key].message(values));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function validateOrderDiagnostics(diagnostics) {
|
|
30
|
+
for (const [key, entry] of Object.entries(diagnostics)) {
|
|
31
|
+
if (!/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(key)) {
|
|
32
|
+
throw new Error(`invalid order diagnostic key: ${key}`);
|
|
33
|
+
}
|
|
34
|
+
if (!entry || Object.keys(entry).length !== 3 ||
|
|
35
|
+
!['category', 'message', 'data'].every((field) => Object.hasOwn(entry, field))) {
|
|
36
|
+
throw new Error(`invalid order diagnostic entry: ${key}`);
|
|
37
|
+
}
|
|
38
|
+
if (!DIAGNOSTIC_CATEGORIES.includes(entry.category)) {
|
|
39
|
+
throw new Error(`unknown order diagnostic category: ${key}`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof entry.message !== 'function') throw new Error(`invalid order diagnostic message: ${key}`);
|
|
42
|
+
if (!Array.isArray(entry.data) ||
|
|
43
|
+
![...entry.data].every((name) => typeof name === 'string' && name.trim()) ||
|
|
44
|
+
new Set(entry.data).size !== entry.data.length) {
|
|
45
|
+
throw new Error(`invalid order diagnostic data: ${key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Plan_79 D4: owns rules for the call line and the task file itself. */
|
|
2
|
+
export const RULES = Object.freeze({
|
|
3
|
+
'task-file-path': Object.freeze({
|
|
4
|
+
data: Object.freeze(['value']),
|
|
5
|
+
topic: 'order',
|
|
6
|
+
rule: 'The call names the task file by an absolute path to a file the orchestrator wrote with its file tool.',
|
|
7
|
+
message: ({ value }) => `--task-file must be an absolute path; got ${JSON.stringify(value)}`,
|
|
8
|
+
}),
|
|
9
|
+
'task-body-required': Object.freeze({
|
|
10
|
+
data: Object.freeze(['channel', 'taskFile']),
|
|
11
|
+
topic: 'order',
|
|
12
|
+
rule: 'The task file carries a non-empty body after the header and the blank line.',
|
|
13
|
+
message: ({ channel, taskFile }) => {
|
|
14
|
+
if (channel === 'task-file') return `task file from --task-file is empty: ${taskFile}`;
|
|
15
|
+
if (channel === 'stdin') return 'task text on stdin is empty';
|
|
16
|
+
throw new Error(`unknown task-body-required channel: ${channel}`);
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
'writing-run-exclusive': Object.freeze({
|
|
20
|
+
data: Object.freeze(['run', 'activePath', 'identity']),
|
|
21
|
+
topic: 'order',
|
|
22
|
+
rule: 'Wait for the active writing run to finish before starting another writing run in the same working tree.',
|
|
23
|
+
message: ({ run, activePath, identity }) => {
|
|
24
|
+
const identityNote = identity === 'unverified' ? '; process identity could not be confirmed' : '';
|
|
25
|
+
return [
|
|
26
|
+
`run ${run} is already active for this repository; two writing runs in one tree are prohibited${identityNote}`,
|
|
27
|
+
`Active run: ${activePath}`,
|
|
28
|
+
].join('\n');
|
|
29
|
+
},
|
|
30
|
+
}),
|
|
31
|
+
'order-launch-exclusive': Object.freeze({
|
|
32
|
+
data: Object.freeze(['variant', 'orderId', 'role', 'pid', 'acquiredAt', 'run']),
|
|
33
|
+
topic: 'order',
|
|
34
|
+
rule: 'Launch one run per order id at a time; repeat the same command to attach to that run.',
|
|
35
|
+
message: ({ variant, orderId, role, pid, acquiredAt, run, holderDescription }) => {
|
|
36
|
+
if (variant === 'holder-unverified') return 'a process that did not answer (unverified)';
|
|
37
|
+
if (variant === 'holder') return `${role} pid ${pid} since ${acquiredAt}`;
|
|
38
|
+
if (variant === 'busy') {
|
|
39
|
+
return `order id "${orderId}" is being launched by ${holderDescription}; repeat the same command later — ` +
|
|
40
|
+
'it attaches to that run instead of starting another.';
|
|
41
|
+
}
|
|
42
|
+
if (variant === 'race') {
|
|
43
|
+
return `another launch of order id "${orderId}" registered ${run} while this one was preparing; ` +
|
|
44
|
+
'repeat the same command — it attaches to that run.';
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`unknown order-launch-exclusive variant: ${variant}`);
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
'agent-call-model': Object.freeze({
|
|
50
|
+
data: Object.freeze(['agentType']),
|
|
51
|
+
topic: 'order',
|
|
52
|
+
rule: 'Call a dispatcher without model; its installed agent file owns the model.',
|
|
53
|
+
message: ({ agentType }) =>
|
|
54
|
+
"Order gate denied the Agent call because it passes `model`: a dispatcher's model is set in one place, "
|
|
55
|
+
+ `its installed agent file. Call ${agentType} without model.`,
|
|
56
|
+
}),
|
|
57
|
+
'agent-call-teammate': Object.freeze({
|
|
58
|
+
data: Object.freeze(['field', 'agentType']),
|
|
59
|
+
topic: 'order',
|
|
60
|
+
rule: 'Call a dispatcher with subagent_type only, without teammate fields.',
|
|
61
|
+
message: ({ field, agentType }) =>
|
|
62
|
+
`Order gate denied the Agent call because it passes \`${field}\`: a dispatcher launched as a `
|
|
63
|
+
+ 'teammate escapes the dispatcher gate, whose agent_type becomes the teammate name. Call '
|
|
64
|
+
+ `${agentType} with subagent_type only, without name or team_name.`,
|
|
65
|
+
}),
|
|
66
|
+
'dispatcher-call-shape': Object.freeze({
|
|
67
|
+
data: Object.freeze(['violation', 'label', 'first', 'line', 'sequence']),
|
|
68
|
+
topic: 'order',
|
|
69
|
+
rule: 'The call contains one exact task file label with a non-empty absolute path safe inside shell double quotes.',
|
|
70
|
+
message: ({ violation, label, first, line, sequence }) => {
|
|
71
|
+
if (violation === 'free-text') return 'not a `label: value` line; free text belongs in the task file';
|
|
72
|
+
if (violation === 'empty') return `label \`${label}\` has an empty value`;
|
|
73
|
+
if (violation === 'duplicate') return `label \`${label}\` is given twice (line ${first} and line ${line})`;
|
|
74
|
+
if (violation === 'spelling') return `write the label exactly as \`${label}:\``;
|
|
75
|
+
if (violation === 'header') {
|
|
76
|
+
return `label \`${label}\` moved to the task-file header; the call is only \`task file: <absolute path>\``;
|
|
77
|
+
}
|
|
78
|
+
if (violation === 'missing') return `missing required label \`${label}\``;
|
|
79
|
+
if (violation === 'placeholder') return `label \`${label}\` is still a placeholder`;
|
|
80
|
+
if (violation === 'absolute') return 'label `task file` must be an absolute path';
|
|
81
|
+
if (violation === 'unsafe') {
|
|
82
|
+
return `label \`${label}\` contains ${JSON.stringify(sequence)}; put free text in the task file`;
|
|
83
|
+
}
|
|
84
|
+
if (violation === 'rewritten') {
|
|
85
|
+
return `label \`${label}\` contains ${JSON.stringify(sequence)}, which the shell rewrites inside double quotes; `
|
|
86
|
+
+ 'write the path with forward slashes and without `$`';
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`unknown dispatcher-call-shape violation: ${violation}`);
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
'dispatcher-task-file-help': Object.freeze({
|
|
92
|
+
data: Object.freeze([]),
|
|
93
|
+
topic: 'order',
|
|
94
|
+
rule: 'The orchestrator writes the task file verbatim with its file tool and supplies its absolute path.',
|
|
95
|
+
message: () => 'Absolute path to a file holding the task statement verbatim, written by the orchestrator with its file tool. The dispatcher passes the path to the runner and never creates, reads or rewrites the file: writing it from the shell reintroduces the permission prompt this file channel exists to remove. Given no path, return the refusal.',
|
|
96
|
+
}),
|
|
97
|
+
'dispatcher-call-refusal': Object.freeze({
|
|
98
|
+
data: Object.freeze(['part', 'line', 'text', 'label', 'example']),
|
|
99
|
+
topic: 'order',
|
|
100
|
+
rule: 'The Agent call text is only `task file: <absolute path>`; every order label goes in the task-file header ' +
|
|
101
|
+
'and free text in the task file body.',
|
|
102
|
+
message: ({ part, line, text, label, example, reason, detail, explanation, reasons }) => {
|
|
103
|
+
if (part === 'intro') return 'Order gate denied the Agent call: the call text must be only `label: value` lines.';
|
|
104
|
+
if (part === 'detail') return ` — ${explanation} Example: \`${label}: ${example}\`.`;
|
|
105
|
+
if (part === 'problem') return `- ${reason}${detail}`;
|
|
106
|
+
if (part === 'line') return `- line ${line}: \`${text}\` — ${reason}`;
|
|
107
|
+
if (part === 'call') return 'The call is only `task file: <absolute path>`.';
|
|
108
|
+
if (part === 'header') return 'The order goes in the header at the top of that file:';
|
|
109
|
+
if (part === 'free-text') return 'Free text belongs in the task file.';
|
|
110
|
+
if (part === 'command-line') return `line ${line}: ${reason}`;
|
|
111
|
+
if (part === 'command') return `the call text must be only label: value lines: ${reasons}`;
|
|
112
|
+
throw new Error(`unknown dispatcher-call-refusal part: ${part}`);
|
|
113
|
+
},
|
|
114
|
+
}),
|
|
115
|
+
'dispatcher-call-summary': Object.freeze({
|
|
116
|
+
data: Object.freeze(['variant', 'required', 'conditional', 'optional']),
|
|
117
|
+
topic: 'order',
|
|
118
|
+
rule: 'Each dispatcher description names the header labels its task file requires, the conditional ones ' +
|
|
119
|
+
'with their condition, and the optional ones.',
|
|
120
|
+
message: ({ variant, required, conditional, optional }) => {
|
|
121
|
+
if (variant === 'unknown-agent') return '';
|
|
122
|
+
if (variant === 'labels') {
|
|
123
|
+
return `The call is only \`task file\` (an absolute path). The task-file header requires ${required.join(', ')}; `
|
|
124
|
+
+ `conditional labels: ${conditional.join(', ')}; optional labels: ${optional.join(', ')}. `
|
|
125
|
+
+ 'Free text belongs in the task file.';
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`unknown dispatcher-call-summary variant: ${variant}`);
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
});
|