@lyupro/codex-bridge 0.6.10 → 0.6.11
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 +81 -0
- package/README.md +15 -4
- package/cli/dispatcher-model-check.mjs +59 -0
- package/cli/doctor-host-side.mjs +2 -1
- package/cli/doctor-installation.mjs +111 -111
- package/cli/doctor.mjs +13 -5
- package/cli/handback-witness-check.mjs +24 -18
- package/cli/hosts.mjs +2 -10
- package/cli/install-owners.mjs +1 -4
- package/cli/install-record.mjs +2 -1
- package/cli/inventory-transition.mjs +1 -1
- package/cli/manifest.mjs +6 -6
- package/package.json +2 -2
- package/src/agents/codex-advisor.md +25 -119
- package/src/agents/codex-build.md +15 -133
- package/src/agents/codex-review.md +10 -109
- package/src/agents/codex-scout.md +11 -130
- package/src/home/hooks/dispatcher-gate.mjs +14 -9
- package/src/home/hooks/live-runs.mjs +0 -41
- package/src/home/hooks/order-gate.mjs +59 -40
- package/src/home/hooks/reply-guard.mjs +52 -65
- package/src/home/hooks/reply-identity.mjs +64 -0
- package/src/home/hooks/reply-verdicts.mjs +31 -27
- package/src/home/hooks/reply-witness.mjs +48 -0
- package/src/home/lib/claude-layout.mjs +14 -0
- package/src/home/lib/dispatcher-call.mjs +171 -0
- package/src/home/lib/dispatcher-command.mjs +18 -56
- package/src/home/lib/dispatcher-gate.mjs +4 -0
- package/src/home/lib/dispatcher-model.mjs +112 -0
- package/src/home/lib/dispatcher-order.mjs +21 -22
- package/src/home/lib/dispatcher-pin.mjs +60 -0
- package/src/home/lib/dispatcher-protocol.mjs +91 -0
- package/src/home/lib/dispatcher-stop.mjs +11 -2
- package/src/home/lib/handback-witness.mjs +137 -48
- package/src/home/lib/home-registry.mjs +7 -0
- package/src/home/lib/install-owner-roots.mjs +53 -0
- package/src/home/lib/meta/paths.mjs +1 -1
- package/src/home/lib/meta/reply.mjs +2 -1
- package/src/home/lib/model-family.mjs +38 -0
- package/src/home/lib/observation-ledger.mjs +145 -0
- package/src/home/lib/order-schema.mjs +204 -0
- package/src/home/lib/order-values.mjs +46 -0
- package/src/home/lib/runner/args.mjs +140 -231
- package/src/home/lib/runner/attach.mjs +3 -3
- package/src/home/lib/runner/continuation.mjs +174 -192
- package/src/home/lib/runner/launcher.mjs +9 -9
- package/src/home/lib/runner/order-options.mjs +30 -0
- package/src/home/lib/runner/order-owner.mjs +2 -1
- package/src/home/lib/runner/pass-gate.mjs +11 -9
- package/src/home/lib/runner/preflight.mjs +4 -3
- package/src/home/lib/runner/scope-check.mjs +11 -10
- package/src/home/lib/runner/task-input.mjs +23 -10
- package/src/home/lib/subagent-model-env.mjs +44 -0
- package/src/home/lib/task-header.mjs +158 -127
- package/src/home/lib/untyped-stop.mjs +30 -0
- package/src/home/lib/required-inputs.mjs +0 -301
- /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
|
@@ -8,6 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
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
|
+
import { orderInputName } from '../order-schema.mjs';
|
|
11
12
|
|
|
12
13
|
// Directories a self-walk must never descend into. Only reached when the repository has no git:
|
|
13
14
|
// with git present the file list comes from the repository itself, which already knows what is
|
|
@@ -89,14 +90,14 @@ function noMatchRefusal(pattern, repoRoot, gitListed) {
|
|
|
89
90
|
pattern,
|
|
90
91
|
reason: `does not match any existing path under ${repoRoot}; ` +
|
|
91
92
|
'git could not list that folder as a repository, so it was walked instead',
|
|
92
|
-
action:
|
|
93
|
+
action: `start the run from the repository root (cd into it, or pass ${orderInputName('repository')} <repository root>); ` +
|
|
93
94
|
'if this folder is the intended one, correct the pattern',
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
return {
|
|
97
98
|
pattern,
|
|
98
99
|
reason: 'does not match any existing path in the repository',
|
|
99
|
-
action:
|
|
100
|
+
action: `correct the pattern or declare an intentionally new path with ${orderInputName('scope new')}`,
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
|
|
@@ -172,14 +173,14 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
|
|
|
172
173
|
const allPatterns = [...declared, ...newPaths];
|
|
173
174
|
const newPathKeys = new Set(newPaths.map((pattern) => normalizePath(pattern)));
|
|
174
175
|
|
|
175
|
-
// The
|
|
176
|
-
// pattern into the declared list, so position named
|
|
176
|
+
// The label is decided by `scope new:` membership, not list position: orderOptions() merges every
|
|
177
|
+
// `scope new:` pattern into the declared list, so position named `scope:` for a directory given only to `scope new:`
|
|
177
178
|
// (OW-042 B1, caught on the live runner 2026-10-04 after the unit test passed separate lists).
|
|
178
179
|
for (const pattern of allPatterns) {
|
|
179
|
-
const
|
|
180
|
+
const label = newPathKeys.has(normalizePath(pattern)) ? 'scope new' : 'scope';
|
|
180
181
|
const refusal = structuralRefusal(repoRoot, pattern);
|
|
181
|
-
if (refusal) return { pattern,
|
|
182
|
-
if (!pattern) return { ...noMatchRefusal(pattern),
|
|
182
|
+
if (refusal) return { pattern, label, ...refusal };
|
|
183
|
+
if (!pattern) return { ...noMatchRefusal(pattern), label };
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
const required = allPatterns.filter((pattern) => !newPathKeys.has(normalizePath(pattern)));
|
|
@@ -191,7 +192,7 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
|
|
|
191
192
|
} catch (error) {
|
|
192
193
|
return {
|
|
193
194
|
pattern: required[0],
|
|
194
|
-
|
|
195
|
+
label: 'scope',
|
|
195
196
|
reason: `could not parse the pattern: ${error.message}`,
|
|
196
197
|
action: 'correct the pattern and retry',
|
|
197
198
|
};
|
|
@@ -204,11 +205,11 @@ export function validateScope(repoRoot, patterns, scopeNewPatterns = []) {
|
|
|
204
205
|
if (error.code !== 'ERR_GIT_PATH_NOT_UTF8') throw error;
|
|
205
206
|
return {
|
|
206
207
|
pattern: required[0],
|
|
207
|
-
|
|
208
|
+
label: 'scope',
|
|
208
209
|
reason: error.message,
|
|
209
210
|
action: 'rename the non-UTF-8 file using a UTF-8 file name and retry',
|
|
210
211
|
};
|
|
211
212
|
}
|
|
212
213
|
const missing = matchers.find((matcher) => !matches.matched.has(matcher.pattern));
|
|
213
|
-
return missing ? { ...noMatchRefusal(missing.pattern, repoRoot, matches.gitListed),
|
|
214
|
+
return missing ? { ...noMatchRefusal(missing.pattern, repoRoot, matches.gitListed), label: 'scope' } : null;
|
|
214
215
|
}
|
|
@@ -1,25 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settles the order
|
|
3
|
-
*
|
|
2
|
+
* Settles the order header, task, sub-questions and verification command from the document,
|
|
3
|
+
* and refuses before a run folder or a paid process can exist.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* parsing runs before the task file has been read, and the file is read without knowing which
|
|
7
|
-
* flags were passed. Deciding it in either of them alone is how a scout order carrying its
|
|
8
|
-
* questions in the file got refused for not passing --question.
|
|
5
|
+
* Plan_63 D1/D8: transport arguments carry no order fields; file and stdin share this boundary.
|
|
9
6
|
*/
|
|
10
7
|
import { die, readTaskDocument } from './args.mjs';
|
|
11
8
|
import { questionKindRefusal } from './question-kind.mjs';
|
|
9
|
+
import { orderFromHeader, orderInputName, renderOrderProblems } from '../order-schema.mjs';
|
|
10
|
+
import { orderOptions } from './order-options.mjs';
|
|
12
11
|
|
|
13
|
-
export function settleTaskInput(opts) {
|
|
12
|
+
export function settleTaskInput(opts, { cwd = process.cwd() } = {}) {
|
|
14
13
|
const { task, header, questions: fileQuestions, verify: fileVerify } = readTaskDocument(opts);
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
const { order, problems } = orderFromHeader(opts.agent, header);
|
|
15
|
+
if (problems.length) {
|
|
16
|
+
die(renderOrderProblems(opts.agent, problems));
|
|
17
|
+
}
|
|
18
|
+
const normalized = orderOptions(opts.agent, order, { cwd });
|
|
19
|
+
if (normalized.problems.length) {
|
|
20
|
+
die(normalized.problems.map(({ label, reason }) => `${orderInputName(label)} ${reason}`).join('\n'));
|
|
21
|
+
}
|
|
22
|
+
Object.assign(opts, normalized.options);
|
|
23
|
+
opts.continue = Boolean(header.grant);
|
|
24
|
+
opts.questions = fileQuestions.length ? fileQuestions : undefined;
|
|
25
|
+
opts.verify = fileVerify;
|
|
17
26
|
// Still before the run folder exists and before a token of someone else's quota is touched,
|
|
18
27
|
// which is where every refusal of this kind belongs.
|
|
19
28
|
if (opts.agent === 'codex-scout' && !opts.questions?.length) {
|
|
20
29
|
die(
|
|
21
30
|
'a sub-question is required for codex-scout: put one Markdown list item per sub-question ' +
|
|
22
|
-
'under a `Questions` heading in the task
|
|
31
|
+
'under a `Questions` heading in the task document. ' +
|
|
23
32
|
'The runner will not infer questions from the task text; no quota was spent.',
|
|
24
33
|
);
|
|
25
34
|
}
|
|
@@ -31,5 +40,9 @@ export function settleTaskInput(opts) {
|
|
|
31
40
|
);
|
|
32
41
|
}
|
|
33
42
|
}
|
|
43
|
+
if (opts.noWait && opts.continue) {
|
|
44
|
+
die(`--no-wait cannot be combined with a ${orderInputName(header.grant.kind)} grant: ` +
|
|
45
|
+
'checking an existing run must never authorize a new one.');
|
|
46
|
+
}
|
|
34
47
|
return { task, header };
|
|
35
48
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** decides whether the host's subagent-model environment overrides a dispatcher's frontmatter pin
|
|
2
|
+
* Plan_67 D6 (b) and the host documentation facts recorded in its Council (2026-09-25):
|
|
3
|
+
* env moved below frontmatter in 2.1.251; FORCE=1 overrides it starting with 2.1.257.
|
|
4
|
+
* Unknown host versions are not evidence for a pre-start refusal.
|
|
5
|
+
*/
|
|
6
|
+
import { modelFamily } from './model-family.mjs';
|
|
7
|
+
|
|
8
|
+
// 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';
|
|
11
|
+
|
|
12
|
+
function versionParts(value) {
|
|
13
|
+
if (typeof value !== 'string' || !/^\d+\.\d+\.\d+(?:\.\d+)*$/.test(value)) return null;
|
|
14
|
+
return value.split('.').map(BigInt);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function compareVersions(left, right) {
|
|
18
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
19
|
+
const difference = (left[index] ?? 0n) - (right[index] ?? 0n);
|
|
20
|
+
if (difference !== 0n) return difference < 0n ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function envOverridesPin({ env, hostVersion }) {
|
|
26
|
+
const model = env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
27
|
+
if (typeof model !== 'string' || model.length === 0) return false;
|
|
28
|
+
const version = versionParts(hostVersion);
|
|
29
|
+
if (version === null) return false;
|
|
30
|
+
return compareVersions(version, versionParts(FRONTMATTER_PRECEDENCE_VERSION)) < 0
|
|
31
|
+
|| (env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE === '1'
|
|
32
|
+
&& compareVersions(version, versionParts(SUBAGENT_MODEL_FORCE_VERSION)) >= 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function envModelProblem({ env, hostVersion, pinFamily }) {
|
|
36
|
+
if (!envOverridesPin({ env, hostVersion })) return null;
|
|
37
|
+
const model = env.CLAUDE_CODE_SUBAGENT_MODEL;
|
|
38
|
+
const family = modelFamily(model);
|
|
39
|
+
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.';
|
|
44
|
+
}
|
|
@@ -1,127 +1,158 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reads the orchestrator's metadata header at the top of a task file — the one place a grant or an
|
|
3
|
-
* advice line may stand.
|
|
4
|
-
*
|
|
5
|
-
* Plan_75 D5: on 2026-10-03 20:42 wrapped prose in a task file began a line with `continue:`, and
|
|
6
|
-
* the runner, which looked for grants on any line, refused the order over its own prose. Grants and
|
|
7
|
-
* advice had two parsers and the task hash a third rule; this module is the single reader, and a
|
|
8
|
-
* line below the header that still looks like real metadata is refused rather than silently obeyed
|
|
9
|
-
* or silently ignored.
|
|
10
|
-
*/
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { splitGrantValue } from './
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
spellings.push(`${wrapper}${label}[ \\t]*[:=]
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
descriptor('
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
line
|
|
84
|
-
reason
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Reads the orchestrator's metadata header at the top of a task file — the one place a grant or an
|
|
3
|
+
* advice line may stand.
|
|
4
|
+
*
|
|
5
|
+
* Plan_75 D5: on 2026-10-03 20:42 wrapped prose in a task file began a line with `continue:`, and
|
|
6
|
+
* the runner, which looked for grants on any line, refused the order over its own prose. Grants and
|
|
7
|
+
* advice had two parsers and the task hash a third rule; this module is the single reader, and a
|
|
8
|
+
* line below the header that still looks like real metadata is refused rather than silently obeyed
|
|
9
|
+
* or silently ignored.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { splitGrantValue } from './order-values.mjs';
|
|
13
|
+
import { ALL_ORDER_LABELS } from './order-schema.mjs';
|
|
14
|
+
|
|
15
|
+
const BARE_RUN = /^(?!\.{1,2}$)[A-Za-z0-9._-]+$/;
|
|
16
|
+
const MISPLACED_GRANT = /^[A-Za-z0-9._-]+(?:$| — | - |:)/;
|
|
17
|
+
|
|
18
|
+
function validateGrant(value) {
|
|
19
|
+
const grant = splitGrantValue(value);
|
|
20
|
+
return grant && BARE_RUN.test(grant.run)
|
|
21
|
+
? null
|
|
22
|
+
: 'grant must name a bare run folder (not . or ..) and a non-placeholder reason';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function descriptor(label, validate, misplaced) {
|
|
26
|
+
const spellings = [`${label}[ \\t]*[:=]`];
|
|
27
|
+
for (const wrapper of ['\\*', '_', '`', '\\*\\*', '__']) {
|
|
28
|
+
spellings.push(`${wrapper}${label}${wrapper}[ \\t]*[:=]`);
|
|
29
|
+
spellings.push(`${wrapper}${label}[ \\t]*[:=]${wrapper}`);
|
|
30
|
+
}
|
|
31
|
+
return Object.freeze({
|
|
32
|
+
label,
|
|
33
|
+
validate,
|
|
34
|
+
misplaced,
|
|
35
|
+
shape: new RegExp(
|
|
36
|
+
// Plan_75 D5: try a wrapper before treating its first star as a list marker.
|
|
37
|
+
`^[ \\t]*(?:[-*][ \\t]*)??(?:${spellings.join('|')})[ \\t]*(.*?)[ \\t]*$`,
|
|
38
|
+
'i',
|
|
39
|
+
),
|
|
40
|
+
repairHint: `move it into the header at the top of the file, spelled ${label}: <value>; `
|
|
41
|
+
+ 'if it is an example, quote it with > or reword it inside a sentence',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Plan_75 D5, 2026-10-03 20:42: one registry prevents wrapped task prose becoming a grant.
|
|
46
|
+
const LABELS = Object.freeze([
|
|
47
|
+
// Plan_75 D5: empty advice closes the strict header but must still explain the missing value.
|
|
48
|
+
descriptor('advice', (value) => value.trim() && !/[\r\n]/.test(value)
|
|
49
|
+
? null : 'advice value must be non-empty and single line',
|
|
50
|
+
(value) => !value || /^\S+$/.test(value)
|
|
51
|
+
|| path.posix.isAbsolute(value) || path.win32.isAbsolute(value)),
|
|
52
|
+
descriptor('continue', validateGrant, (value) => MISPLACED_GRANT.test(value)),
|
|
53
|
+
descriptor('retry', validateGrant, (value) => MISPLACED_GRANT.test(value)),
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const HEADER_LABELS = Object.freeze([...ALL_ORDER_LABELS, 'advice']);
|
|
57
|
+
const GUARDED_LABELS = Object.freeze(LABELS.map(({ label }) => label));
|
|
58
|
+
const ORDER_ONLY_LABELS = Object.freeze(HEADER_LABELS.filter((label) => !GUARDED_LABELS.includes(label)));
|
|
59
|
+
const HEADER_LINE = new RegExp(`^(${HEADER_LABELS.join('|')}):[ \\t]*(\\S.*?)[ \\t]*$`);
|
|
60
|
+
|
|
61
|
+
function normalizedHeaderLabel(line) {
|
|
62
|
+
const colon = line.indexOf(':');
|
|
63
|
+
if (colon === -1) return null;
|
|
64
|
+
return line.slice(0, colon).trim().toLowerCase()
|
|
65
|
+
.replace(/^[-*][ \t]+/, '')
|
|
66
|
+
.replace(/^[*`_]+|[*`_]+$/g, '')
|
|
67
|
+
.replace(/[-_]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseTaskHeader(text) {
|
|
71
|
+
const lines = text.replace(/^\uFEFF/, '').split(/\r\n|\n|\r/);
|
|
72
|
+
const fields = {};
|
|
73
|
+
const entries = [];
|
|
74
|
+
const problems = [];
|
|
75
|
+
let headerEnd = 0;
|
|
76
|
+
|
|
77
|
+
for (const line of lines) {
|
|
78
|
+
const match = line.match(HEADER_LINE);
|
|
79
|
+
if (!match) {
|
|
80
|
+
// Plan_63 D1/D3: a misspelled or unknown order line must not silently turn into task prose. The
|
|
81
|
+
// header still closes here (Plan_75 D5), and grant/advice spellings stay with the misplaced guard.
|
|
82
|
+
const label = normalizedHeaderLabel(line);
|
|
83
|
+
const rawLabel = line.slice(0, line.indexOf(':')).trim();
|
|
84
|
+
let reason = null;
|
|
85
|
+
if (ORDER_ONLY_LABELS.includes(label)) {
|
|
86
|
+
reason = line.trimEnd() === `${label}:` ? `header label "${label}" has an empty value`
|
|
87
|
+
: `write the header label exactly as "${label}:"`;
|
|
88
|
+
} else if (!GUARDED_LABELS.includes(label) && entries.length && /^[a-z][a-z _-]*:[ \t]*\S/.test(line)) {
|
|
89
|
+
reason = `unknown header label "${rawLabel}"; known labels: ${HEADER_LABELS.join(', ')}`;
|
|
90
|
+
}
|
|
91
|
+
if (reason) problems.push({ lineNo: headerEnd + 1, line, reason });
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
const [, label, value] = match;
|
|
95
|
+
const entry = { label, value, lineNo: headerEnd + 1, line };
|
|
96
|
+
if (Object.hasOwn(fields, label)) {
|
|
97
|
+
problems.push({ lineNo: entry.lineNo, line, reason: `duplicate ${label} label` });
|
|
98
|
+
} else {
|
|
99
|
+
fields[label] = value;
|
|
100
|
+
}
|
|
101
|
+
entries.push(entry);
|
|
102
|
+
headerEnd++;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Plan_75 D5: conflicting authorization is a label-presence error even if a value is invalid.
|
|
106
|
+
if (Object.hasOwn(fields, 'continue') && Object.hasOwn(fields, 'retry')) {
|
|
107
|
+
const conflict = entries.find(({ label }) => label === 'retry');
|
|
108
|
+
problems.push({
|
|
109
|
+
lineNo: conflict.lineNo,
|
|
110
|
+
line: conflict.line,
|
|
111
|
+
reason: 'continue and retry cannot both be present; keep exactly one grant label',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let grant = null;
|
|
116
|
+
let advice = null;
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
const definition = LABELS.find(({ label }) => label === entry.label);
|
|
119
|
+
if (!definition) continue;
|
|
120
|
+
const reason = definition.validate(entry.value);
|
|
121
|
+
if (reason) {
|
|
122
|
+
problems.push({ lineNo: entry.lineNo, line: entry.line, reason: `${entry.label}: ${reason}` });
|
|
123
|
+
} else if (entry.value !== fields[entry.label]) {
|
|
124
|
+
continue;
|
|
125
|
+
} else if (entry.label === 'advice') {
|
|
126
|
+
advice ??= entry.value;
|
|
127
|
+
} else {
|
|
128
|
+
grant ??= { kind: entry.label, ...splitGrantValue(entry.value) };
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Plan_75 D5, 2026-10-03 20:42: fences are examples, not an escape from the misplaced guard.
|
|
133
|
+
for (let index = headerEnd; index < lines.length; index++) {
|
|
134
|
+
for (const definition of LABELS) {
|
|
135
|
+
const match = lines[index].match(definition.shape);
|
|
136
|
+
if (!match || !definition.misplaced(match[1])) continue;
|
|
137
|
+
const reason = match[1] ? `misplaced ${definition.label} metadata`
|
|
138
|
+
: `${definition.label} value must be non-empty and single line`;
|
|
139
|
+
problems.push({
|
|
140
|
+
lineNo: index + 1,
|
|
141
|
+
line: lines[index],
|
|
142
|
+
reason: `${reason}; ${definition.repairHint}`,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
problems.sort((a, b) => a.lineNo - b.lineNo);
|
|
148
|
+
const parsed = { fields, grant, advice, body: lines.slice(headerEnd).join('\n'), problems };
|
|
149
|
+
// Plan_63 C1: retain source locations for schema diagnostics without changing serialized results.
|
|
150
|
+
Object.defineProperty(parsed, 'entries', { value: entries });
|
|
151
|
+
return parsed;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function taskHeaderRefusal(parsed) {
|
|
155
|
+
if (!parsed.problems.length) return null;
|
|
156
|
+
return parsed.problems.map(({ lineNo, line, reason }) => `line ${lineNo}: ${reason}: ${line}`)
|
|
157
|
+
.concat('The run folder was not created; quota was not spent.').join('\n');
|
|
158
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** decides whether a subagent stop without agent_type was a Codex dispatcher, from evidence bound to that stop
|
|
2
|
+
* Plan_67 D9: all 13 missing-type alarms across nine host versions were noise; require evidence
|
|
3
|
+
* for this stop instead of assuming every untyped subagent is a dispatcher.
|
|
4
|
+
*/
|
|
5
|
+
import { CLI_NAMES } from './cli-names.mjs';
|
|
6
|
+
import { ORDER_AGENTS } from './order-schema.mjs';
|
|
7
|
+
|
|
8
|
+
export function untypedStopEvidence({ state, toolUses }) {
|
|
9
|
+
if (state && typeof state === 'object' && !Array.isArray(state) && !state.corrupt
|
|
10
|
+
&& typeof state.agentType === 'string' && ORDER_AGENTS.includes(state.agentType)
|
|
11
|
+
&& Array.isArray(state.seenToolUseIds) && state.seenToolUseIds.length > 0) {
|
|
12
|
+
return { dispatcher: true, via: 'gate-state', agentType: state.agentType };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (toolUses !== null) {
|
|
16
|
+
for (const toolUse of toolUses) {
|
|
17
|
+
if (toolUse?.name !== 'Bash' || typeof toolUse.command !== 'string'
|
|
18
|
+
|| /[;&|`$<>\r\n]/.test(toolUse.command)) continue;
|
|
19
|
+
const tokens = toolUse.command.trim().split(/\s+/);
|
|
20
|
+
if (!CLI_NAMES.includes(tokens[0]) || tokens[1] !== 'run') continue;
|
|
21
|
+
for (let index = 2; index < tokens.length - 1; index += 1) {
|
|
22
|
+
if (tokens[index] === '--agent' && ORDER_AGENTS.includes(tokens[index + 1])) {
|
|
23
|
+
return { dispatcher: true, via: 'run-command', runAgent: tokens[index + 1] };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { dispatcher: false, reason: toolUses === null ? 'transcript-unreadable' : 'no-evidence' };
|
|
30
|
+
}
|