@lyupro/codex-bridge 0.6.9 → 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 +132 -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/lifecycle-lock-inspect.mjs +3 -2
- package/cli/lifecycle-lock.mjs +19 -132
- package/cli/manifest.mjs +6 -6
- package/cli/read.mjs +20 -0
- package/cli/settings-merge.mjs +3 -29
- package/package.json +2 -2
- package/scripts/home-write-inventory.mjs +12 -2
- package/src/agents/codex-advisor.md +37 -121
- package/src/agents/codex-build.md +27 -135
- package/src/agents/codex-review.md +19 -111
- package/src/agents/codex-scout.md +21 -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 +80 -47
- package/src/home/hooks/reply-guard.mjs +52 -65
- package/src/home/hooks/reply-identity.mjs +64 -0
- package/src/home/hooks/reply-parser.mjs +1 -1
- 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/command-path.mjs +32 -0
- package/src/home/lib/dispatcher-call.mjs +171 -0
- package/src/home/lib/dispatcher-command.mjs +18 -54
- 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/kernel-lock.mjs +171 -0
- package/src/home/lib/meta/chain.mjs +19 -8
- package/src/home/lib/meta/context-only.mjs +43 -0
- package/src/home/lib/meta/events.mjs +14 -2
- package/src/home/lib/meta/launch-rows.mjs +23 -0
- package/src/home/lib/meta/paths.mjs +1 -1
- package/src/home/lib/meta/reply.mjs +39 -4
- package/src/home/lib/meta/run-state.mjs +3 -0
- package/src/home/lib/meta/verdict.mjs +16 -8
- 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/run-codex.mjs +6 -3
- package/src/home/lib/runner/advise-carry.mjs +2 -1
- package/src/home/lib/runner/args.mjs +140 -220
- package/src/home/lib/runner/attach.mjs +24 -17
- package/src/home/lib/runner/build-evidence.mjs +29 -0
- package/src/home/lib/runner/codex-availability.mjs +50 -0
- package/src/home/lib/runner/codex-cmd.mjs +2 -19
- package/src/home/lib/runner/continuation.mjs +174 -140
- package/src/home/lib/runner/exit-codes.mjs +13 -0
- package/src/home/lib/runner/flag-baseline.mjs +105 -0
- package/src/home/lib/runner/flag-scan.mjs +165 -0
- package/src/home/lib/runner/git-state.mjs +0 -33
- package/src/home/lib/runner/launcher.mjs +237 -277
- package/src/home/lib/runner/order-claim.mjs +84 -0
- package/src/home/lib/runner/order-options.mjs +30 -0
- package/src/home/lib/runner/order-owner.mjs +19 -0
- package/src/home/lib/runner/pass-gate.mjs +111 -0
- package/src/home/lib/runner/preflight.mjs +26 -28
- package/src/home/lib/runner/prompts.mjs +7 -2
- package/src/home/lib/runner/question-kind.mjs +41 -0
- package/src/home/lib/runner/run-env.mjs +2 -1
- package/src/home/lib/runner/sandbox-probe.mjs +1 -1
- package/src/home/lib/runner/scope-check.mjs +31 -12
- package/src/home/lib/runner/task-input.mjs +34 -12
- package/src/home/lib/runner/worker-admission.mjs +43 -0
- package/src/home/lib/runner/worker.mjs +19 -18
- package/src/home/lib/subagent-model-env.mjs +44 -0
- package/src/home/lib/task-header.mjs +158 -0
- package/src/home/lib/untyped-stop.mjs +30 -0
- package/src/home/lib/write-meta.mjs +17 -5
- package/src/home/lib/required-inputs.mjs +0 -261
- /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the dispatcher order registry for task-file headers.
|
|
3
|
+
* Plan_63 D9: the call carries only task file, so order entries belong here without runner flags.
|
|
4
|
+
* The D1/D3 `continue: none` regression still requires one schema for all header readers.
|
|
5
|
+
*/
|
|
6
|
+
import { isInputPlaceholder } from './order-values.mjs';
|
|
7
|
+
import { firstShellUnsafeSequence } from './shell-unsafe.mjs';
|
|
8
|
+
|
|
9
|
+
const freezeEntries = (entries) => Object.freeze(entries.map((entry) => Object.freeze(entry)));
|
|
10
|
+
|
|
11
|
+
export const CONTINUATION_ORDER_INPUT = Object.freeze({
|
|
12
|
+
label: 'continue',
|
|
13
|
+
explanation:
|
|
14
|
+
'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.',
|
|
15
|
+
example: '2026-08-05_092913_plan14-build — LIMIT at step 3, tests unwritten',
|
|
16
|
+
conditional: 'when this pass continues or repeats a named run',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export const RETRY_ORDER_INPUT = Object.freeze({
|
|
20
|
+
label: 'retry',
|
|
21
|
+
explanation:
|
|
22
|
+
'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.',
|
|
23
|
+
example: '2026-10-03_172017_cc-d66-advisor — model at capacity, same pass again',
|
|
24
|
+
conditional: 'when this pass continues or repeats a named run',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const REQUIRED_ORDER_INPUTS = Object.freeze({
|
|
28
|
+
'codex-scout': freezeEntries([
|
|
29
|
+
{
|
|
30
|
+
label: 'order id',
|
|
31
|
+
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.',
|
|
32
|
+
example: 'plan-13-scout-20260804',
|
|
33
|
+
},
|
|
34
|
+
CONTINUATION_ORDER_INPUT,
|
|
35
|
+
RETRY_ORDER_INPUT,
|
|
36
|
+
]),
|
|
37
|
+
'codex-build': freezeEntries([
|
|
38
|
+
{
|
|
39
|
+
label: 'order id',
|
|
40
|
+
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.',
|
|
41
|
+
example: 'plan-13-build-20260804',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: 'scope',
|
|
45
|
+
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.',
|
|
46
|
+
example: 'src/home/lib/runner/**,tests/runner/**',
|
|
47
|
+
},
|
|
48
|
+
CONTINUATION_ORDER_INPUT,
|
|
49
|
+
RETRY_ORDER_INPUT,
|
|
50
|
+
]),
|
|
51
|
+
'codex-review': freezeEntries([
|
|
52
|
+
{
|
|
53
|
+
label: 'order id',
|
|
54
|
+
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.',
|
|
55
|
+
example: 'plan-13-review-20260804',
|
|
56
|
+
},
|
|
57
|
+
CONTINUATION_ORDER_INPUT,
|
|
58
|
+
RETRY_ORDER_INPUT,
|
|
59
|
+
]),
|
|
60
|
+
// Plan_59 D7: the caller must name the phase and authorize the continued decision pass.
|
|
61
|
+
'codex-advisor': freezeEntries([
|
|
62
|
+
{
|
|
63
|
+
label: 'order id',
|
|
64
|
+
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.',
|
|
65
|
+
example: 'plan-59-advisor-20260922',
|
|
66
|
+
},
|
|
67
|
+
CONTINUATION_ORDER_INPUT,
|
|
68
|
+
RETRY_ORDER_INPUT,
|
|
69
|
+
{
|
|
70
|
+
label: 'phase',
|
|
71
|
+
explanation: 'Pass scope first to predict risks and check the reading boundary, then advise with a continue: grant naming the scope run of the same order.',
|
|
72
|
+
example: 'scope',
|
|
73
|
+
},
|
|
74
|
+
]),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export const ORDER_AGENTS = Object.freeze(Object.keys(REQUIRED_ORDER_INPUTS));
|
|
78
|
+
|
|
79
|
+
const OPTIONAL_ORDER_INPUTS = freezeEntries([
|
|
80
|
+
{
|
|
81
|
+
label: 'repository',
|
|
82
|
+
agents: ORDER_AGENTS,
|
|
83
|
+
explanation: 'The repository the runner works in. Pass its path as a labelled value so the dispatcher needs no cd.',
|
|
84
|
+
example: 'C:/work/codex-bridge',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
label: 'scope new',
|
|
88
|
+
agents: Object.freeze(['codex-build']),
|
|
89
|
+
explanation: 'Comma-separated globs for new files this build may create, relative to the repository root.',
|
|
90
|
+
example: 'src/home/lib/dispatcher-call.mjs,tests/dispatcher-call.test.mjs',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
label: 'slug',
|
|
94
|
+
agents: ORDER_AGENTS,
|
|
95
|
+
explanation: 'The readable name used for the run folder. It names this pass without replacing the order id.',
|
|
96
|
+
example: 'plan-76-parser',
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: 'effort',
|
|
100
|
+
agents: ORDER_AGENTS,
|
|
101
|
+
explanation: 'The reasoning effort assigned to this pass. The runner checks which effort values it accepts.',
|
|
102
|
+
example: 'high',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
label: 'changeset',
|
|
106
|
+
agents: Object.freeze(['codex-review']),
|
|
107
|
+
explanation: 'The changes to review: uncommitted, base:<branch>, or commit:<sha>. Name only the ordered changeset.',
|
|
108
|
+
example: 'base:main',
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const schemas = new Map(ORDER_AGENTS.map((agentType) => [agentType,
|
|
113
|
+
freezeEntries([
|
|
114
|
+
...REQUIRED_ORDER_INPUTS[agentType],
|
|
115
|
+
...OPTIONAL_ORDER_INPUTS.filter((entry) => entry.agents.includes(agentType))
|
|
116
|
+
.map((entry) => ({ ...entry, optional: true })),
|
|
117
|
+
].map((entry) => ({
|
|
118
|
+
...entry,
|
|
119
|
+
required: !entry.conditional && !entry.optional,
|
|
120
|
+
conditional: Boolean(entry.conditional),
|
|
121
|
+
optional: Boolean(entry.optional),
|
|
122
|
+
}))),
|
|
123
|
+
]));
|
|
124
|
+
const NO_LABELS = Object.freeze([]);
|
|
125
|
+
|
|
126
|
+
export function orderLabelsFor(agentType) {
|
|
127
|
+
return schemas.get(agentType) ?? NO_LABELS;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const ALL_ORDER_LABELS = Object.freeze([...new Set(
|
|
131
|
+
ORDER_AGENTS.flatMap((agentType) => orderLabelsFor(agentType).map(({ label }) => label)),
|
|
132
|
+
)].sort());
|
|
133
|
+
|
|
134
|
+
const inputNames = new Set(ALL_ORDER_LABELS);
|
|
135
|
+
|
|
136
|
+
// Plan_63 D7: switching the order input channel changes only this function.
|
|
137
|
+
export function orderInputName(label) {
|
|
138
|
+
if (!inputNames.has(label)) throw new Error(`Unknown order input label "${label}"`);
|
|
139
|
+
return `\`${label}:\``;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function orderFromHeader(agentType, parsed) {
|
|
143
|
+
const order = new Map();
|
|
144
|
+
const problems = [];
|
|
145
|
+
if (!schemas.has(agentType)) {
|
|
146
|
+
problems.push({ lineNo: null, line: '', reason: `unknown dispatcher agent "${agentType}"` });
|
|
147
|
+
return { order, problems };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const labels = orderLabelsFor(agentType);
|
|
151
|
+
const accepted = new Set(labels.map(({ label }) => label));
|
|
152
|
+
for (const [label, value] of Object.entries(parsed.fields)) {
|
|
153
|
+
if (!ALL_ORDER_LABELS.includes(label)) continue;
|
|
154
|
+
// parseTaskHeader retains the first duplicate value; diagnostics must name that same line.
|
|
155
|
+
const entry = parsed.entries?.find((entry) => entry.label === label);
|
|
156
|
+
const location = entry ? { lineNo: entry.lineNo, line: entry.line }
|
|
157
|
+
: { lineNo: null, line: '' };
|
|
158
|
+
if (!accepted.has(label)) {
|
|
159
|
+
problems.push({ ...location, reason: `label "${label}" is not accepted by ${agentType}` });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
// Grant values are validated by parseTaskHeader; re-reporting them would duplicate its problem.
|
|
163
|
+
if (label === 'continue' || label === 'retry') continue;
|
|
164
|
+
order.set(label, value);
|
|
165
|
+
if (isInputPlaceholder(value, label)) {
|
|
166
|
+
problems.push({ ...location, reason: `label "${label}" is still a placeholder` });
|
|
167
|
+
}
|
|
168
|
+
// Plan_63 D5: the flag-era refusal (Plan_42) keeps its pair in the header — an order value is a short
|
|
169
|
+
// identifier, so a shell sequence in it is free text written in the wrong place.
|
|
170
|
+
const sequence = firstShellUnsafeSequence(value);
|
|
171
|
+
if (sequence !== null) {
|
|
172
|
+
problems.push({ ...location, reason: `${orderInputName(label)} contains forbidden shell sequence ` +
|
|
173
|
+
`${JSON.stringify(sequence)}; put free text in the task file body and keep header values short` });
|
|
174
|
+
}
|
|
175
|
+
// Plan_63 D5: new paths cannot replace the existing-file boundary of a writing order.
|
|
176
|
+
if (agentType === 'codex-build' && label === 'scope'
|
|
177
|
+
&& !value.split(',').some((pattern) => pattern.trim())) {
|
|
178
|
+
problems.push({ ...location,
|
|
179
|
+
reason: `${orderInputName(label)} is required for codex-build: declare at least one existing path pattern; ` +
|
|
180
|
+
`${orderInputName('scope new')} does not replace it.` });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const { label, required, example } of labels) {
|
|
184
|
+
if (required && !Object.hasOwn(parsed.fields, label)) {
|
|
185
|
+
problems.push({ lineNo: null, line: '',
|
|
186
|
+
reason: `missing required header label "${label}:"; example: ${label}: ${example}` });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return { order, problems };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function renderOrderProblems(agentType, problems) {
|
|
193
|
+
return [...problems.map(({ lineNo, line, reason }) => lineNo === null
|
|
194
|
+
? reason : `line ${lineNo}: ${reason}: ${line}`), renderOrderHeaderHelp(agentType)].join('\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function renderOrderHeaderHelp(agentType) {
|
|
198
|
+
const labels = orderLabelsFor(agentType);
|
|
199
|
+
return [
|
|
200
|
+
...labels.filter(({ required }) => required),
|
|
201
|
+
...labels.filter(({ conditional }) => conditional),
|
|
202
|
+
...labels.filter(({ optional }) => optional),
|
|
203
|
+
].map(({ label, example }) => `${label}: ${example}`).join('\n');
|
|
204
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns one responsibility: the grammar of a single order value.
|
|
3
|
+
* Plan_63 D9: call and header readers share this leaf without importing either registry.
|
|
4
|
+
*/
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const PLACEHOLDER_VALUES = new Set(['todo', 'tbd', 'label', 'none', 'order id', 'scope', 'xxx']);
|
|
8
|
+
|
|
9
|
+
function cleanValue(value) {
|
|
10
|
+
if (typeof value !== 'string') return '';
|
|
11
|
+
return value.trim().replace(/^([`'\"])(.*)\1$/, '$2').trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isAbsoluteTaskFilePath(value) {
|
|
15
|
+
const cleaned = cleanValue(value);
|
|
16
|
+
return path.posix.isAbsolute(cleaned) || path.win32.isAbsolute(cleaned);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isPlaceholder(value) {
|
|
20
|
+
const cleaned = cleanValue(value);
|
|
21
|
+
if (!cleaned) return true;
|
|
22
|
+
if (cleaned.startsWith('<') && cleaned.endsWith('>')) return true;
|
|
23
|
+
return PLACEHOLDER_VALUES.has(cleaned.replace(/[.,;:]+$/, '').trim().toLowerCase());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Plan_59: scope is a real phase value, while it remains a placeholder for existing inputs. Plan_63 D5: none is a
|
|
27
|
+
// real reasoning effort the flag channel always accepted, so the header channel must not refuse it as a template.
|
|
28
|
+
const REAL_VALUES = Object.freeze({ phase: 'scope', effort: 'none' });
|
|
29
|
+
export const isInputPlaceholder = (value, label) =>
|
|
30
|
+
!(Object.hasOwn(REAL_VALUES, label) && cleanValue(value) === REAL_VALUES[label]) && isPlaceholder(value);
|
|
31
|
+
|
|
32
|
+
// Plan_75 D5, 2026-10-03 20:42: header parsing must reuse the existing grant value grammar.
|
|
33
|
+
export function splitGrantValue(value) {
|
|
34
|
+
if (isPlaceholder(value)) return null;
|
|
35
|
+
|
|
36
|
+
for (const separator of [/\s+—\s+/, /\s+-\s+/, /\s*:\s*/]) {
|
|
37
|
+
const match = value.match(new RegExp(`^(.+?)${separator.source}(.+)$`));
|
|
38
|
+
if (!match) continue;
|
|
39
|
+
const run = cleanValue(match[1]);
|
|
40
|
+
const reason = cleanValue(match[2]);
|
|
41
|
+
if (isPlaceholder(run) || isPlaceholder(reason)) return null;
|
|
42
|
+
return { run, reason };
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -54,6 +54,7 @@ import { setWorkerDir, getRun, emitReply } from './runner/run-context.mjs';
|
|
|
54
54
|
import { launcher } from './runner/launcher.mjs';
|
|
55
55
|
import { worker } from './runner/worker.mjs';
|
|
56
56
|
import { RunnerUsageError } from './runner/args.mjs';
|
|
57
|
+
import { EXIT } from './runner/exit-codes.mjs';
|
|
57
58
|
|
|
58
59
|
export { parseArgs } from './runner/args.mjs';
|
|
59
60
|
export { runsPrefixInside, worktreeSnapshot } from './runner/git-state.mjs';
|
|
@@ -88,18 +89,20 @@ export function installRunnerCrashBoundary() {
|
|
|
88
89
|
`Log: codex-bridge read ${currentRun}`,
|
|
89
90
|
]);
|
|
90
91
|
emitReply(reply);
|
|
91
|
-
process.exit(
|
|
92
|
+
process.exit(EXIT.FAIL);
|
|
92
93
|
} catch {
|
|
93
94
|
// Falls through to the bare reply below: the disk itself is not cooperating.
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
emitReply(
|
|
97
98
|
[
|
|
98
|
-
`FAIL — Codex runner crashed before creating the run folder: ${
|
|
99
|
+
`FAIL — Codex runner crashed before creating the run folder: ${
|
|
100
|
+
String(err.message).replace(/\s+/g, ' ').slice(0, 150)
|
|
101
|
+
}`,
|
|
99
102
|
'No artifacts; nothing to inspect',
|
|
100
103
|
].join('\n'),
|
|
101
104
|
);
|
|
102
|
-
process.exit(
|
|
105
|
+
process.exit(EXIT.FAIL);
|
|
103
106
|
});
|
|
104
107
|
}
|
|
105
108
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Owns the scope snapshot shared by advise readers because run 2026-09-23_141341_plan59-a1-stdin-hang left them with different inputs. */
|
|
2
|
+
import { EXIT } from './exit-codes.mjs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { readJsonFileSync } from '../json-file.mjs';
|
|
4
5
|
import { parseAdvisorTask } from '../meta/advisor-task.mjs';
|
|
@@ -32,7 +33,7 @@ export function advisorTaskOrRefuse(input) {
|
|
|
32
33
|
try {
|
|
33
34
|
return advisorTaskArtifact(input);
|
|
34
35
|
} catch (error) {
|
|
35
|
-
return die(`${error.message} The run folder was not created; quota was not spent.`,
|
|
36
|
+
return die(`${error.message} The run folder was not created; quota was not spent.`, EXIT.FAIL);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
|