@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
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { firstShellUnsafeSequence } from './shell-unsafe.mjs';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Defines the inputs the orchestrator must give each Codex dispatcher.
|
|
6
|
-
*
|
|
7
|
-
* This contract belongs beside the gate because the dispatcher prompt was the only place that
|
|
8
|
-
* named order id and scope, so the caller launched codex-build without either value and the
|
|
9
|
-
* runner refused the call before Codex could do any work. Pass 2 can render the same entries
|
|
10
|
-
* into agent instructions without creating a second list that can drift.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
const PLACEHOLDER_VALUES = new Set(['todo', 'tbd', 'label', 'none', 'order id', 'scope', 'xxx']);
|
|
14
|
-
|
|
15
|
-
const freezeEntries = (entries) => Object.freeze(entries.map((entry) => Object.freeze(entry)));
|
|
16
|
-
|
|
17
|
-
export const CONTINUATION_INPUT = Object.freeze({
|
|
18
|
-
label: 'continue',
|
|
19
|
-
source: 'the orchestrator',
|
|
20
|
-
explanation:
|
|
21
|
-
'The run folder this pass is ordered to continue, followed by why the orchestrator is spending another pass. A continuation is assigned by the orchestrator; after a verdict, the dispatcher returns it and stops.',
|
|
22
|
-
example: '2026-08-05_092913_plan14-build — LIMIT at step 3, tests unwritten',
|
|
23
|
-
conditional: 'when --continue is passed',
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
export const RETRY_INPUT = Object.freeze({
|
|
27
|
-
label: 'retry',
|
|
28
|
-
source: 'the orchestrator',
|
|
29
|
-
explanation:
|
|
30
|
-
'The failed run this pass repeats, followed by why the orchestrator pays for the same pass again. A retry repeats that failed pass under its own order; it does not authorize the next pass.',
|
|
31
|
-
example: '2026-10-03_172017_cc-d66-advisor — model at capacity, same pass again',
|
|
32
|
-
conditional: 'when --continue is passed',
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The task statement reaches the runner as a file because every other channel puts the invocation
|
|
37
|
-
* back into the multi-line form that no permission rule can cover — a heredoc on stdin, or a
|
|
38
|
-
* quoted argument the shell mangles. The orchestrator writes that file with its own file tool,
|
|
39
|
-
* which never crosses the shell at all.
|
|
40
|
-
*
|
|
41
|
-
* It is listed as a required input, not merely explained in the prompt body, because the rendered
|
|
42
|
-
* summary is the ONLY part of this contract the orchestrator ever reads. Left out of the list, a
|
|
43
|
-
* dispatcher that was given no path filled the gap itself: on 2026-08-15 codex-build wrote the file
|
|
44
|
-
* with `cat > … << 'EOF'` and earned exactly the permission window the file was introduced to end.
|
|
45
|
-
* The 2026-08-15 relative-path incident also made the repository cwd silently choose a different
|
|
46
|
-
* task.md, so both the producer gate and runner use this cross-platform absolute-path contract.
|
|
47
|
-
*/
|
|
48
|
-
export const TASK_FILE_INPUT = Object.freeze({
|
|
49
|
-
label: 'task file',
|
|
50
|
-
source: 'the orchestrator',
|
|
51
|
-
explanation: 'Absolute path to a file holding the task statement verbatim, written by the orchestrator with its file tool. The dispatcher passes it as --task-file and never creates, reads or rewrites it: writing it from the shell reintroduces the permission prompt this flag exists to remove. Given no path, start the runner without the flag and return its refusal.',
|
|
52
|
-
example: 'C:/Users/me/AppData/Local/Temp/claude/<session>/scratchpad/task-plan-13.md',
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export const REQUIRED_INPUTS = Object.freeze({
|
|
56
|
-
'codex-scout': freezeEntries([
|
|
57
|
-
{
|
|
58
|
-
label: 'order id',
|
|
59
|
-
source: 'the orchestrator',
|
|
60
|
-
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
61
|
-
example: 'plan-13-scout-20260804',
|
|
62
|
-
},
|
|
63
|
-
TASK_FILE_INPUT,
|
|
64
|
-
CONTINUATION_INPUT,
|
|
65
|
-
RETRY_INPUT,
|
|
66
|
-
]),
|
|
67
|
-
'codex-build': freezeEntries([
|
|
68
|
-
{
|
|
69
|
-
label: 'order id',
|
|
70
|
-
source: 'the orchestrator',
|
|
71
|
-
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
72
|
-
example: 'plan-13-build-20260804',
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
label: 'scope',
|
|
76
|
-
source: 'the orchestrator',
|
|
77
|
-
explanation: 'Comma-separated globs relative to the repository root, listing every file the run may touch — including each caller of what changes, not only the file being edited. Anything outside the list fails the run.',
|
|
78
|
-
example: 'src/home/lib/runner/**,tests/runner/**',
|
|
79
|
-
},
|
|
80
|
-
TASK_FILE_INPUT,
|
|
81
|
-
CONTINUATION_INPUT,
|
|
82
|
-
RETRY_INPUT,
|
|
83
|
-
]),
|
|
84
|
-
'codex-review': freezeEntries([
|
|
85
|
-
{
|
|
86
|
-
label: 'order id',
|
|
87
|
-
source: 'the orchestrator',
|
|
88
|
-
explanation: 'The label this order is known by. Repeating a call with the same label joins the run already in flight and costs no quota; a different piece of work needs a new label, never a reused one.',
|
|
89
|
-
example: 'plan-13-review-20260804',
|
|
90
|
-
},
|
|
91
|
-
TASK_FILE_INPUT,
|
|
92
|
-
CONTINUATION_INPUT,
|
|
93
|
-
RETRY_INPUT,
|
|
94
|
-
]),
|
|
95
|
-
// Plan_59 D7: the caller must name the phase and authorize the continued decision pass.
|
|
96
|
-
'codex-advisor': freezeEntries([
|
|
97
|
-
{
|
|
98
|
-
label: 'order id',
|
|
99
|
-
source: 'the orchestrator',
|
|
100
|
-
explanation: 'The label this design order is known by. Keep the same order id for scope and advise so phase 2 can settle the risks predicted in phase 1.',
|
|
101
|
-
example: 'plan-59-advisor-20260922',
|
|
102
|
-
},
|
|
103
|
-
TASK_FILE_INPUT,
|
|
104
|
-
CONTINUATION_INPUT,
|
|
105
|
-
RETRY_INPUT,
|
|
106
|
-
{
|
|
107
|
-
label: 'phase',
|
|
108
|
-
source: 'the orchestrator',
|
|
109
|
-
explanation: 'Pass scope first to predict risks and check the reading boundary, then advise with --continue and a continuation grant naming the scope run of the same order.',
|
|
110
|
-
example: 'scope',
|
|
111
|
-
},
|
|
112
|
-
]),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
116
|
-
|
|
117
|
-
function cleanValue(value) {
|
|
118
|
-
if (typeof value !== 'string') return '';
|
|
119
|
-
return value.trim().replace(/^([`'\"])(.*)\1$/, '$2').trim();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function isAbsoluteTaskFilePath(value) {
|
|
123
|
-
const cleaned = cleanValue(value);
|
|
124
|
-
return path.posix.isAbsolute(cleaned) || path.win32.isAbsolute(cleaned);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function isPlaceholder(value) {
|
|
128
|
-
const cleaned = cleanValue(value);
|
|
129
|
-
if (!cleaned) return true;
|
|
130
|
-
if (cleaned.startsWith('<') && cleaned.endsWith('>')) return true;
|
|
131
|
-
return PLACEHOLDER_VALUES.has(cleaned.replace(/[.,;:]+$/, '').trim().toLowerCase());
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Plan_59: scope is a real phase value, while it remains a placeholder for existing inputs.
|
|
135
|
-
export const isInputPlaceholder = (value, label) =>
|
|
136
|
-
!(label === 'phase' && cleanValue(value) === 'scope') && isPlaceholder(value);
|
|
137
|
-
|
|
138
|
-
// Plan_75 D5: dispatcher prompts still need one spelling of a labelled line for reading values.
|
|
139
|
-
const labelledLine = (label) => {
|
|
140
|
-
const labelPattern = escapeRegExp(label).replaceAll('\\ ', '\\s+');
|
|
141
|
-
return `(?:[-*]\\s*)?(?:[*_` + '`' + `]?${labelPattern}[*_` + '`' + `]?)\\s*(?::|=|—|-)`;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export function extractValue(promptText, label) {
|
|
145
|
-
const flag = `--${label.replaceAll(/\s+/g, '-')}`;
|
|
146
|
-
const direct = new RegExp(`(?:^|\\r?\\n)\\s*${labelledLine(label)}\\s*([^\\r\\n]*)`, 'im');
|
|
147
|
-
const directMatch = promptText.match(direct);
|
|
148
|
-
if (directMatch) return cleanValue(directMatch[1]);
|
|
149
|
-
|
|
150
|
-
const flagPattern = new RegExp(
|
|
151
|
-
`${escapeRegExp(flag)}(?:=|\\s+)(?:"([^"]*)"|'([^']*)'|([^\\s\\r\\n]+))`,
|
|
152
|
-
'i',
|
|
153
|
-
);
|
|
154
|
-
const flagMatch = promptText.match(flagPattern);
|
|
155
|
-
if (!flagMatch) return null;
|
|
156
|
-
return cleanValue(flagMatch[1] ?? flagMatch[2] ?? flagMatch[3]);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const MAX_DIAGNOSIS_LINE_LENGTH = 160;
|
|
160
|
-
|
|
161
|
-
function readableDiagnosisLine(line) {
|
|
162
|
-
const trimmed = line.trim();
|
|
163
|
-
if (trimmed.length <= MAX_DIAGNOSIS_LINE_LENGTH) return trimmed;
|
|
164
|
-
return `${trimmed.slice(0, MAX_DIAGNOSIS_LINE_LENGTH - 1)}…`;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Explains the Plan_28 incident without widening the strict input parser's accepted spellings. */
|
|
168
|
-
export function diagnoseInput(promptText, label) {
|
|
169
|
-
if (typeof label !== 'string' || !label.trim()) return null;
|
|
170
|
-
const prompt = typeof promptText === 'string' ? promptText : '';
|
|
171
|
-
const labelPattern = escapeRegExp(label).replaceAll('\\ ', '\\s+');
|
|
172
|
-
const candidatePattern = new RegExp(
|
|
173
|
-
`^[ \\t]*(?:[-*][ \\t]*)?(?:[*_` + '`' + `]?${labelPattern}[*_` + '`' + `]?)(?=$|[^A-Za-z0-9_])[^\\r\\n]*`,
|
|
174
|
-
'i',
|
|
175
|
-
);
|
|
176
|
-
const candidateLine = prompt.split(/\r?\n/).find((line) => candidatePattern.test(line));
|
|
177
|
-
if (candidateLine === undefined) return null;
|
|
178
|
-
|
|
179
|
-
const value = extractValue(candidateLine, label);
|
|
180
|
-
if (value !== null && !isInputPlaceholder(value, label)) {
|
|
181
|
-
if (label === TASK_FILE_INPUT.label && !isAbsoluteTaskFilePath(value)) {
|
|
182
|
-
return { line: readableDiagnosisLine(candidateLine), reason: `value \`${value}\` is not an absolute path` };
|
|
183
|
-
}
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const line = readableDiagnosisLine(candidateLine);
|
|
188
|
-
if (value !== null && isInputPlaceholder(value, label)) {
|
|
189
|
-
const displayedValue = value.trim();
|
|
190
|
-
if (!displayedValue) return { line, reason: 'value is empty; replace it with a concrete value' };
|
|
191
|
-
return {
|
|
192
|
-
line,
|
|
193
|
-
reason: `value \`${displayedValue}\` is a placeholder; replace it with a concrete value`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const labelTail = candidateLine.match(
|
|
198
|
-
new RegExp(`^[ \\t]*(?:[-*][ \\t]*)?(?:[*_` + '`' + `]?${labelPattern}[*_` + '`' + `]?)([^\\r\\n]*)$`, 'i'),
|
|
199
|
-
)?.[1] ?? '';
|
|
200
|
-
if (labelTail.includes(':')) {
|
|
201
|
-
return { line, reason: `expected \`${label}:\` with nothing between the label and the colon` };
|
|
202
|
-
}
|
|
203
|
-
return { line, reason: `expected \`${label}: value\` with a separator immediately after the label` };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Plan_75 D1, TradeForge capacity incident (2026-10-03): both grants need the same run/reason grammar.
|
|
207
|
-
function parseLabelledGrant(promptText, label) {
|
|
208
|
-
const prompt = typeof promptText === 'string' ? promptText : '';
|
|
209
|
-
const value = extractValue(prompt, label);
|
|
210
|
-
return splitGrantValue(value);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Plan_75 D5, 2026-10-03 20:42: header parsing must reuse the existing grant value grammar.
|
|
214
|
-
export function splitGrantValue(value) {
|
|
215
|
-
if (isPlaceholder(value)) return null;
|
|
216
|
-
|
|
217
|
-
for (const separator of [/\s+—\s+/, /\s+-\s+/, /\s*:\s*/]) {
|
|
218
|
-
const match = value.match(new RegExp(`^(.+?)${separator.source}(.+)$`));
|
|
219
|
-
if (!match) continue;
|
|
220
|
-
const run = cleanValue(match[1]);
|
|
221
|
-
const reason = cleanValue(match[2]);
|
|
222
|
-
if (isPlaceholder(run) || isPlaceholder(reason)) return null;
|
|
223
|
-
return { run, reason };
|
|
224
|
-
}
|
|
225
|
-
return null;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/** Keeps the 2026-08-05 continuation incident's run and reason in the shared input parser. */
|
|
229
|
-
export function parseContinuationGrant(promptText) {
|
|
230
|
-
return parseLabelledGrant(promptText, CONTINUATION_INPUT.label);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function parseRetryGrant(promptText) {
|
|
234
|
-
return parseLabelledGrant(promptText, RETRY_INPUT.label);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export function parseGrant(promptText) {
|
|
238
|
-
const continuation = parseContinuationGrant(promptText);
|
|
239
|
-
const retry = parseRetryGrant(promptText);
|
|
240
|
-
if (continuation && retry) {
|
|
241
|
-
return { error: 'exactly one grant per task: remove either the continue: or the retry: line' };
|
|
242
|
-
}
|
|
243
|
-
if (continuation) return { kind: 'continue', ...continuation };
|
|
244
|
-
if (retry) return { kind: 'retry', ...retry };
|
|
245
|
-
return null;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/** Returns the immutable input entries for one dispatcher type. */
|
|
249
|
-
export function requiredInputsFor(agentType) {
|
|
250
|
-
return REQUIRED_INPUTS[agentType] || [];
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/** Returns required entries whose value is absent or is still an obvious template placeholder. */
|
|
254
|
-
export function missingInputs(agentType, promptText) {
|
|
255
|
-
const prompt = typeof promptText === 'string' ? promptText : '';
|
|
256
|
-
return requiredInputsFor(agentType).filter((entry) => {
|
|
257
|
-
if (entry.conditional) return false;
|
|
258
|
-
const value = extractValue(prompt, entry.label);
|
|
259
|
-
if (isInputPlaceholder(value, entry.label)) return true;
|
|
260
|
-
return entry === TASK_FILE_INPUT && !isAbsoluteTaskFilePath(value);
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/** Finds labelled command-line values that would disable the host's standing permission. */
|
|
265
|
-
export function shellUnsafeInputs(agentType, promptText) {
|
|
266
|
-
const prompt = typeof promptText === 'string' ? promptText : '';
|
|
267
|
-
return requiredInputsFor(agentType).flatMap((entry) => {
|
|
268
|
-
if (entry.conditional) return [];
|
|
269
|
-
const value = extractValue(prompt, entry.label);
|
|
270
|
-
if (value === null || isInputPlaceholder(value, entry.label)) return [];
|
|
271
|
-
const sequence = firstShellUnsafeSequence(value);
|
|
272
|
-
return sequence === null ? [] : [{ entry, sequence }];
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** Renders the compact contract the orchestrator sees while choosing a dispatcher. */
|
|
277
|
-
export function renderRequiredInputSummary(agentType) {
|
|
278
|
-
const entries = requiredInputsFor(agentType);
|
|
279
|
-
if (!entries.length) return '';
|
|
280
|
-
const labels = entries.map((entry) => {
|
|
281
|
-
const label = `\`${entry.label}\``;
|
|
282
|
-
return entry.conditional ? `${label} (${entry.conditional})` : label;
|
|
283
|
-
});
|
|
284
|
-
// Commas until the last pair: the list grew to three entries when the continuation grant
|
|
285
|
-
// joined it, and "a and b and c" reads as a stutter in the description the orchestrator
|
|
286
|
-
// sees while choosing a dispatcher.
|
|
287
|
-
const rendered = labels.length > 1
|
|
288
|
-
? `${labels.slice(0, -1).join(', ')} and ${labels[labels.length - 1]}`
|
|
289
|
-
: labels[0];
|
|
290
|
-
return `Requires ${entries[0].source}-provided ${rendered}.`;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/** Renders the same contract for pass 2 agent instructions. */
|
|
294
|
-
export function renderRequiredInputs(agentType) {
|
|
295
|
-
return requiredInputsFor(agentType)
|
|
296
|
-
.map((entry) => {
|
|
297
|
-
const condition = entry.conditional ? ` Condition: ${entry.conditional}.` : '';
|
|
298
|
-
return `- ${entry.label}: ${entry.explanation} Example: \`${entry.example}\`.${condition}`;
|
|
299
|
-
})
|
|
300
|
-
.join('\n');
|
|
301
|
-
}
|
|
File without changes
|