@lyupro/codex-bridge 0.6.11 → 0.6.14
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 +139 -0
- package/README.md +12 -9
- package/cli/active-hosts.mjs +44 -0
- package/cli/command-registry.mjs +58 -1
- package/cli/dispatcher-model-check.mjs +14 -12
- package/cli/doctor-host-side.mjs +117 -111
- package/cli/doctor-installation.mjs +132 -111
- package/cli/doctor-load-witness.mjs +75 -0
- package/cli/doctor-runs.mjs +41 -6
- package/cli/doctor-sandbox.mjs +64 -0
- package/cli/doctor.mjs +43 -18
- package/cli/guidance.mjs +42 -0
- package/cli/handback-witness-check.mjs +23 -19
- package/cli/host-inspection.mjs +1 -0
- package/cli/hosts.mjs +4 -3
- package/cli/install-record.mjs +9 -2
- package/cli/inventory-confirm.mjs +75 -0
- package/cli/inventory-transition.mjs +3 -1
- package/cli/manifest.mjs +6 -3
- package/cli/read.mjs +1 -2
- package/cli/removal-outcomes.mjs +5 -2
- package/cli/removal-plan.mjs +4 -1
- package/cli/remove-layout.mjs +4 -4
- package/cli/run-lookup.mjs +14 -2
- package/cli/runs-inventory.mjs +10 -2
- package/cli/runs-move-copy.mjs +123 -0
- package/cli/runs-move-history.mjs +78 -0
- package/cli/runs-move-remove.mjs +92 -0
- package/cli/runs-move.mjs +96 -0
- package/cli/stop.mjs +1 -2
- package/cli/uninstall-purge.mjs +1 -0
- package/cli/uninstall.mjs +2 -0
- package/cli/update.mjs +12 -7
- package/package.json +4 -2
- package/scripts/home-write-inventory.mjs +17 -2
- package/scripts/isolated-roots.mjs +2 -1
- package/src/{agents → claude/agents}/codex-advisor.md +8 -7
- package/src/{agents → claude/agents}/codex-build.md +7 -6
- package/src/{agents → claude/agents}/codex-review.md +6 -5
- package/src/{agents → claude/agents}/codex-scout.md +7 -6
- package/src/{commands → claude/commands}/usage.md +9 -5
- package/src/claude/rules/core.md +29 -0
- package/src/{rules → codex/rules}/codex-bridge.rules +1 -1
- package/src/home/guidance/advisor.md +42 -0
- package/src/home/guidance/budget.md +37 -0
- package/src/home/guidance/concurrency.md +17 -0
- package/src/home/guidance/order.md +66 -0
- package/src/home/guidance/records.md +32 -0
- package/src/home/hooks/live-runs.mjs +2 -1
- package/src/home/hooks/load-witness.mjs +23 -0
- package/src/home/lib/claude-layout.mjs +1 -0
- package/src/home/lib/codex-home.mjs +8 -0
- package/src/home/lib/home-registry.mjs +21 -0
- package/src/home/lib/hook-definitions.mjs +2 -0
- package/src/home/lib/host-version.mjs +33 -14
- package/src/home/lib/install-owner-roots.mjs +55 -53
- package/src/home/lib/load-witness.mjs +113 -0
- package/src/home/lib/meta/reply.mjs +1 -1
- package/src/home/lib/runner/codex-availability.mjs +11 -2
- package/src/home/lib/runner/exit-codes.mjs +1 -1
- package/src/home/lib/runner/launcher.mjs +10 -4
- package/src/home/lib/runner/preflight.mjs +12 -4
- package/src/home/lib/runner/retired-roots.mjs +81 -0
- package/src/home/lib/runner/run-autocommit.mjs +109 -0
- package/src/home/lib/runner/runs-root.mjs +47 -7
- package/src/home/lib/runner/sandbox-diagnosis.mjs +180 -0
- package/src/home/lib/runner/sandbox-probe.mjs +29 -1
- package/src/home/lib/runner/task-input.mjs +30 -0
- package/src/home/lib/runner/worker.mjs +5 -0
- /package/src/{commands → claude/commands}/env.md +0 -0
- /package/src/{commands → claude/commands}/model.md +0 -0
package/cli/doctor-runs.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/** Reports the state of run storage on this machine: location, live count, retention. */
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
4
5
|
import { readRunConfig, retentionNotice } from '../src/home/lib/run-config.mjs';
|
|
5
|
-
import {
|
|
6
|
-
import { resolveProjectRunsDir } from '../src/home/lib/runner/project-dir.mjs';
|
|
6
|
+
import { runsRootResolution, staleOverrideRefusal } from '../src/home/lib/runner/runs-root.mjs';
|
|
7
|
+
import { normalizeRepoPath, resolveProjectRunsDir } from '../src/home/lib/runner/project-dir.mjs';
|
|
8
|
+
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
7
9
|
import { allLiveRuns } from '../src/home/hooks/live-runs.mjs';
|
|
8
10
|
import { STOP_COMMAND_TEMPLATE } from '../src/home/lib/stop-contract.mjs';
|
|
9
11
|
import { check } from './doctor-format.mjs';
|
|
@@ -27,14 +29,47 @@ function repoRoot(cwd) {
|
|
|
27
29
|
return top.status === 0 && top.stdout.trim() ? top.stdout.trim() : cwd;
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function runsGitTop(dir) {
|
|
33
|
+
if (!fs.existsSync(dir)) return null;
|
|
34
|
+
const top = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: dir, encoding: 'utf8', windowsHide: true });
|
|
35
|
+
return top.status === 0 && top.stdout.trim() ? top.stdout.trim() : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runsRootCheck(options = {}) {
|
|
39
|
+
try {
|
|
40
|
+
const resolution = options.resolution ?? runsRootResolution();
|
|
41
|
+
const gitTop = options.gitTop ?? runsGitTop;
|
|
42
|
+
const root = path.resolve(resolution.root);
|
|
43
|
+
const value = root + ' (' + resolution.source + ')';
|
|
44
|
+
if (resolution.staleOverride) {
|
|
45
|
+
return check('runsRoot', 'fail', value + ' — ' + staleOverrideRefusal(resolution));
|
|
46
|
+
}
|
|
47
|
+
if (resolution.source === 'legacy') {
|
|
48
|
+
return check('runsRoot', 'warn', value + ' — move pending: run codex-bridge runs move when no other project is running.');
|
|
49
|
+
}
|
|
50
|
+
if (resolution.source === 'moved' && fs.existsSync(resolution.legacyRoot)
|
|
51
|
+
&& fs.statSync(resolution.legacyRoot).isDirectory()) {
|
|
52
|
+
return check('runsRoot', 'warn', value + ' — the old folder ' + resolution.legacyRoot + ' still exists; codex-bridge runs move removes it.');
|
|
53
|
+
}
|
|
54
|
+
const top = gitTop(root);
|
|
55
|
+
if (top && normalizeRepoPath(top) !== normalizeRepoPath(root)
|
|
56
|
+
&& normalizeRepoPath(top) !== normalizeRepoPath(resolveBrandHome().root)) {
|
|
57
|
+
return check('runsRoot', 'warn', value + ' — run records land in the git repository ' + top + ', which is not their own; they accumulate there uncommitted.');
|
|
58
|
+
}
|
|
59
|
+
return check('runsRoot', 'ok', value);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return check('runsRoot', 'fail', err.message);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/**
|
|
31
66
|
* A marker that cannot be read is exactly what doctor exists to report, so it is caught here.
|
|
32
67
|
* Left to propagate it would kill the whole diagnosis and hide the seven checks around it.
|
|
33
68
|
*/
|
|
34
|
-
export function projectRunsCheck() {
|
|
69
|
+
export function projectRunsCheck(resolution) {
|
|
35
70
|
let resolved;
|
|
36
71
|
try {
|
|
37
|
-
resolved = resolveProjectRunsDir(
|
|
72
|
+
resolved = resolveProjectRunsDir(resolution.root, repoRoot(process.cwd()), { create: false });
|
|
38
73
|
} catch (err) {
|
|
39
74
|
return check('projectRuns', 'fail', err.message);
|
|
40
75
|
}
|
|
@@ -42,10 +77,10 @@ export function projectRunsCheck() {
|
|
|
42
77
|
return check('projectRuns', 'ok', `${path.resolve(resolved.dir)} (${note})`);
|
|
43
78
|
}
|
|
44
79
|
|
|
45
|
-
export function liveRunsCheck() {
|
|
80
|
+
export function liveRunsCheck(resolution) {
|
|
46
81
|
let count;
|
|
47
82
|
try {
|
|
48
|
-
count = allLiveRuns(
|
|
83
|
+
count = allLiveRuns(resolution.root, { requireConfirmedIdentity: true }).length;
|
|
49
84
|
} catch (err) {
|
|
50
85
|
return check('liveRuns', 'warn', `working-run count unavailable: ${err.message}`);
|
|
51
86
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Detects the dead sandbox before an order, after the 2026-09-16, 2026-09-25 and 2026-10-10 refusals. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { check } from './doctor-format.mjs';
|
|
5
|
+
import { repositoryRoot } from './hosts.mjs';
|
|
6
|
+
import { sandboxModeFor } from '../src/home/lib/runner/codex-args.mjs';
|
|
7
|
+
import { probeSandbox } from '../src/home/lib/runner/sandbox-probe.mjs';
|
|
8
|
+
import { formatSandboxDiagnosis } from '../src/home/lib/runner/sandbox-diagnosis.mjs';
|
|
9
|
+
|
|
10
|
+
// Probe reasons are full sentences; joining one to "; probed folder" printed "process.; probed" live.
|
|
11
|
+
const clause = (reason) => String(reason).replace(/\.$/, '');
|
|
12
|
+
|
|
13
|
+
// Plan_78 B4a: a timed-out attempt ends its form: at most 15 s plus ~15 s stopping
|
|
14
|
+
// (taskkill 10 s, then stop grace). Only one form can time out with the guard below;
|
|
15
|
+
// including the version check, the worst case stays well under the host shell's 120 s default.
|
|
16
|
+
export async function sandboxChecks({
|
|
17
|
+
codexAvailable, cwd = process.cwd(), probe = probeSandbox, platform = process.platform,
|
|
18
|
+
timeoutMs = 15_000,
|
|
19
|
+
} = {}) {
|
|
20
|
+
const root = repositoryRoot(cwd);
|
|
21
|
+
const folder = `${root} (${fs.existsSync(path.join(root, '.git')) ? 'git repository' : 'current folder'})`;
|
|
22
|
+
const rows = [];
|
|
23
|
+
let readOnlyInconclusive = false;
|
|
24
|
+
// Plan_57: concurrent helper setups race on one access-rights state file. Keep both forms sequential.
|
|
25
|
+
for (const agent of ['codex-scout', 'codex-build']) {
|
|
26
|
+
const key = `sandbox:${sandboxModeFor(agent)}`;
|
|
27
|
+
if (codexAvailable === false) {
|
|
28
|
+
rows.push(check(key, 'warn', `not probed: Codex CLI is unavailable; folder: ${folder}`));
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// Plan_57: if taskkill fails, only cmd.exe dies; Codex may still update shared sandbox state.
|
|
32
|
+
if (agent === 'codex-build' && readOnlyInconclusive) {
|
|
33
|
+
rows.push(check(key, 'warn', `not probed: the read-only probe was inconclusive; rerun codex-bridge doctor; probed folder: ${folder}`));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const result = await probe({ agent, repo: root, platform, timeoutMs });
|
|
37
|
+
if (agent === 'codex-scout') readOnlyInconclusive = result.outcome === 'inconclusive';
|
|
38
|
+
switch (result.outcome) {
|
|
39
|
+
case 'alive':
|
|
40
|
+
rows.push(check(key, 'ok', `${clause(result.reason)}; probed folder: ${folder}`));
|
|
41
|
+
break;
|
|
42
|
+
case 'dead': {
|
|
43
|
+
const diagnosis = result.diagnosis == null
|
|
44
|
+
? ['No sandbox log diagnosis is available.'] : formatSandboxDiagnosis(result.diagnosis);
|
|
45
|
+
rows.push(check(key, 'fail', [
|
|
46
|
+
result.reason,
|
|
47
|
+
...diagnosis.map((line) => ` ${line}`),
|
|
48
|
+
' Repair the host sandbox and rerun codex-bridge doctor.',
|
|
49
|
+
` Probed folder: ${folder}`,
|
|
50
|
+
].join('\n')));
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
case 'inconclusive':
|
|
54
|
+
rows.push(check(key, 'warn', `readiness not established: ${clause(result.reason)}; probed folder: ${folder}`));
|
|
55
|
+
break;
|
|
56
|
+
case 'skipped':
|
|
57
|
+
rows.push(check(key, 'warn', `not probed on ${platform}: the package has never observed this platform's sandbox; folder: ${folder}`));
|
|
58
|
+
break;
|
|
59
|
+
default:
|
|
60
|
+
throw new Error(`unknown sandbox probe outcome: ${result.outcome}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return rows;
|
|
64
|
+
}
|
package/cli/doctor.mjs
CHANGED
|
@@ -5,12 +5,13 @@ import { spawnSync } from 'node:child_process';
|
|
|
5
5
|
import { packageSource } from './package-source.mjs';
|
|
6
6
|
import { readInstallRecord, packageInfo } from './manifest.mjs';
|
|
7
7
|
import { readInstallRecordFile, recordTarget } from './install-record.mjs';
|
|
8
|
-
import {
|
|
8
|
+
import { runsRootResolution } from '../src/home/lib/runner/runs-root.mjs';
|
|
9
9
|
import { check, renderDoctor } from './doctor-format.mjs';
|
|
10
10
|
import { homeOwnersCheck, hostSideCheck, ownerEntry, ownersInSyncCheck } from './doctor-host-side.mjs';
|
|
11
11
|
import { inspectHost } from './host-inspection.mjs';
|
|
12
12
|
import {
|
|
13
13
|
agentsCheck,
|
|
14
|
+
claudeRulesCheck,
|
|
14
15
|
conventionsCheck,
|
|
15
16
|
exists,
|
|
16
17
|
isFile,
|
|
@@ -18,18 +19,20 @@ import {
|
|
|
18
19
|
rulesCheck,
|
|
19
20
|
} from './doctor-installation.mjs';
|
|
20
21
|
import { hookChecks } from './doctor-hooks.mjs';
|
|
22
|
+
import { loadWitnessChecks } from './doctor-load-witness.mjs';
|
|
21
23
|
import { contractStatus, readHostContract } from './host-contract.mjs';
|
|
22
|
-
import { handbackWitnessStatus } from './handback-witness-check.mjs';
|
|
24
|
+
import { handbackWitnessStatus } from './handback-witness-check.mjs';
|
|
23
25
|
import { dispatcherModelChecks } from './dispatcher-model-check.mjs';
|
|
24
26
|
import { dispatcherContractStatus } from './dispatcher-contract.mjs';
|
|
25
27
|
import { DISPATCHER_CONTRACT_FILE, readDispatcherContract } from './dispatcher-contract-record.mjs';
|
|
26
28
|
import { PROBE_COMMAND } from './host-contract.mjs';
|
|
27
|
-
import { readHandbackWitness } from '../src/home/lib/handback-witness.mjs';
|
|
29
|
+
import { readHandbackWitness } from '../src/home/lib/handback-witness.mjs';
|
|
28
30
|
import { readDispatcherModel } from '../src/home/lib/dispatcher-model.mjs';
|
|
29
31
|
import { brandStateDir } from '../src/home/lib/brand-home.mjs';
|
|
30
32
|
import { readHostObservations } from '../src/home/lib/host-observations.mjs';
|
|
33
|
+
import { activeHostVersions } from './active-hosts.mjs';
|
|
31
34
|
import { otherHostCheck, sessionHostCheck, sessionHostVersions } from './session-hosts.mjs';
|
|
32
|
-
import { liveRunsCheck, projectRunsCheck, retentionCheck } from './doctor-runs.mjs';
|
|
35
|
+
import { liveRunsCheck, projectRunsCheck, retentionCheck, runsRootCheck } from './doctor-runs.mjs';
|
|
33
36
|
|
|
34
37
|
export { renderDoctor };
|
|
35
38
|
|
|
@@ -47,7 +50,10 @@ function sourceCheck() {
|
|
|
47
50
|
export function probeCodex() {
|
|
48
51
|
const command = process.platform === 'win32' ? process.env.ComSpec || 'cmd.exe' : 'codex';
|
|
49
52
|
const args = process.platform === 'win32' ? ['/d', '/s', '/c', 'codex --version'] : ['--version'];
|
|
50
|
-
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true });
|
|
53
|
+
const result = spawnSync(command, args, { encoding: 'utf8', windowsHide: true, timeout: 15_000 });
|
|
54
|
+
if (result.error?.code === 'ETIMEDOUT') {
|
|
55
|
+
return { available: false, value: 'Codex CLI version check timed out after 15000 ms.' };
|
|
56
|
+
}
|
|
51
57
|
if (result.error || result.status !== 0) {
|
|
52
58
|
return { available: false, value: (result.stderr || result.error?.message || 'not found').trim() };
|
|
53
59
|
}
|
|
@@ -80,14 +86,17 @@ function dispatcherContractChecks({ record, version, stateDir }) {
|
|
|
80
86
|
export async function diagnose({
|
|
81
87
|
host,
|
|
82
88
|
codexProbe = probeCodex,
|
|
89
|
+
sandbox,
|
|
83
90
|
bridgeProbe = probeCodexBridge,
|
|
84
91
|
launcherProbe,
|
|
85
92
|
currentPackage,
|
|
86
93
|
contractRecord,
|
|
87
|
-
handbackWitnessRecord,
|
|
94
|
+
handbackWitnessRecord,
|
|
88
95
|
dispatcherModelRecord,
|
|
89
96
|
dispatcherContractRecord,
|
|
90
97
|
hostVersion,
|
|
98
|
+
activeHosts: injectedActiveHosts,
|
|
99
|
+
now,
|
|
91
100
|
observations,
|
|
92
101
|
} = {}) {
|
|
93
102
|
const checks = [sourceCheck()];
|
|
@@ -142,6 +151,8 @@ export async function diagnose({
|
|
|
142
151
|
checks.push(agents);
|
|
143
152
|
const rules = await rulesCheck(host, record);
|
|
144
153
|
checks.push(rules);
|
|
154
|
+
const claudeRules = await claudeRulesCheck(host, record);
|
|
155
|
+
checks.push(claudeRules);
|
|
145
156
|
checks.push(await permissionsCheck(host));
|
|
146
157
|
const bridge = bridgeProbe();
|
|
147
158
|
checks.push(bridgeCommandCheck(bridge));
|
|
@@ -150,6 +161,11 @@ export async function diagnose({
|
|
|
150
161
|
const hostObservations = observations === undefined ? readHostObservations({ stateDir }) : observations;
|
|
151
162
|
const observedVersions = sessionHostVersions(hostObservations);
|
|
152
163
|
const detectedHostVersion = hostVersion === undefined ? observedVersions[0] ?? null : hostVersion;
|
|
164
|
+
const witnessRecord = handbackWitnessRecord === undefined ? readHandbackWitness({ stateDir }) : handbackWitnessRecord;
|
|
165
|
+
const modelRecord = dispatcherModelRecord === undefined ? readDispatcherModel({ stateDir }) : dispatcherModelRecord;
|
|
166
|
+
const activeHosts = injectedActiveHosts ?? (hostVersion !== undefined ? [hostVersion].filter(Boolean)
|
|
167
|
+
: activeHostVersions({ observations: hostObservations, witnessRecord, modelRecord, now }));
|
|
168
|
+
checks.push(...await loadWitnessChecks({ host, record, stateDir, activeHosts }));
|
|
153
169
|
checks.push(sessionHostCheck(observedVersions, hostObservations));
|
|
154
170
|
const refusalRecord = contractRecord === undefined ? await readHostContract(host) : contractRecord;
|
|
155
171
|
const hostContract = contractStatus({
|
|
@@ -161,15 +177,15 @@ export async function diagnose({
|
|
|
161
177
|
: hostContract.state === 'ignored' ? 'fail' : 'warn';
|
|
162
178
|
checks.push(check('hostContract', hostContractStatus, hostContract.message));
|
|
163
179
|
const witness = handbackWitnessStatus({
|
|
164
|
-
record:
|
|
165
|
-
|
|
180
|
+
record: witnessRecord,
|
|
181
|
+
activeHosts,
|
|
166
182
|
stateDir,
|
|
167
183
|
});
|
|
168
|
-
checks.push(check('handbackWitness', ['violation', 'unreadable'].includes(witness.state) ? 'warn' : 'ok', witness.message));
|
|
169
|
-
checks.push(...dispatcherModelChecks({
|
|
170
|
-
record:
|
|
171
|
-
|
|
172
|
-
stateDir,
|
|
184
|
+
checks.push(check('handbackWitness', ['violation', 'unreadable'].includes(witness.state) ? 'warn' : 'ok', witness.message));
|
|
185
|
+
checks.push(...dispatcherModelChecks({
|
|
186
|
+
record: modelRecord,
|
|
187
|
+
activeHosts,
|
|
188
|
+
stateDir,
|
|
173
189
|
}));
|
|
174
190
|
const dispatcherRecord = dispatcherContractRecord === undefined ? readDispatcherContract({ stateDir }) : dispatcherContractRecord;
|
|
175
191
|
checks.push(...dispatcherContractChecks({
|
|
@@ -187,18 +203,27 @@ export async function diagnose({
|
|
|
187
203
|
|
|
188
204
|
const codex = codexProbe();
|
|
189
205
|
checks.push(check('codex', codex.available ? 'ok' : 'warn', codex.value || 'available'));
|
|
206
|
+
if (typeof sandbox === 'function') checks.push(...await sandbox({ codexAvailable: codex.available }));
|
|
190
207
|
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0], 10);
|
|
191
208
|
checks.push(check('node', nodeMajor >= 24 ? 'ok' : 'fail', `${process.versions.node} (requires >=24)`));
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
209
|
+
// Plan_77 D6: catch a corrupt move record without aborting doctor, and share one resolution.
|
|
210
|
+
let resolution;
|
|
211
|
+
const runsRoot = runsRootCheck({ get resolution() { return (resolution = runsRootResolution()); } });
|
|
212
|
+
checks.push(runsRoot);
|
|
213
|
+
const unavailable = 'unavailable: CODEX_RUNS_ROOT points under a retired runs root';
|
|
214
|
+
const projectRuns = resolution?.staleOverride ? check('projectRuns', 'fail', unavailable)
|
|
215
|
+
: resolution ? projectRunsCheck(resolution) : check('projectRuns', 'fail', runsRoot.value);
|
|
216
|
+
checks.push(resolution?.staleOverride ? check('liveRuns', 'fail', unavailable)
|
|
217
|
+
: resolution ? liveRunsCheck(resolution)
|
|
218
|
+
: check('liveRuns', 'warn', `working-run count unavailable: ${runsRoot.value}`));
|
|
195
219
|
checks.push(projectRuns);
|
|
196
220
|
|
|
197
221
|
return {
|
|
198
222
|
exitCode: !record || recordBroken || missingFiles.length || agents.status === 'fail' || rules.status === 'fail'
|
|
223
|
+
|| claudeRules.status === 'fail'
|
|
199
224
|
|| hostContractStatus === 'fail' || retention.status === 'fail' || conventions.status === 'fail'
|
|
200
|
-
|| projectRuns.status === 'fail'
|
|
201
|
-
|| checks.some((item) => /^(hook|dispatcherContract|dispatcherModel):/.test(item.key) && item.status === 'fail')
|
|
225
|
+
|| runsRoot.status === 'fail' || projectRuns.status === 'fail'
|
|
226
|
+
|| checks.some((item) => /^(hook|dispatcherContract|dispatcherModel|sandbox):/.test(item.key) && item.status === 'fail')
|
|
202
227
|
|| checks.some((item) => item.key.startsWith('otherHost:') && item.status === 'fail') ? 1 : 0,
|
|
203
228
|
checks,
|
|
204
229
|
record,
|
package/cli/guidance.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Prints detailed package guidance from the installed brand home. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
5
|
+
|
|
6
|
+
export const GUIDANCE_TOPICS = Object.freeze({
|
|
7
|
+
order: 'Task file, header labels, and scope.',
|
|
8
|
+
advisor: 'Scope and advise phases, and the continue grant.',
|
|
9
|
+
budget: 'Run deadline and slicing.',
|
|
10
|
+
records: 'Finding and reading runs.',
|
|
11
|
+
concurrency: 'Parallel and writing runs.',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const topicList = () => [
|
|
15
|
+
'Topics:',
|
|
16
|
+
...Object.entries(GUIDANCE_TOPICS).map(([topic, summary]) => ` ${topic} — ${summary}`),
|
|
17
|
+
'',
|
|
18
|
+
'Usage: codex-bridge guidance [<topic>]',
|
|
19
|
+
].join('\n');
|
|
20
|
+
|
|
21
|
+
export function guidance(argv, { env = process.env, homedir } = {}) {
|
|
22
|
+
if (argv.length === 0) return { exitCode: 0, output: topicList() };
|
|
23
|
+
if (argv.length > 1) {
|
|
24
|
+
return { exitCode: 2, output: `codex-bridge guidance accepts at most one topic.\n${topicList()}` };
|
|
25
|
+
}
|
|
26
|
+
const [topic] = argv;
|
|
27
|
+
if (!Object.hasOwn(GUIDANCE_TOPICS, topic)) {
|
|
28
|
+
return { exitCode: 2, output: `Unknown guidance topic "${topic}".\n${topicList()}` };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// The 2026-08-26 seeded-config incident forbids reading installed files from the clone.
|
|
32
|
+
const { root } = resolveBrandHome({ homedir, env });
|
|
33
|
+
const file = path.join(root, 'guidance', `${topic}.md`);
|
|
34
|
+
try {
|
|
35
|
+
return { exitCode: 0, output: fs.readFileSync(file, 'utf8') };
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
exitCode: 1,
|
|
39
|
+
output: `Cannot read guidance file ${file}: ${error.message}\nrun codex-bridge update`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,37 +1,41 @@
|
|
|
1
|
-
/** Surfaces handback drift;
|
|
1
|
+
/** Surfaces handback drift; Plan_67 D12 judges every host with recent dispatcher activity. */
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { WITNESS_FILE } from '../src/home/lib/handback-witness.mjs';
|
|
4
4
|
import { entryState } from '../src/home/lib/observation-ledger.mjs';
|
|
5
5
|
|
|
6
|
-
export function handbackWitnessStatus({ record,
|
|
6
|
+
export function handbackWitnessStatus({ record, activeHosts, stateDir }) {
|
|
7
7
|
if (record?.corrupt) {
|
|
8
8
|
return { state: 'unreadable', message: `The handback witness is unreadable; delete ${path.join(stateDir, WITNESS_FILE)} to reset it.` };
|
|
9
9
|
}
|
|
10
10
|
const entries = Object.values(record?.ledger?.entries || {});
|
|
11
|
+
let history = '';
|
|
12
|
+
const recovered = entries.filter((entry) => entryState(entry) === 'recovered').length;
|
|
13
|
+
const historical = entries.filter((entry) => entryState(entry) === 'violation'
|
|
14
|
+
&& !activeHosts.includes(entry.lastObservation.data.hostVersion)).length;
|
|
15
|
+
const legacy = record?.legacy || [];
|
|
16
|
+
if (recovered || historical || legacy.length) {
|
|
17
|
+
const untyped = legacy.filter((entry) => entry.disposition === 'legacy-untyped-unverified').length;
|
|
18
|
+
const unclassified = legacy.filter((entry) => entry.disposition === 'unclassified').length;
|
|
19
|
+
history = ` History: ${recovered} recovered entries; ${historical} unresolved entries on hosts without dispatcher activity in the last 24 hours (history); ${untyped} older alarms without dispatcher evidence, kept for history (legacy-untyped-unverified); ${unclassified} unclassified legacy entries.`;
|
|
20
|
+
}
|
|
11
21
|
const violations = entries.filter((entry) => entryState(entry) === 'violation'
|
|
12
|
-
&& entry.lastObservation.data.hostVersion
|
|
22
|
+
&& activeHosts.includes(entry.lastObservation.data.hostVersion));
|
|
13
23
|
if (violations.length > 0) {
|
|
14
24
|
const details = violations.map((entry) => {
|
|
15
25
|
const { data, detail, at } = entry.lastViolation;
|
|
16
|
-
return
|
|
26
|
+
return `host ${data.hostVersion}: ${data.cause}${data.agentType ? ` / ${data.agentType}` : ''}: ${detail} at ${at}`;
|
|
17
27
|
});
|
|
18
28
|
return {
|
|
19
29
|
state: 'violation',
|
|
20
|
-
message: `Dispatcher violation(s)
|
|
30
|
+
message: `Dispatcher violation(s): ${details.join('; ')}; do not trust a dispatcher answer from that time; see Plan_62 D15.${history}`,
|
|
21
31
|
};
|
|
22
32
|
}
|
|
23
|
-
const intercepted = record?.intercepted?.[
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (recovered || historical || legacy.length) {
|
|
32
|
-
const untyped = legacy.filter((entry) => entry.disposition === 'legacy-untyped-unverified').length;
|
|
33
|
-
const unclassified = legacy.filter((entry) => entry.disposition === 'unclassified').length;
|
|
34
|
-
message += ` History: ${recovered} recovered entries; ${historical} unresolved entries on other hosts (historical, not current); ${untyped} older alarms without dispatcher evidence, kept for history (legacy-untyped-unverified); ${unclassified} unclassified legacy entries.`;
|
|
35
|
-
}
|
|
36
|
-
return { state: 'ok', message };
|
|
33
|
+
const intercepted = activeHosts.filter((host) => record?.intercepted?.[host])
|
|
34
|
+
.map((host) => `host ${host} at ${record.intercepted[host]}`);
|
|
35
|
+
const message = !activeHosts.length
|
|
36
|
+
? 'No dispatcher activity in the last 24 hours.'
|
|
37
|
+
: intercepted.length
|
|
38
|
+
? `Newest intercepted handback on ${intercepted.join('; ')}.`
|
|
39
|
+
: 'Not observed yet — a handback is seen only when a dispatcher runs in an interactive session.';
|
|
40
|
+
return { state: 'ok', message: message + history };
|
|
37
41
|
}
|
package/cli/host-inspection.mjs
CHANGED
|
@@ -144,6 +144,7 @@ export async function inspectHost(host, { owner = null, packageRoot } = {}) {
|
|
|
144
144
|
};
|
|
145
145
|
await walk(context, host.agentsDir);
|
|
146
146
|
await walk(context, host.commandsDir);
|
|
147
|
+
await walk(context, host.rulesDir);
|
|
147
148
|
await inspectLegacyDirectory(context, host.legacyAgentsDir, plan.filter((item) => isWithin(host.agentsDir, item.target)));
|
|
148
149
|
await inspectLegacyDirectory(context, host.legacyCommandsDir, plan.filter((item) => isWithin(host.commandsDir, item.target)));
|
|
149
150
|
await inspectLegacyRecord(context);
|
package/cli/hosts.mjs
CHANGED
|
@@ -3,9 +3,10 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
6
|
-
import { claudePaths } from '../src/home/lib/claude-layout.mjs';
|
|
6
|
+
import { claudePaths } from '../src/home/lib/claude-layout.mjs';
|
|
7
|
+
import { resolveCodexHome } from '../src/home/lib/codex-home.mjs';
|
|
7
8
|
|
|
8
|
-
function repositoryRoot(start) {
|
|
9
|
+
export function repositoryRoot(start) {
|
|
9
10
|
let current = path.resolve(start);
|
|
10
11
|
while (true) {
|
|
11
12
|
if (fs.existsSync(path.join(current, '.git'))) return current;
|
|
@@ -23,7 +24,7 @@ export function resolveHost({
|
|
|
23
24
|
codexHome,
|
|
24
25
|
brandRoot,
|
|
25
26
|
} = {}) {
|
|
26
|
-
const resolvedCodexHome = codexHome ||
|
|
27
|
+
const resolvedCodexHome = codexHome || resolveCodexHome({ homedir });
|
|
27
28
|
const codexRulesDir = path.join(resolvedCodexHome, 'rules');
|
|
28
29
|
// Plan_25 moves package-owned runtime files out of foreign Claude settings, whose absolute
|
|
29
30
|
// hook paths caused the package-file-layout incident. Keep the brand root overrideable so tests
|
package/cli/install-record.mjs
CHANGED
|
@@ -78,8 +78,10 @@ function normalizedFiles(record) {
|
|
|
78
78
|
validPath(entry.path, 'installation record file path');
|
|
79
79
|
// Run artifacts are the user's data, not ours. Refusing them here means neither uninstall nor
|
|
80
80
|
// update needs its own guard against deleting a run folder someone listed as an installed file.
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// Plan_77 D1: the records move to runs/ of the brand home, so that folder is refused too.
|
|
82
|
+
const top = entry.path.split(/[\\/]/)[0];
|
|
83
|
+
if (top === 'codex-runs' || (entry.root === 'brand' && top === 'runs')) {
|
|
84
|
+
throw new Error(`installation record files must not name run artifacts under ${top}`);
|
|
83
85
|
}
|
|
84
86
|
return { root: entry.root, path: entry.path };
|
|
85
87
|
});
|
|
@@ -307,6 +309,11 @@ async function publishInstallRecord(host, next) {
|
|
|
307
309
|
await writer.rename('install-record', temporary, target);
|
|
308
310
|
}
|
|
309
311
|
|
|
312
|
+
/** Plan_67 D11 / A4: changing inventory must not rebuild owners or hide fingerprint lag. */
|
|
313
|
+
export function publishInstallRecordOnly(host, next) {
|
|
314
|
+
return publishInstallRecord(host, next);
|
|
315
|
+
}
|
|
316
|
+
|
|
310
317
|
/**
|
|
311
318
|
* Publishes the record whole through a temporary and a rename: a direct write left a half-written
|
|
312
319
|
* record for every host sharing the home if the process died mid-write (advice A3, risk r3).
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Lets the operator declare a shared home's installation inventory complete, the one statement the package cannot prove.
|
|
2
|
+
* Plan_67 D11, OW-047: only the operator can account for unrecorded hosts using this home.
|
|
3
|
+
*/
|
|
4
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
5
|
+
import { publishInstallRecordOnly, readInstallRecordFile } from './install-record.mjs';
|
|
6
|
+
import { validateFormat2 } from './install-owners.mjs';
|
|
7
|
+
import { registryHintLines } from './inventory-transition.mjs';
|
|
8
|
+
import { withLifecycle } from './lifecycle-transaction.mjs';
|
|
9
|
+
import { readRulesRegistry } from './rules-owners.mjs';
|
|
10
|
+
import { askYesNo, isInteractive } from './terminal-question.mjs';
|
|
11
|
+
|
|
12
|
+
export const INVENTORY_CONFIRM_COMMAND = 'codex-bridge inventory confirm';
|
|
13
|
+
|
|
14
|
+
const question = 'Are these all the hosts using this home?';
|
|
15
|
+
const updateHint = 'Run codex-bridge update to migrate the installation record.';
|
|
16
|
+
|
|
17
|
+
async function confirmInRun(host, dryRun, questionOptions) {
|
|
18
|
+
let record;
|
|
19
|
+
try {
|
|
20
|
+
record = await readInstallRecordFile(host);
|
|
21
|
+
if (!isFormat2(record)) {
|
|
22
|
+
return { exitCode: 1, output: `No format-2 installation record was found. ${updateHint}` };
|
|
23
|
+
}
|
|
24
|
+
validateFormat2(record);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return { exitCode: 1, output: `Invalid installation record: ${error.message} ${updateHint}` };
|
|
27
|
+
}
|
|
28
|
+
const owners = Object.values(record.owners);
|
|
29
|
+
if (owners.length === 0) {
|
|
30
|
+
return { exitCode: 1, output: 'no recorded owners; install into each host first' };
|
|
31
|
+
}
|
|
32
|
+
if (record.inventory === 'complete' && !Object.hasOwn(record, 'legacy')) {
|
|
33
|
+
return { exitCode: 0, output: 'The inventory is already complete.' };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const screen = [
|
|
37
|
+
`Home: ${host.brandRoot}`,
|
|
38
|
+
'Recorded hosts:',
|
|
39
|
+
...owners.map((owner) => ` ${owner.root}`),
|
|
40
|
+
...registryHintLines(host, (await readRulesRegistry(host))?.owners ?? []),
|
|
41
|
+
"Confirming lets the last recorded host's uninstall remove the shared image. "
|
|
42
|
+
+ 'A host that uses this home but is not listed must be enrolled first: '
|
|
43
|
+
+ 'codex-bridge install --host "<path>".',
|
|
44
|
+
].join('\n');
|
|
45
|
+
if (dryRun) return { exitCode: 0, output: `${screen}\nDry run: nothing changed.` };
|
|
46
|
+
const prompt = `${screen}\n${question}`;
|
|
47
|
+
if (!isInteractive(questionOptions)) {
|
|
48
|
+
return { exitCode: 1, output: `${prompt}\nRun this command in a terminal to answer.` };
|
|
49
|
+
}
|
|
50
|
+
const answer = await askYesNo(prompt, questionOptions);
|
|
51
|
+
if (answer === 'cancel') {
|
|
52
|
+
return { exitCode: 130, output: `${prompt}\nCancelled; nothing was changed.` };
|
|
53
|
+
}
|
|
54
|
+
if (answer !== 'yes') {
|
|
55
|
+
return { exitCode: 1, output: `${prompt}\nNothing changed: the inventory stays incomplete.` };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A4 pre-mortem: the raw fields are evidence of lag, not material to regenerate on confirmation.
|
|
59
|
+
const next = { ...record, inventory: 'complete' };
|
|
60
|
+
delete next.legacy;
|
|
61
|
+
await publishInstallRecordOnly(host, next);
|
|
62
|
+
return { exitCode: 0, output: `${prompt}\nRecorded the inventory as complete.` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function inventoryConfirm({ host, dryRun = false, ...questionOptions }) {
|
|
66
|
+
const action = (ticket) => {
|
|
67
|
+
// Plan_67 R2: a first install may create the home after the unlocked missing-home check.
|
|
68
|
+
if (!dryRun && ticket === undefined) {
|
|
69
|
+
return { exitCode: 1, output: `No format-2 installation record was found. ${updateHint}` };
|
|
70
|
+
}
|
|
71
|
+
return confirmInRun(host, dryRun, questionOptions);
|
|
72
|
+
};
|
|
73
|
+
// Plan_67 D11: dry-run only reads; a real declaration holds the lifecycle lock before reading.
|
|
74
|
+
return dryRun ? action() : withLifecycle(host, 'inventory', action);
|
|
75
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* store nothing about a refusal.
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
|
+
import { existsSync } from 'node:fs';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { askYesNo, isInteractive } from './terminal-question.mjs';
|
|
8
9
|
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
@@ -36,7 +37,8 @@ export function registryHintLines(host, candidates) {
|
|
|
36
37
|
const otherCandidates = candidates.filter((root) => root !== owner);
|
|
37
38
|
return [
|
|
38
39
|
'Codex rules registry host roots are a hint only; they may be stale or belong to another home:',
|
|
39
|
-
...(otherCandidates.length ? otherCandidates.map((root) =>
|
|
40
|
+
...(otherCandidates.length ? otherCandidates.map((root) =>
|
|
41
|
+
` ${root}${existsSync(root) ? '' : ' (folder does not exist)'}`) : [' none found']),
|
|
40
42
|
];
|
|
41
43
|
}
|
|
42
44
|
|
package/cli/manifest.mjs
CHANGED
|
@@ -45,7 +45,7 @@ export {
|
|
|
45
45
|
|
|
46
46
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
47
47
|
export const PACKAGE_ROOT = path.resolve(HERE, '..');
|
|
48
|
-
const RULES_SOURCE = `src/rules/${RULES_NAME}`;
|
|
48
|
+
const RULES_SOURCE = `src/codex/rules/${RULES_NAME}`;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Files the operator owns once they exist. Seeded on first install so the defaults are
|
|
@@ -57,8 +57,9 @@ const RULES_SOURCE = `src/rules/${RULES_NAME}`;
|
|
|
57
57
|
export const SEEDED_SOURCES = Object.freeze(['src/home/config.json', 'src/home/conventions.md']);
|
|
58
58
|
|
|
59
59
|
export const INSTALL_TABLE = Object.freeze([
|
|
60
|
-
{ source: 'src/agents/*.md', root: 'claude', target: 'agentsDir', processing: 'placeholders' },
|
|
61
|
-
{ source: 'src/commands/*.md', root: 'claude', target: 'commandsDir', processing: 'placeholders' },
|
|
60
|
+
{ source: 'src/claude/agents/*.md', root: 'claude', target: 'agentsDir', processing: 'placeholders' },
|
|
61
|
+
{ source: 'src/claude/commands/*.md', root: 'claude', target: 'commandsDir', processing: 'placeholders' },
|
|
62
|
+
{ source: 'src/claude/rules/*.md', base: 'src/claude/rules', root: 'claude', target: 'rulesDir', processing: 'copy' },
|
|
62
63
|
// The source is the host image. Keeping one source root and one target root makes any future
|
|
63
64
|
// remapping visible as a test failure instead of another clone-only import success.
|
|
64
65
|
{ source: 'src/home/**', root: 'brand', target: 'brandRoot', processing: 'copy' },
|
|
@@ -139,6 +140,8 @@ function rootFor(host, mapping) {
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
function targetRelative(packageRoot, source, mapping) {
|
|
143
|
+
// Plan_64 D9: copy rows outside src/home need their own base to avoid escaping the target directory.
|
|
144
|
+
if (mapping.base) return path.relative(path.join(packageRoot, mapping.base), source);
|
|
142
145
|
if (mapping.processing === 'placeholders' || mapping.source === 'package.json') {
|
|
143
146
|
return path.basename(source);
|
|
144
147
|
}
|
package/cli/read.mjs
CHANGED
|
@@ -3,7 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
import { readJsonFileSync } from '../src/home/lib/json-file.mjs';
|
|
4
4
|
import { readEvents } from '../src/home/lib/meta/events.mjs';
|
|
5
5
|
import { safeSlice } from '../src/home/lib/meta/paths.mjs';
|
|
6
|
-
import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
|
|
7
6
|
import { resolveRunFolder } from './run-lookup.mjs';
|
|
8
7
|
|
|
9
8
|
const result = (exitCode, output) => ({ exitCode, output });
|
|
@@ -85,7 +84,7 @@ function renderEvent(event, transportError) {
|
|
|
85
84
|
}
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
export function read({ run, cwd = process.cwd(), runsRootPath
|
|
87
|
+
export function read({ run, cwd = process.cwd(), runsRootPath } = {}) {
|
|
89
88
|
const lookup = resolveRunFolder({ command: 'read', run, cwd, runsRootPath });
|
|
90
89
|
if (lookup.error) return result(1, lookup.error);
|
|
91
90
|
|
package/cli/removal-outcomes.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Plan_65 D12 item 7 requires actual counts and named keeps: planned removal must not claim
|
|
5
5
|
* that edited image files disappeared, or confuse removing an owner with deleting its record.
|
|
6
6
|
*/
|
|
7
|
-
import {
|
|
7
|
+
import { runsRootResolution } from '../src/home/lib/runner/runs-root.mjs';
|
|
8
8
|
|
|
9
9
|
const unsafeReason = (reason) => reason.startsWith('link') || reason.startsWith('unreadable');
|
|
10
10
|
// An image file kept by content evidence is named; one kept by policy (another owner, an incomplete
|
|
@@ -12,7 +12,10 @@ const unsafeReason = (reason) => reason.startsWith('link') || reason.startsWith(
|
|
|
12
12
|
const EVIDENCE_KEEP = new Set(['changed', 'link']);
|
|
13
13
|
const namedKeep = (entry) => unsafeReason(entry.reason) || entry.id === null
|
|
14
14
|
|| (entry.id === 'install-image' && EVIDENCE_KEEP.has(entry.reason));
|
|
15
|
-
const artifactsLine = () =>
|
|
15
|
+
const artifactsLine = () => {
|
|
16
|
+
const resolution = runsRootResolution();
|
|
17
|
+
return `Run artifacts in ${resolution.staleOverride ? resolution.homeRoot : resolution.root} are outside uninstall and stay.`;
|
|
18
|
+
};
|
|
16
19
|
|
|
17
20
|
function fileLines(files, host, dryRun) {
|
|
18
21
|
const lines = [];
|
package/cli/removal-plan.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import {
|
|
9
9
|
HOME_ARTIFACTS,
|
|
10
10
|
HOME_DIRECTORIES,
|
|
11
|
+
RUN_RECORDS_DIR,
|
|
11
12
|
classifyHomePath,
|
|
12
13
|
homeArtifact,
|
|
13
14
|
} from '../src/home/lib/home-registry.mjs';
|
|
@@ -99,7 +100,9 @@ export function planHomeFiles({ command, inspection, imageMembers, imageEvidence
|
|
|
99
100
|
}
|
|
100
101
|
for (const entry of inspection.unknown) {
|
|
101
102
|
add(entry.relative, { id: null, role: null, removal: null }, {
|
|
102
|
-
|
|
103
|
+
// The inspection never walks an undeclared folder; the run records folder is one, by design.
|
|
104
|
+
action: 'keep', reason: entry.relative === RUN_RECORDS_DIR && entry.kind === 'directory' ? 'run records'
|
|
105
|
+
: entry.kind === 'directory' ? 'unknown directory' : 'unknown',
|
|
103
106
|
});
|
|
104
107
|
}
|
|
105
108
|
for (const file of inspection.files) {
|