@lyupro/codex-bridge 0.6.8 → 0.6.10
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 +160 -0
- package/README.md +6 -4
- package/bin/codex-bridge.mjs +35 -177
- package/cli/command-help.mjs +49 -0
- package/cli/command-registry.mjs +284 -0
- package/cli/copy.mjs +42 -5
- package/cli/dispatcher-contract-record.mjs +3 -2
- package/cli/doctor-host-side.mjs +110 -0
- package/cli/doctor.mjs +17 -1
- package/cli/home-inspection.mjs +122 -0
- package/cli/home-plan-execute.mjs +140 -0
- package/cli/home-removal-plan.mjs +61 -0
- package/cli/hook-recognizer.mjs +78 -0
- package/cli/host-contract.mjs +11 -5
- package/cli/host-inspection.mjs +156 -0
- package/cli/host-removal.mjs +91 -0
- package/cli/image-evidence.mjs +44 -0
- package/cli/install-owners.mjs +244 -0
- package/cli/install-record.mjs +71 -9
- package/cli/install.mjs +88 -75
- package/cli/inventory-removal.mjs +84 -0
- package/cli/inventory-transition.mjs +66 -0
- package/cli/lifecycle-lock-clear.mjs +104 -0
- package/cli/lifecycle-lock-inspect.mjs +92 -0
- package/cli/lifecycle-lock.mjs +222 -0
- package/cli/lifecycle-transaction.mjs +57 -0
- package/cli/link-segments.mjs +45 -0
- package/cli/model-set.mjs +4 -3
- package/cli/model-speed.mjs +9 -2
- package/cli/model.mjs +5 -3
- package/cli/owner-record.mjs +40 -0
- package/cli/owner-sync-apply.mjs +108 -0
- package/cli/owner-sync.mjs +115 -0
- package/cli/projects.mjs +5 -2
- package/cli/prune-args.mjs +9 -4
- package/cli/prune.mjs +23 -72
- package/cli/purge-live-runs.mjs +98 -0
- package/cli/purge-preflight.mjs +103 -0
- package/cli/read.mjs +22 -1
- package/cli/record-removal.mjs +43 -0
- package/cli/removal-outcomes.mjs +115 -0
- package/cli/removal-plan.mjs +187 -0
- package/cli/remove-layout.mjs +40 -1
- package/cli/rules-owners.mjs +4 -2
- package/cli/settings-merge.mjs +3 -29
- package/cli/terminal-question.mjs +41 -0
- package/cli/uninstall-purge.mjs +139 -0
- package/cli/uninstall-rules.mjs +72 -0
- package/cli/uninstall.mjs +208 -127
- package/cli/unlock-lifecycle.mjs +102 -0
- package/cli/unlock.mjs +7 -4
- package/cli/update.mjs +102 -27
- package/package.json +4 -3
- package/scripts/home-write-audit.mjs +286 -0
- package/scripts/home-write-inventory.mjs +59 -0
- package/scripts/home-write-lexer.mjs +182 -0
- package/scripts/isolated-roots.mjs +6 -1
- package/scripts/open-work/check.mjs +180 -0
- package/scripts/open-work/live-thresholds.mjs +155 -0
- package/scripts/open-work/register.mjs +56 -0
- package/scripts/root-guard/root-entries.mjs +15 -0
- package/scripts/run-tests.mjs +26 -1
- package/src/agents/codex-advisor.md +23 -10
- package/src/agents/codex-build.md +24 -14
- package/src/agents/codex-review.md +17 -10
- package/src/agents/codex-scout.md +22 -12
- package/src/home/conventions.md +9 -0
- package/src/home/hooks/guard-tries.mjs +6 -4
- package/src/home/hooks/order-gate.mjs +27 -19
- package/src/home/hooks/reply-guard.mjs +2 -7
- package/src/home/hooks/reply-parser.mjs +1 -1
- package/src/home/hooks/worktree-witness.mjs +6 -6
- package/src/home/lib/agents.mjs +1 -1
- package/src/home/lib/atomic-json.mjs +24 -6
- package/src/home/lib/command-path.mjs +32 -0
- package/src/home/lib/config-edit.mjs +25 -10
- package/src/home/lib/dispatcher-command.mjs +4 -2
- package/src/home/lib/dispatcher-state.mjs +25 -10
- package/src/home/lib/file-lock.mjs +41 -10
- package/src/home/lib/handback-witness.mjs +7 -4
- package/src/home/lib/home-registry.mjs +211 -0
- package/src/home/lib/home-write.mjs +189 -0
- package/src/home/lib/hook-diagnostics.mjs +20 -0
- package/src/home/lib/host-observations.mjs +7 -4
- package/src/home/lib/kernel-lock.mjs +171 -0
- package/src/home/lib/meta/advice-status.mjs +2 -2
- package/src/home/lib/meta/advice-verdict.mjs +30 -1
- 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 +16 -3
- package/src/home/lib/meta/launch-rows.mjs +23 -0
- package/src/home/lib/meta/paths.mjs +24 -24
- package/src/home/lib/meta/reply.mjs +37 -3
- package/src/home/lib/meta/run-snapshots.mjs +29 -0
- package/src/home/lib/meta/run-state.mjs +3 -0
- package/src/home/lib/meta/snapshot-format.mjs +96 -0
- package/src/home/lib/meta/verdict.mjs +30 -24
- package/src/home/lib/required-inputs.mjs +48 -8
- package/src/home/lib/run-codex.mjs +6 -3
- package/src/home/lib/run-config.mjs +10 -1
- package/src/home/lib/runner/advise-carry.mjs +2 -1
- package/src/home/lib/runner/args.mjs +16 -5
- package/src/home/lib/runner/attach.mjs +21 -14
- 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 +60 -8
- 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-paths.mjs +74 -0
- package/src/home/lib/runner/git-state.mjs +37 -66
- package/src/home/lib/runner/launcher.mjs +233 -273
- package/src/home/lib/runner/order-claim.mjs +84 -0
- package/src/home/lib/runner/order-owner.mjs +18 -0
- package/src/home/lib/runner/pass-gate.mjs +109 -0
- package/src/home/lib/runner/preflight.mjs +22 -18
- package/src/home/lib/runner/prompts.mjs +42 -4
- 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/schemas.mjs +4 -1
- package/src/home/lib/runner/scope-check.mjs +55 -28
- package/src/home/lib/runner/task-input.mjs +11 -2
- package/src/home/lib/runner/worker-admission.mjs +43 -0
- package/src/home/lib/runner/worker.mjs +19 -18
- package/src/home/lib/task-header.mjs +127 -0
- package/src/home/lib/write-meta.mjs +17 -5
|
@@ -23,6 +23,27 @@ function retentionReply(runDir) {
|
|
|
23
23
|
return `Retention: freed ${formatBytes(bytes)} from ${runs} runs older than ${days} days`;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function attemptReply(runDir) {
|
|
27
|
+
const retryOf = readJson(path.join(runDir, 'status.json'))?.retry_of;
|
|
28
|
+
if (typeof retryOf !== 'string' || !retryOf.trim()) return null;
|
|
29
|
+
const runsRoot = path.dirname(runDir);
|
|
30
|
+
const key = (name) => process.platform === 'win32' ? name.toLowerCase() : name;
|
|
31
|
+
const seen = new Set([key(path.basename(runDir))]);
|
|
32
|
+
let previous = retryOf;
|
|
33
|
+
let attempt = 1;
|
|
34
|
+
// Plan_75 D1, 2026-10-03 TradeForge: show retries without trusting broken provenance links.
|
|
35
|
+
for (let hops = 0; hops < 50; hops += 1) {
|
|
36
|
+
if (typeof previous !== 'string' || !previous || /[/\\]/.test(previous)
|
|
37
|
+
|| previous === '.' || previous === '..' || seen.has(key(previous))) break;
|
|
38
|
+
const status = readJson(path.join(runsRoot, previous, 'status.json'));
|
|
39
|
+
if (!status) break;
|
|
40
|
+
seen.add(key(previous));
|
|
41
|
+
attempt += 1;
|
|
42
|
+
previous = status.retry_of;
|
|
43
|
+
}
|
|
44
|
+
return `Attempt: ${attempt} of this pass — retry of ${retryOf}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
// Five copies of Retention missed writeFailure(), so both reply assemblers share this point.
|
|
27
48
|
// The 2026-09-16 dead Codex sandbox incident is why the launcher probes the sandbox at all.
|
|
28
49
|
export function launchRows(runDir) {
|
|
@@ -33,6 +54,8 @@ export function launchRows(runDir) {
|
|
|
33
54
|
const reason = line(probe.reason, 160) || 'no reason recorded';
|
|
34
55
|
rows.push(`Sandbox probe: inconclusive — ${reason} The run started without a sandbox check.`);
|
|
35
56
|
}
|
|
57
|
+
const attempt = attemptReply(runDir);
|
|
58
|
+
if (attempt) rows.push(attempt);
|
|
36
59
|
return rows;
|
|
37
60
|
}
|
|
38
61
|
|
|
@@ -3,11 +3,21 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Two sides spell the same file differently — git prints repo-relative paths with forward
|
|
5
5
|
* slashes, Codex writes whatever it likes — so nothing above this module compares a path
|
|
6
|
-
* before it has been through here.
|
|
7
|
-
* filesystem for reading artifacts: everything else receives text, JSON or a byte count from it.
|
|
6
|
+
* before it has been through here. Strict snapshot judgement belongs to run-snapshots.mjs.
|
|
8
7
|
*/
|
|
9
8
|
import fs from 'node:fs';
|
|
10
9
|
import { readJsonFileSync } from '../json-file.mjs';
|
|
10
|
+
import { compareSnapshots, decodeSnapshot } from './snapshot-format.mjs';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `slice` counts UTF-16 units and can leave half of an emoji or a rare CJK character at the cut;
|
|
14
|
+
* a lone surrogate in a reply is the same kind of broken text Plan_68 D6 refuses from the model.
|
|
15
|
+
*/
|
|
16
|
+
export const safeSlice = (value, max) => {
|
|
17
|
+
const text = String(value ?? '').toWellFormed();
|
|
18
|
+
const result = text.slice(0, max);
|
|
19
|
+
return result.isWellFormed() ? result : result.slice(0, -1);
|
|
20
|
+
};
|
|
11
21
|
|
|
12
22
|
/** One line, no newlines, bounded length — a five-line reply must stay five lines. */
|
|
13
23
|
export const line = (value, max = 200) => {
|
|
@@ -15,7 +25,7 @@ export const line = (value, max = 200) => {
|
|
|
15
25
|
if (text.length <= max) return text;
|
|
16
26
|
|
|
17
27
|
const ellipsis = '...';
|
|
18
|
-
if (max <= ellipsis.length) return text
|
|
28
|
+
if (max <= ellipsis.length) return safeSlice(text, Math.max(0, max));
|
|
19
29
|
|
|
20
30
|
const contentLimit = max - ellipsis.length;
|
|
21
31
|
const boundary = text.lastIndexOf(' ', contentLimit);
|
|
@@ -24,11 +34,11 @@ export const line = (value, max = 200) => {
|
|
|
24
34
|
// where a word ended. Dropping a trailing number here as well cost `All 298 12345678 rest`
|
|
25
35
|
// its 298 — a number that fitted whole — because the digit test compared the limit rather
|
|
26
36
|
// than the point the text was actually cut at.
|
|
27
|
-
if (boundary > 0) return `${text
|
|
37
|
+
if (boundary > 0) return `${safeSlice(text, boundary)}${ellipsis}`;
|
|
28
38
|
|
|
29
39
|
// No space to retreat to: the limit lands inside a single long word. Only here can the cut
|
|
30
40
|
// land mid-number, so only here is a partial number dropped.
|
|
31
|
-
return `${text
|
|
41
|
+
return `${safeSlice(text, contentLimit).replace(/\d+$/, '')}${ellipsis}`;
|
|
32
42
|
};
|
|
33
43
|
|
|
34
44
|
export const readText = (file) => {
|
|
@@ -60,32 +70,22 @@ export const size = (file) => {
|
|
|
60
70
|
};
|
|
61
71
|
|
|
62
72
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
73
|
+
* Tolerant display reader for reply, run-state and write-meta; invalid snapshots display
|
|
74
|
+
* an empty Map. Strict judgement goes through run-snapshots.mjs.
|
|
65
75
|
*/
|
|
66
76
|
export const snapshotMap = (text) => {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const parts = row.split('\t');
|
|
70
|
-
if (parts.length < 3) continue;
|
|
71
|
-
map.set(parts.slice(2).join('\t').trim(), `${parts[0]}\t${parts[1]}`);
|
|
72
|
-
}
|
|
73
|
-
return map;
|
|
77
|
+
const snapshot = decodeSnapshot(text);
|
|
78
|
+
return snapshot.ok ? snapshot.rows : new Map();
|
|
74
79
|
};
|
|
75
80
|
|
|
76
81
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
82
|
+
* Tolerant display reader for reply, run-state and write-meta; invalid comparisons display
|
|
83
|
+
* no changes. Strict judgement goes through run-snapshots.mjs. Snapshot states measure
|
|
84
|
+
* edits even when a file's porcelain letter stays unchanged.
|
|
80
85
|
*/
|
|
81
86
|
export const changedPaths = (before, after) => {
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
const paths = [];
|
|
85
|
-
for (const [file, state] of now) if (was.get(file) !== state) paths.push(file);
|
|
86
|
-
// A path that dropped out was touched too: Codex restored it to its committed state.
|
|
87
|
-
for (const file of was.keys()) if (!now.has(file)) paths.push(file);
|
|
88
|
-
return paths;
|
|
87
|
+
const comparison = compareSnapshots(before, after);
|
|
88
|
+
return comparison.ok ? comparison.changed : [];
|
|
89
89
|
};
|
|
90
90
|
|
|
91
91
|
/** Directories a delegated build has no business editing, whatever the task says. */
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Renders the reply lines a dispatcher is allowed to return, one format per agent.
|
|
3
3
|
*
|
|
4
4
|
* AGENTS adds reply strategies to the shared agent registry: for each one it describes how
|
|
5
|
-
* to tell the result file is filled in and which reply strategy below renders it.
|
|
5
|
+
* to tell the result file is filled in and which reply strategy below renders it.
|
|
6
|
+
* FAIL, LIMIT and UNAVAILABLE bypass the per-agent strategy — a run that
|
|
6
7
|
* produced nothing has nothing agent-specific left to say.
|
|
7
8
|
*
|
|
8
9
|
* The lines built here ARE the reply. Agents forward this text verbatim instead of
|
|
@@ -24,6 +25,11 @@ const runChanges = (runDir) =>
|
|
|
24
25
|
|
|
25
26
|
const readCommand = (runDir) => `codex-bridge read ${runDir}`;
|
|
26
27
|
|
|
28
|
+
function stripVerdict(value) {
|
|
29
|
+
// Run 2026-10-01_114244_plan72-r3q-live-probe-20261001 repeated OK because the model prefixed its summary.
|
|
30
|
+
return String(value ?? '').replace(/^(?:OK|FAIL|LIMIT|UNAVAILABLE)(?: — | - |: )/, '');
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
export const AGENTS = {
|
|
28
34
|
'codex-scout': {
|
|
29
35
|
...AGENT_DEFINITIONS['codex-scout'],
|
|
@@ -54,7 +60,7 @@ function scoutReply(ctx) {
|
|
|
54
60
|
const unknowns = (r.unknowns || []).filter(Boolean);
|
|
55
61
|
const coverage = scoutCoverage(ctx.runDir, r);
|
|
56
62
|
return [
|
|
57
|
-
`OK — ${line(r.answer, 160)}`,
|
|
63
|
+
`OK — ${line(stripVerdict(r.answer), 160)}`,
|
|
58
64
|
// Any explicit question gets a coverage line, including a valid one-question order.
|
|
59
65
|
...(coverage ? [`Coverage: ${coverage}`] : []),
|
|
60
66
|
`Key finding: ${top ? `${line(top.fact, 130)} (${line(top.where, 60)})` : 'no findings listed'}`,
|
|
@@ -68,6 +74,7 @@ function buildReply(ctx) {
|
|
|
68
74
|
const { work: touchedPaths, environment } = runChanges(ctx.runDir);
|
|
69
75
|
const paths = touchedPaths.slice(0, 3).join(', ');
|
|
70
76
|
const flags = readText(path.join(ctx.runDir, 'flags.txt')).split(/\r?\n/).filter(Boolean);
|
|
77
|
+
const gaps = readText(path.join(ctx.runDir, 'flags-coverage.txt')).split(/\r?\n/).filter((gap) => gap.trim());
|
|
71
78
|
// A multi-line verification is several commands, and collapsing them into one 60-character
|
|
72
79
|
// line cut the last one mid-word: the reply named a command nobody could run.
|
|
73
80
|
const commands = String(r.verify_command ?? '')
|
|
@@ -90,7 +97,7 @@ function buildReply(ctx) {
|
|
|
90
97
|
? `${paths ? `${paths} · ` : ''}changes were made by an earlier run of this task`
|
|
91
98
|
: paths || 'worktree untouched';
|
|
92
99
|
return [
|
|
93
|
-
`OK — ${line(r.summary, 300)}`,
|
|
100
|
+
`OK — ${line(stripVerdict(r.summary), 300)}`,
|
|
94
101
|
`Files: ${touchedPaths.length} changed · ${files}`,
|
|
95
102
|
// Only when something outside the run wrote to the tree. Subtracting those paths from the
|
|
96
103
|
// verdict without naming them would hide a real edit behind a pattern.
|
|
@@ -99,6 +106,9 @@ function buildReply(ctx) {
|
|
|
99
106
|
: []),
|
|
100
107
|
`Verification: ${verify} — ${verdict}`,
|
|
101
108
|
`Flags: ${flags.length ? `${flags.length} TODO/skip — ${line(flags.slice(0, 3).join(' | '), 140)}` : 'none'}`,
|
|
109
|
+
...(gaps.length
|
|
110
|
+
? [`Flags coverage: incomplete — ${line(gaps[0], 120)}${gaps.length > 1 ? ` (+${gaps.length - 1} more)` : ''}`]
|
|
111
|
+
: []),
|
|
102
112
|
`Report: ${ctx.file('report.md')} · Log: ${readCommand(ctx.runDir)}`,
|
|
103
113
|
];
|
|
104
114
|
}
|
|
@@ -223,3 +233,27 @@ export function limitReply(ctx, meta) {
|
|
|
223
233
|
rows.push(`Log: ${readCommand(ctx.runDir)}`);
|
|
224
234
|
return rows;
|
|
225
235
|
}
|
|
236
|
+
|
|
237
|
+
/** Plan_60 D2: pre-start and post-start refusals share the same handoff contract. */
|
|
238
|
+
export function unavailableRows(signal) {
|
|
239
|
+
return [
|
|
240
|
+
'UNAVAILABLE — Codex is not available (missing or signed out); ' +
|
|
241
|
+
'hand the task to the next executor, do not retry',
|
|
242
|
+
`Signal: ${line(signal, 170)}`,
|
|
243
|
+
'Operator check: codex --version; codex login status (sign in with codex login)',
|
|
244
|
+
];
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function unavailableReply(ctx, meta) {
|
|
248
|
+
const rows = unavailableRows(meta.reason);
|
|
249
|
+
if (ctx.agent === 'codex-build') {
|
|
250
|
+
const { work: touched } = runChanges(ctx.runDir);
|
|
251
|
+
rows.push(
|
|
252
|
+
`Worktree: ${
|
|
253
|
+
touched.length ? `has unfinished changes (${touched.length}), see git-after.txt` : 'no new changes'
|
|
254
|
+
}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
rows.push(`Log: ${readCommand(ctx.runDir)}`);
|
|
258
|
+
return rows;
|
|
259
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a run's before/after snapshots and compare them strictly.
|
|
3
|
+
* On 2026-09-30 git-quoted Cyrillic names made an in-scope edit look out of scope;
|
|
4
|
+
* missing or untrustworthy snapshots must refuse judgement, never imply a clean tree.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { compareSnapshots } from './snapshot-format.mjs';
|
|
9
|
+
|
|
10
|
+
export function readSnapshot(file) {
|
|
11
|
+
try {
|
|
12
|
+
return fs.readFileSync(file, 'utf8');
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.code === 'ENOENT') return null;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function runSnapshotChanges(runDir) {
|
|
20
|
+
return compareSnapshots(
|
|
21
|
+
readSnapshot(path.join(runDir, 'state-before.txt')),
|
|
22
|
+
readSnapshot(path.join(runDir, 'state-after.txt')),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function snapshotRefusal(comparison) {
|
|
27
|
+
const issue = comparison.side ? `${comparison.side} ${comparison.issue}` : comparison.issue;
|
|
28
|
+
return `worktree snapshots cannot be compared (${issue}): ${comparison.detail}`;
|
|
29
|
+
}
|
|
@@ -12,6 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
import { changedPaths, line, normalizePath, readJson, readText, size } from './paths.mjs';
|
|
13
13
|
import { withLaunchRows } from './launch-rows.mjs';
|
|
14
14
|
import { runLiveness } from './run-liveness.mjs';
|
|
15
|
+
import { removeFlagBaselineCopies } from '../runner/flag-baseline.mjs';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Run state on disk, merged over whatever is already there. A killed runner leaves no
|
|
@@ -115,6 +116,8 @@ export function markAbandoned(runsRoot, currentTree) {
|
|
|
115
116
|
}
|
|
116
117
|
try {
|
|
117
118
|
writeStatus(runDir, patch);
|
|
119
|
+
// Plan_60 D3a: a dead worker cannot remove its start copies; keep the manifest for inspection.
|
|
120
|
+
if (patch.state === 'abandoned') removeFlagBaselineCopies(runDir);
|
|
118
121
|
changed.push({ run: entry.name, state: patch.state });
|
|
119
122
|
} catch {
|
|
120
123
|
// A folder that cannot be written to is not worth failing a fresh run over.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize, validate and compare worktree snapshots; owns the version rule.
|
|
3
|
+
*
|
|
4
|
+
* On 2026-09-30 git's quoted Cyrillic names made an in-scope edit look out of scope
|
|
5
|
+
* and hid a new Cyrillic file. JSON names preserve exact spelling; mixed versions
|
|
6
|
+
* must refuse comparison rather than repeat that incident.
|
|
7
|
+
*/
|
|
8
|
+
import { parseJsonText } from '../json-file.mjs';
|
|
9
|
+
|
|
10
|
+
export const SNAPSHOT_V2_HEADER = '# codex-bridge-state-v2';
|
|
11
|
+
|
|
12
|
+
// `unreadable`: listed but not readable (a Windows file held by another process gives EBUSY/EPERM).
|
|
13
|
+
const STATE_RE = /^(?:\d+\t\d+|-\t-|U\t(?:\d+:[0-9a-fA-F]{64}|missing|unreadable))$/;
|
|
14
|
+
const LEGACY_ROW_RE = /^(?:\d+\t\d+|-\t-|U\t\d+)\t\S.*$/;
|
|
15
|
+
const validState = (state) => typeof state === 'string' && STATE_RE.exec(state)?.[0] === state;
|
|
16
|
+
const malformed = (detail) => ({ ok: false, issue: 'malformed', detail });
|
|
17
|
+
|
|
18
|
+
export function encodeSnapshot(rows) {
|
|
19
|
+
const paths = new Set();
|
|
20
|
+
const ordered = [];
|
|
21
|
+
for (const row of rows) {
|
|
22
|
+
if (!row || typeof row.path !== 'string' || !validState(row.state)) {
|
|
23
|
+
throw new TypeError('Snapshot rows require a string path and a valid v2 state.');
|
|
24
|
+
}
|
|
25
|
+
if (paths.has(row.path)) throw new TypeError(`Duplicate snapshot path: ${JSON.stringify(row.path)}`);
|
|
26
|
+
paths.add(row.path);
|
|
27
|
+
ordered.push({ path: row.path, state: row.state });
|
|
28
|
+
}
|
|
29
|
+
ordered.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
30
|
+
return [SNAPSHOT_V2_HEADER, ...ordered.map(({ path, state }) => `${state}\t${JSON.stringify(path)}`)]
|
|
31
|
+
.join('\n') + '\n';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function decodeSnapshot(text) {
|
|
35
|
+
if (text === null) return { ok: false, issue: 'missing', detail: 'Snapshot file is absent.' };
|
|
36
|
+
if (typeof text !== 'string') return malformed('Snapshot text must be a string or null.');
|
|
37
|
+
const lines = text.replace(/^\uFEFF/, '').split(/\r?\n/);
|
|
38
|
+
const version = lines[0] === SNAPSHOT_V2_HEADER ? 2 : 1;
|
|
39
|
+
if (version === 2) lines.shift();
|
|
40
|
+
// Only the final line terminator is framing; an empty interior row is damaged data.
|
|
41
|
+
if (lines.at(-1) === '') lines.pop();
|
|
42
|
+
const rows = new Map();
|
|
43
|
+
for (const [index, line] of lines.entries()) {
|
|
44
|
+
const location = `Snapshot line ${index + (version === 2 ? 2 : 1)}`;
|
|
45
|
+
let path;
|
|
46
|
+
let state;
|
|
47
|
+
if (version === 2) {
|
|
48
|
+
const fields = line.split('\t');
|
|
49
|
+
if (fields.length !== 3) return malformed(`${location} must have exactly three tab fields.`);
|
|
50
|
+
state = `${fields[0]}\t${fields[1]}`;
|
|
51
|
+
if (!validState(state)) return malformed(`${location} has an invalid v2 state.`);
|
|
52
|
+
try {
|
|
53
|
+
path = parseJsonText(location, fields[2]);
|
|
54
|
+
} catch {
|
|
55
|
+
return malformed(`${location} has an invalid JSON path.`);
|
|
56
|
+
}
|
|
57
|
+
if (typeof path !== 'string') return malformed(`${location} path must be a JSON string.`);
|
|
58
|
+
} else {
|
|
59
|
+
// The legacy launcher wrote `${snapshot}\n`, so a clean tree is the text "\n" and the old
|
|
60
|
+
// reader skipped blank rows. Refusing them failed every build that started on a clean tree.
|
|
61
|
+
if (line === '') continue;
|
|
62
|
+
if (!LEGACY_ROW_RE.test(line)) return malformed(`${location} has an invalid legacy row.`);
|
|
63
|
+
const fields = line.split('\t');
|
|
64
|
+
state = `${fields[0]}\t${fields[1]}`;
|
|
65
|
+
path = fields.slice(2).join('\t').trim();
|
|
66
|
+
if (path.startsWith('"')) {
|
|
67
|
+
return {
|
|
68
|
+
ok: false,
|
|
69
|
+
issue: 'legacy-quoted-name',
|
|
70
|
+
detail: `${location} contains a git-quoted legacy name whose original spelling is lost.`,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (rows.has(path)) return malformed(`${location} repeats path ${JSON.stringify(path)}.`);
|
|
75
|
+
rows.set(path, state);
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, version, rows };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function compareSnapshots(beforeText, afterText) {
|
|
81
|
+
const before = decodeSnapshot(beforeText);
|
|
82
|
+
if (!before.ok) return { ...before, side: 'before' };
|
|
83
|
+
const after = decodeSnapshot(afterText);
|
|
84
|
+
if (!after.ok) return { ...after, side: 'after' };
|
|
85
|
+
if (before.version !== after.version) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
issue: 'incompatible-versions',
|
|
89
|
+
detail: 'The run started under another package version; restart the run before comparing snapshots.',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const changed = [];
|
|
93
|
+
for (const [path, state] of after.rows) if (before.rows.get(path) !== state) changed.push(path);
|
|
94
|
+
for (const path of before.rows.keys()) if (!after.rows.has(path)) changed.push(path);
|
|
95
|
+
return { ok: true, changed };
|
|
96
|
+
}
|
|
@@ -10,7 +10,6 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import {
|
|
12
12
|
SERVICE_RE,
|
|
13
|
-
changedPaths,
|
|
14
13
|
declaredHits,
|
|
15
14
|
displayPath,
|
|
16
15
|
expandDeclared,
|
|
@@ -29,6 +28,9 @@ import { startupGap } from './startup.mjs';
|
|
|
29
28
|
import { transportGap } from './transport.mjs';
|
|
30
29
|
import { reasonFrom } from './reason.mjs';
|
|
31
30
|
import { adviceGap } from './advice-status.mjs';
|
|
31
|
+
import { allStartupContext, contextOnlyGap } from './context-only.mjs';
|
|
32
|
+
import { compareSnapshots } from './snapshot-format.mjs';
|
|
33
|
+
import { readSnapshot, runSnapshotChanges, snapshotRefusal } from './run-snapshots.mjs';
|
|
32
34
|
|
|
33
35
|
// How much prose an answer must carry once coordinates and paths are subtracted. Measured
|
|
34
36
|
// on the scout run of 2026-07-30 that replied with a table of `file.ts:60-79` rows: every
|
|
@@ -79,16 +81,14 @@ const WRONG_WORK = 'wrong work was done: ';
|
|
|
79
81
|
*/
|
|
80
82
|
export function reportVersusWork(runDir, result, ctx) {
|
|
81
83
|
const declared = (result?.changes || []).flatMap((c) => expandDeclared(c?.file));
|
|
82
|
-
const
|
|
84
|
+
const comparison = runSnapshotChanges(runDir);
|
|
85
|
+
const broken = (reason) => ({ ok: false, carried: false, reason });
|
|
86
|
+
if (!comparison.ok) return broken(snapshotRefusal(comparison));
|
|
83
87
|
// Only the run's own work is compared: a report that names nothing is honest when the sole
|
|
84
88
|
// change in the tree was written by the tooling around the run.
|
|
85
|
-
const touched = splitRunChanges(
|
|
86
|
-
runDir,
|
|
87
|
-
changedPaths(readText(path.join(runDir, 'state-before.txt')), after),
|
|
88
|
-
).work;
|
|
89
|
+
const touched = splitRunChanges(runDir, comparison.changed).work;
|
|
89
90
|
const show = (list) => list.slice(0, 3).join(', ');
|
|
90
91
|
const agreed = (carried = false) => ({ ok: true, carried, reason: null });
|
|
91
|
-
const broken = (reason) => ({ ok: false, carried: false, reason });
|
|
92
92
|
|
|
93
93
|
if (!declared.length && !touched.length) return agreed();
|
|
94
94
|
if (!declared.length) {
|
|
@@ -110,8 +110,11 @@ export function reportVersusWork(runDir, result, ctx) {
|
|
|
110
110
|
|
|
111
111
|
const baseline = chainBaseline(ctx?.runsRoot, ctx?.repo, ctx?.slug, ctx?.taskHash, ctx?.orderId);
|
|
112
112
|
if (baseline !== null) {
|
|
113
|
-
const
|
|
114
|
-
if (
|
|
113
|
+
const chain = compareSnapshots(baseline, readSnapshot(path.join(runDir, 'state-after.txt')));
|
|
114
|
+
if (chain.ok) {
|
|
115
|
+
const accumulated = splitRunChanges(runDir, chain.changed).work;
|
|
116
|
+
if (declared.some((d) => declaredHits(d, accumulated))) return agreed(true);
|
|
117
|
+
}
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
return broken(
|
|
@@ -154,7 +157,7 @@ function substanceLength(text) {
|
|
|
154
157
|
|
|
155
158
|
const questionsOf = (runDir) => {
|
|
156
159
|
const parsed = readJson(path.join(runDir, 'questions.json'));
|
|
157
|
-
return Array.isArray(parsed) ? parsed
|
|
160
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
158
161
|
};
|
|
159
162
|
|
|
160
163
|
/** Answers indexed by question id, first one wins; ids come back in whatever case Codex used. */
|
|
@@ -173,8 +176,8 @@ const answersById = (result) => {
|
|
|
173
176
|
* single-answer check. Every branch here exists because a schema alone accepted the run that
|
|
174
177
|
* answered in coordinates.
|
|
175
178
|
*/
|
|
176
|
-
function scoutCoverageGap(
|
|
177
|
-
const questions =
|
|
179
|
+
function scoutCoverageGap(orderedQuestions, result) {
|
|
180
|
+
const questions = orderedQuestions.filter((q) => q && q.id);
|
|
178
181
|
const show = (list) => list.slice(0, 8).join(', ');
|
|
179
182
|
|
|
180
183
|
if (!questions.length) {
|
|
@@ -209,7 +212,7 @@ function scoutCoverageGap(runDir, result) {
|
|
|
209
212
|
|
|
210
213
|
/** `1/1 sub-questions`, or null for legacy runs with no questions.json artifact. */
|
|
211
214
|
export function scoutCoverage(runDir, result) {
|
|
212
|
-
const questions = questionsOf(runDir);
|
|
215
|
+
const questions = questionsOf(runDir).filter((q) => q && q.id);
|
|
213
216
|
if (!questions.length) return null;
|
|
214
217
|
const byId = answersById(result);
|
|
215
218
|
const answered = questions.filter((q) => String(byId.get(q.id)?.answer || '').trim()).length;
|
|
@@ -286,9 +289,10 @@ export function resolveStatus({ resultOk, exit, agent, result, runDir, events })
|
|
|
286
289
|
// This branch already knows why the run died: the CLI said so itself, with a status. Letting
|
|
287
290
|
// the ordered reason speak here would answer a transport question with the model's older
|
|
288
291
|
// complaint about the task — a LIMIT that never mentions quota is the same "verdict says the
|
|
289
|
-
// wrong thing" defect this module exists to remove.
|
|
292
|
+
// wrong thing" defect this module exists to remove. Plan_60 D2: UNAVAILABLE only on a measured
|
|
293
|
+
// signed-out sample (events.mjs); an unfamiliar failure stays FAIL with its raw CLI text.
|
|
290
294
|
return {
|
|
291
|
-
status: transportError.quota ? 'LIMIT' : 'FAIL',
|
|
295
|
+
status: transportError.quota ? 'LIMIT' : transportError.unavailable ? 'UNAVAILABLE' : 'FAIL',
|
|
292
296
|
reason: transportError.reason,
|
|
293
297
|
};
|
|
294
298
|
}
|
|
@@ -316,19 +320,25 @@ export function resolveStatus({ resultOk, exit, agent, result, runDir, events })
|
|
|
316
320
|
// Answers that never arrived, or arrived as coordinates. Scout-only, and per-question
|
|
317
321
|
// only when questions.json is absent — see scoutCoverageGap().
|
|
318
322
|
if (agent === 'codex-scout') {
|
|
323
|
+
const questions = questionsOf(runDir);
|
|
319
324
|
// The 2026-09-16 dead sandbox produced nonempty evidence without reading code. Require an
|
|
320
325
|
// execution fact first: checking whether evidence paths existed falsely rejected 64/151 runs.
|
|
321
326
|
// Only a run that left a stream can prove it: an archived run from before `--json` has none and
|
|
322
327
|
// is judged by the contract of its day; a `--json` run missing its stream already failed above.
|
|
323
|
-
|
|
328
|
+
// Plan_60 D1 exempts only orders whose every question asks about handed startup context.
|
|
329
|
+
if (eventData.hasStream && eventData.commands_executed === 0 && !allStartupContext(questions)) {
|
|
324
330
|
return {
|
|
325
331
|
status: 'FAIL',
|
|
326
332
|
reason: 'scout executed no command, so no answer rests on reading the code; '
|
|
327
333
|
+ 'check stderr.log for sandbox refusals before changing the order',
|
|
328
334
|
};
|
|
329
335
|
}
|
|
330
|
-
const gap = scoutCoverageGap(
|
|
336
|
+
const gap = scoutCoverageGap(questions, result);
|
|
331
337
|
if (gap) return { status: 'FAIL', reason: gap };
|
|
338
|
+
const contextGap = contextOnlyGap({
|
|
339
|
+
questions, result, commandsExecuted: eventData.hasStream ? eventData.commands_executed : null,
|
|
340
|
+
});
|
|
341
|
+
if (contextGap) return { status: 'FAIL', reason: contextGap };
|
|
332
342
|
}
|
|
333
343
|
// Plan_59 D3/D4/D5/D10: a schema-valid agreement still needs evidence and decision checks.
|
|
334
344
|
// Place the advisor judge before the build-only scope check, alongside the other agent contracts.
|
|
@@ -349,13 +359,9 @@ export function resolveStatus({ resultOk, exit, agent, result, runDir, events })
|
|
|
349
359
|
// Environment writes are subtracted first: attributing them to the run failed an honest
|
|
350
360
|
// pass for a file it never opened. What was subtracted is reported, not swallowed —
|
|
351
361
|
// see the "Environment" line in reply.mjs and environment_changes in meta.json.
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
readText(path.join(runDir, 'state-before.txt')),
|
|
356
|
-
readText(path.join(runDir, 'state-after.txt')),
|
|
357
|
-
),
|
|
358
|
-
);
|
|
362
|
+
const comparison = runSnapshotChanges(runDir);
|
|
363
|
+
if (!comparison.ok) return { status: 'FAIL', reason: snapshotRefusal(comparison) };
|
|
364
|
+
const { work } = splitRunChanges(runDir, comparison.changed);
|
|
359
365
|
const strays = outOfScope(work, patterns);
|
|
360
366
|
if (strays.length) {
|
|
361
367
|
return {
|
|
@@ -23,6 +23,15 @@ export const CONTINUATION_INPUT = Object.freeze({
|
|
|
23
23
|
conditional: 'when --continue is passed',
|
|
24
24
|
});
|
|
25
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
|
+
|
|
26
35
|
/**
|
|
27
36
|
* The task statement reaches the runner as a file because every other channel puts the invocation
|
|
28
37
|
* back into the multi-line form that no permission rule can cover — a heredoc on stdin, or a
|
|
@@ -53,6 +62,7 @@ export const REQUIRED_INPUTS = Object.freeze({
|
|
|
53
62
|
},
|
|
54
63
|
TASK_FILE_INPUT,
|
|
55
64
|
CONTINUATION_INPUT,
|
|
65
|
+
RETRY_INPUT,
|
|
56
66
|
]),
|
|
57
67
|
'codex-build': freezeEntries([
|
|
58
68
|
{
|
|
@@ -69,6 +79,7 @@ export const REQUIRED_INPUTS = Object.freeze({
|
|
|
69
79
|
},
|
|
70
80
|
TASK_FILE_INPUT,
|
|
71
81
|
CONTINUATION_INPUT,
|
|
82
|
+
RETRY_INPUT,
|
|
72
83
|
]),
|
|
73
84
|
'codex-review': freezeEntries([
|
|
74
85
|
{
|
|
@@ -79,6 +90,7 @@ export const REQUIRED_INPUTS = Object.freeze({
|
|
|
79
90
|
},
|
|
80
91
|
TASK_FILE_INPUT,
|
|
81
92
|
CONTINUATION_INPUT,
|
|
93
|
+
RETRY_INPUT,
|
|
82
94
|
]),
|
|
83
95
|
// Plan_59 D7: the caller must name the phase and authorize the continued decision pass.
|
|
84
96
|
'codex-advisor': freezeEntries([
|
|
@@ -90,6 +102,7 @@ export const REQUIRED_INPUTS = Object.freeze({
|
|
|
90
102
|
},
|
|
91
103
|
TASK_FILE_INPUT,
|
|
92
104
|
CONTINUATION_INPUT,
|
|
105
|
+
RETRY_INPUT,
|
|
93
106
|
{
|
|
94
107
|
label: 'phase',
|
|
95
108
|
source: 'the orchestrator',
|
|
@@ -122,13 +135,15 @@ export function isPlaceholder(value) {
|
|
|
122
135
|
export const isInputPlaceholder = (value, label) =>
|
|
123
136
|
!(label === 'phase' && cleanValue(value) === 'scope') && isPlaceholder(value);
|
|
124
137
|
|
|
125
|
-
|
|
138
|
+
// Plan_75 D5: dispatcher prompts still need one spelling of a labelled line for reading values.
|
|
139
|
+
const labelledLine = (label) => {
|
|
126
140
|
const labelPattern = escapeRegExp(label).replaceAll('\\ ', '\\s+');
|
|
141
|
+
return `(?:[-*]\\s*)?(?:[*_` + '`' + `]?${labelPattern}[*_` + '`' + `]?)\\s*(?::|=|—|-)`;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export function extractValue(promptText, label) {
|
|
127
145
|
const flag = `--${label.replaceAll(/\s+/g, '-')}`;
|
|
128
|
-
const direct = new RegExp(
|
|
129
|
-
`(?:^|\\r?\\n)\\s*(?:[-*]\\s*)?(?:[*_` + '`' + `]?${labelPattern}[*_` + '`' + `]?)\\s*(?::|=|—|-)\\s*([^\\r\\n]*)`,
|
|
130
|
-
'im',
|
|
131
|
-
);
|
|
146
|
+
const direct = new RegExp(`(?:^|\\r?\\n)\\s*${labelledLine(label)}\\s*([^\\r\\n]*)`, 'im');
|
|
132
147
|
const directMatch = promptText.match(direct);
|
|
133
148
|
if (directMatch) return cleanValue(directMatch[1]);
|
|
134
149
|
|
|
@@ -188,10 +203,15 @@ export function diagnoseInput(promptText, label) {
|
|
|
188
203
|
return { line, reason: `expected \`${label}: value\` with a separator immediately after the label` };
|
|
189
204
|
}
|
|
190
205
|
|
|
191
|
-
|
|
192
|
-
|
|
206
|
+
// Plan_75 D1, TradeForge capacity incident (2026-10-03): both grants need the same run/reason grammar.
|
|
207
|
+
function parseLabelledGrant(promptText, label) {
|
|
193
208
|
const prompt = typeof promptText === 'string' ? promptText : '';
|
|
194
|
-
const value = extractValue(prompt,
|
|
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) {
|
|
195
215
|
if (isPlaceholder(value)) return null;
|
|
196
216
|
|
|
197
217
|
for (const separator of [/\s+—\s+/, /\s+-\s+/, /\s*:\s*/]) {
|
|
@@ -205,6 +225,26 @@ export function parseContinuationGrant(promptText) {
|
|
|
205
225
|
return null;
|
|
206
226
|
}
|
|
207
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
|
+
|
|
208
248
|
/** Returns the immutable input entries for one dispatcher type. */
|
|
209
249
|
export function requiredInputsFor(agentType) {
|
|
210
250
|
return REQUIRED_INPUTS[agentType] || [];
|
|
@@ -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
|
|
|
@@ -53,7 +53,16 @@ export function readRunConfig(file = CONFIG_PATH) {
|
|
|
53
53
|
try {
|
|
54
54
|
parsed = readJsonFileSync(file);
|
|
55
55
|
} catch (err) {
|
|
56
|
-
|
|
56
|
+
// Only a missing file means "no config". Every error code used to mean that, so a config the
|
|
57
|
+
// run could not open (EACCES, EISDIR, EPERM) ran every role on default models with nobody told
|
|
58
|
+
// (Plan_69 stage 0) — the one reader of an operator file that treated failure as absence.
|
|
59
|
+
if (err.code === 'ENOENT') return { ...DEFAULTS };
|
|
60
|
+
if (err.code) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`${file} cannot be read (${err.code}). Fix its permissions or type, or delete it — without it, ` +
|
|
63
|
+
'the default mode applies: hooks and plugins are disabled.',
|
|
64
|
+
);
|
|
65
|
+
}
|
|
57
66
|
throw new Error(
|
|
58
67
|
`${file} cannot be parsed as JSON (${err.cause?.message || err.message}). Fix or delete the file — without it, ` +
|
|
59
68
|
'the default mode applies: hooks and plugins are disabled.',
|