@lyupro/codex-bridge 0.6.10 → 0.6.12
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 +115 -0
- package/README.md +16 -4
- package/cli/active-hosts.mjs +44 -0
- package/cli/command-registry.mjs +18 -2
- package/cli/dispatcher-model-check.mjs +61 -0
- package/cli/doctor-host-side.mjs +117 -110
- package/cli/doctor-installation.mjs +111 -111
- package/cli/doctor.mjs +19 -4
- package/cli/handback-witness-check.mjs +30 -20
- package/cli/hosts.mjs +2 -10
- package/cli/install-owners.mjs +1 -4
- package/cli/install-record.mjs +7 -1
- package/cli/inventory-confirm.mjs +75 -0
- package/cli/inventory-transition.mjs +4 -2
- package/cli/manifest.mjs +6 -6
- package/package.json +2 -2
- package/src/agents/codex-advisor.md +25 -119
- package/src/agents/codex-build.md +15 -133
- package/src/agents/codex-review.md +10 -109
- package/src/agents/codex-scout.md +11 -130
- package/src/home/hooks/dispatcher-gate.mjs +14 -9
- package/src/home/hooks/live-runs.mjs +0 -41
- package/src/home/hooks/order-gate.mjs +59 -40
- package/src/home/hooks/reply-guard.mjs +52 -65
- package/src/home/hooks/reply-identity.mjs +64 -0
- package/src/home/hooks/reply-verdicts.mjs +31 -27
- package/src/home/hooks/reply-witness.mjs +48 -0
- package/src/home/lib/claude-layout.mjs +14 -0
- package/src/home/lib/dispatcher-call.mjs +171 -0
- package/src/home/lib/dispatcher-command.mjs +18 -56
- package/src/home/lib/dispatcher-gate.mjs +4 -0
- package/src/home/lib/dispatcher-model.mjs +112 -0
- package/src/home/lib/dispatcher-order.mjs +21 -22
- package/src/home/lib/dispatcher-pin.mjs +60 -0
- package/src/home/lib/dispatcher-protocol.mjs +91 -0
- package/src/home/lib/dispatcher-stop.mjs +11 -2
- package/src/home/lib/handback-witness.mjs +137 -48
- package/src/home/lib/home-registry.mjs +7 -0
- package/src/home/lib/install-owner-roots.mjs +55 -0
- package/src/home/lib/meta/paths.mjs +1 -1
- package/src/home/lib/meta/reply.mjs +2 -1
- package/src/home/lib/model-family.mjs +38 -0
- package/src/home/lib/observation-ledger.mjs +145 -0
- package/src/home/lib/order-schema.mjs +204 -0
- package/src/home/lib/order-values.mjs +46 -0
- package/src/home/lib/runner/args.mjs +140 -231
- package/src/home/lib/runner/attach.mjs +3 -3
- package/src/home/lib/runner/continuation.mjs +174 -192
- package/src/home/lib/runner/launcher.mjs +9 -9
- package/src/home/lib/runner/order-options.mjs +30 -0
- package/src/home/lib/runner/order-owner.mjs +2 -1
- package/src/home/lib/runner/pass-gate.mjs +11 -9
- package/src/home/lib/runner/preflight.mjs +4 -3
- package/src/home/lib/runner/scope-check.mjs +11 -10
- package/src/home/lib/runner/task-input.mjs +23 -10
- package/src/home/lib/subagent-model-env.mjs +44 -0
- package/src/home/lib/task-header.mjs +158 -127
- package/src/home/lib/untyped-stop.mjs +30 -0
- package/src/home/lib/required-inputs.mjs +0 -301
- /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
|
@@ -1,111 +1,111 @@
|
|
|
1
|
-
/** Reports whether what this package installed into the host still matches the package. */
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
buildInstallPlan,
|
|
6
|
-
contentFingerprint,
|
|
7
|
-
fileFingerprint,
|
|
8
|
-
} from './manifest.mjs';
|
|
9
|
-
import { plannedContent } from './copy.mjs';
|
|
10
|
-
import { RULE_LIST_NAMES, inspectPermissions } from './permissions.mjs';
|
|
11
|
-
import { readRulesRegistry } from './rules-owners.mjs';
|
|
12
|
-
import { parseFrontmatter } from '
|
|
13
|
-
import { check } from './doctor-format.mjs';
|
|
14
|
-
|
|
15
|
-
export async function exists(target) {
|
|
16
|
-
try {
|
|
17
|
-
await fs.access(target);
|
|
18
|
-
return true;
|
|
19
|
-
} catch {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export async function isFile(target) {
|
|
25
|
-
try {
|
|
26
|
-
return (await fs.stat(target)).isFile();
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function agentsCheck(host, record) {
|
|
33
|
-
if (!record) return check('agents', 'warn', 'Agent definitions were not checked; run codex-bridge install');
|
|
34
|
-
const plan = await buildInstallPlan(host);
|
|
35
|
-
const agents = plan.filter((item) => /[\\/]src[\\/]agents[\\/][^\\/]+\.md$/.test(item.source));
|
|
36
|
-
const mismatches = [];
|
|
37
|
-
const unreadable = [];
|
|
38
|
-
for (const item of agents) {
|
|
39
|
-
const expected = contentFingerprint(await plannedContent(item, host.brandRoot));
|
|
40
|
-
if (await fileFingerprint(item.target) !== expected) mismatches.push(path.basename(item.target));
|
|
41
|
-
const expectedName = path.basename(item.source, '.md');
|
|
42
|
-
try {
|
|
43
|
-
const frontmatter = parseFrontmatter(await fs.readFile(item.target, 'utf8'));
|
|
44
|
-
if (!frontmatter) throw new Error('frontmatter is missing');
|
|
45
|
-
if (frontmatter.name !== expectedName) {
|
|
46
|
-
throw new Error(`name is ${JSON.stringify(frontmatter.name)}, expected ${JSON.stringify(expectedName)}`);
|
|
47
|
-
}
|
|
48
|
-
} catch (err) {
|
|
49
|
-
unreadable.push(`${path.basename(item.target)} (${err.message})`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
// The 2026-08-10 registration failure left files present but unreadable by Claude Code. Drift was
|
|
53
|
-
// advisory until a 2026-08-16 checklist run planted the pre-Plan_41 invocation — a path instead of
|
|
54
|
-
// the package command — in an installed definition: doctor kept exit 0 while every delegation
|
|
55
|
-
// would stop on a permission prompt. A definition that is not this package's is not judged healthy.
|
|
56
|
-
if (unreadable.length) {
|
|
57
|
-
return check('agents', 'fail', `Installed agent definitions cannot be read: ${unreadable.join(', ')}; run codex-bridge update --force`);
|
|
58
|
-
}
|
|
59
|
-
return mismatches.length
|
|
60
|
-
? check('agents', 'fail', `Installed agent definitions differ from this package: ${mismatches.join(', ')}; run codex-bridge update --force`)
|
|
61
|
-
: check('agents', 'ok', `${agents.length} installed agent definition(s) match this package`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function conventionsCheck(host) {
|
|
65
|
-
const file = host.brandConventionsPath;
|
|
66
|
-
let content;
|
|
67
|
-
try {
|
|
68
|
-
content = await fs.readFile(file, 'utf8');
|
|
69
|
-
} catch (err) {
|
|
70
|
-
if (err.code === 'ENOENT') return check('conventions', 'ok', `${file} (not found; optional)`);
|
|
71
|
-
return check('conventions', 'fail', `cannot read ${file}: ${err.message}`);
|
|
72
|
-
}
|
|
73
|
-
return content.trim()
|
|
74
|
-
? check('conventions', 'ok', `${file} (found)`)
|
|
75
|
-
: check('conventions', 'warn', `${file} (found but empty)`);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export async function rulesCheck(host, record) {
|
|
79
|
-
if (!record) return check('rules', 'warn', 'cannot check before installation');
|
|
80
|
-
if (!record.rules) {
|
|
81
|
-
return check('rules', 'warn', 'rules were not installed by this installation; install or update will add them');
|
|
82
|
-
}
|
|
83
|
-
let registry;
|
|
84
|
-
try {
|
|
85
|
-
registry = await readRulesRegistry(host);
|
|
86
|
-
} catch (err) {
|
|
87
|
-
// Keep every diagnostic visible: package removal on a broken registry left the host without its watchdog.
|
|
88
|
-
return check('rules', 'fail', err.message);
|
|
89
|
-
}
|
|
90
|
-
const ownerNote = registry?.owners.length > 1 ? `; ${registry.owners.length} owners` : '';
|
|
91
|
-
const fingerprint = await fileFingerprint(record.rules.path);
|
|
92
|
-
if (!fingerprint) return check('rules', 'fail', `${record.rules.path}${ownerNote ? ` (${registry.owners.length} owners)` : ''}`);
|
|
93
|
-
return fingerprint === record.rules.fingerprint
|
|
94
|
-
? check('rules', 'ok', `${record.rules.path} (matches record${ownerNote})`)
|
|
95
|
-
: check('rules', 'warn', `${record.rules.path} (modified after installation${ownerNote})`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function permissionsCheck(host) {
|
|
99
|
-
try {
|
|
100
|
-
const status = await inspectPermissions(host.settingsPath);
|
|
101
|
-
// Permission rules are an optional operator action; Plan_22 keeps their absence a warning so
|
|
102
|
-
// doctor does not turn a healthy installation red merely because hardening was not requested.
|
|
103
|
-
// The ask count is part of the line because a full set shadowed by `ask` reads as working and
|
|
104
|
-
// is not: the live run of Plan_22-1 found this line saying `installed (24/24)` over it.
|
|
105
|
-
const shadow = status.askCount ? `, ${status.askCount} shadowed by ask` : '';
|
|
106
|
-
return check('permissions', status.state === 'installed' ? 'ok' : 'warn',
|
|
107
|
-
`${status.state} (${status.present}/${status.total} own strings in ${RULE_LIST_NAMES}${shadow})`);
|
|
108
|
-
} catch (err) {
|
|
109
|
-
return check('permissions', 'warn', `cannot inspect permission rules: ${err.message}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
1
|
+
/** Reports whether what this package installed into the host still matches the package. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
buildInstallPlan,
|
|
6
|
+
contentFingerprint,
|
|
7
|
+
fileFingerprint,
|
|
8
|
+
} from './manifest.mjs';
|
|
9
|
+
import { plannedContent } from './copy.mjs';
|
|
10
|
+
import { RULE_LIST_NAMES, inspectPermissions } from './permissions.mjs';
|
|
11
|
+
import { readRulesRegistry } from './rules-owners.mjs';
|
|
12
|
+
import { parseFrontmatter } from '../src/home/lib/frontmatter.mjs';
|
|
13
|
+
import { check } from './doctor-format.mjs';
|
|
14
|
+
|
|
15
|
+
export async function exists(target) {
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(target);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function isFile(target) {
|
|
25
|
+
try {
|
|
26
|
+
return (await fs.stat(target)).isFile();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function agentsCheck(host, record) {
|
|
33
|
+
if (!record) return check('agents', 'warn', 'Agent definitions were not checked; run codex-bridge install');
|
|
34
|
+
const plan = await buildInstallPlan(host);
|
|
35
|
+
const agents = plan.filter((item) => /[\\/]src[\\/]agents[\\/][^\\/]+\.md$/.test(item.source));
|
|
36
|
+
const mismatches = [];
|
|
37
|
+
const unreadable = [];
|
|
38
|
+
for (const item of agents) {
|
|
39
|
+
const expected = contentFingerprint(await plannedContent(item, host.brandRoot));
|
|
40
|
+
if (await fileFingerprint(item.target) !== expected) mismatches.push(path.basename(item.target));
|
|
41
|
+
const expectedName = path.basename(item.source, '.md');
|
|
42
|
+
try {
|
|
43
|
+
const frontmatter = parseFrontmatter(await fs.readFile(item.target, 'utf8'));
|
|
44
|
+
if (!frontmatter) throw new Error('frontmatter is missing');
|
|
45
|
+
if (frontmatter.name !== expectedName) {
|
|
46
|
+
throw new Error(`name is ${JSON.stringify(frontmatter.name)}, expected ${JSON.stringify(expectedName)}`);
|
|
47
|
+
}
|
|
48
|
+
} catch (err) {
|
|
49
|
+
unreadable.push(`${path.basename(item.target)} (${err.message})`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// The 2026-08-10 registration failure left files present but unreadable by Claude Code. Drift was
|
|
53
|
+
// advisory until a 2026-08-16 checklist run planted the pre-Plan_41 invocation — a path instead of
|
|
54
|
+
// the package command — in an installed definition: doctor kept exit 0 while every delegation
|
|
55
|
+
// would stop on a permission prompt. A definition that is not this package's is not judged healthy.
|
|
56
|
+
if (unreadable.length) {
|
|
57
|
+
return check('agents', 'fail', `Installed agent definitions cannot be read: ${unreadable.join(', ')}; run codex-bridge update --force`);
|
|
58
|
+
}
|
|
59
|
+
return mismatches.length
|
|
60
|
+
? check('agents', 'fail', `Installed agent definitions differ from this package: ${mismatches.join(', ')}; run codex-bridge update --force`)
|
|
61
|
+
: check('agents', 'ok', `${agents.length} installed agent definition(s) match this package`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function conventionsCheck(host) {
|
|
65
|
+
const file = host.brandConventionsPath;
|
|
66
|
+
let content;
|
|
67
|
+
try {
|
|
68
|
+
content = await fs.readFile(file, 'utf8');
|
|
69
|
+
} catch (err) {
|
|
70
|
+
if (err.code === 'ENOENT') return check('conventions', 'ok', `${file} (not found; optional)`);
|
|
71
|
+
return check('conventions', 'fail', `cannot read ${file}: ${err.message}`);
|
|
72
|
+
}
|
|
73
|
+
return content.trim()
|
|
74
|
+
? check('conventions', 'ok', `${file} (found)`)
|
|
75
|
+
: check('conventions', 'warn', `${file} (found but empty)`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function rulesCheck(host, record) {
|
|
79
|
+
if (!record) return check('rules', 'warn', 'cannot check before installation');
|
|
80
|
+
if (!record.rules) {
|
|
81
|
+
return check('rules', 'warn', 'rules were not installed by this installation; install or update will add them');
|
|
82
|
+
}
|
|
83
|
+
let registry;
|
|
84
|
+
try {
|
|
85
|
+
registry = await readRulesRegistry(host);
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// Keep every diagnostic visible: package removal on a broken registry left the host without its watchdog.
|
|
88
|
+
return check('rules', 'fail', err.message);
|
|
89
|
+
}
|
|
90
|
+
const ownerNote = registry?.owners.length > 1 ? `; ${registry.owners.length} owners` : '';
|
|
91
|
+
const fingerprint = await fileFingerprint(record.rules.path);
|
|
92
|
+
if (!fingerprint) return check('rules', 'fail', `${record.rules.path}${ownerNote ? ` (${registry.owners.length} owners)` : ''}`);
|
|
93
|
+
return fingerprint === record.rules.fingerprint
|
|
94
|
+
? check('rules', 'ok', `${record.rules.path} (matches record${ownerNote})`)
|
|
95
|
+
: check('rules', 'warn', `${record.rules.path} (modified after installation${ownerNote})`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function permissionsCheck(host) {
|
|
99
|
+
try {
|
|
100
|
+
const status = await inspectPermissions(host.settingsPath);
|
|
101
|
+
// Permission rules are an optional operator action; Plan_22 keeps their absence a warning so
|
|
102
|
+
// doctor does not turn a healthy installation red merely because hardening was not requested.
|
|
103
|
+
// The ask count is part of the line because a full set shadowed by `ask` reads as working and
|
|
104
|
+
// is not: the live run of Plan_22-1 found this line saying `installed (24/24)` over it.
|
|
105
|
+
const shadow = status.askCount ? `, ${status.askCount} shadowed by ask` : '';
|
|
106
|
+
return check('permissions', status.state === 'installed' ? 'ok' : 'warn',
|
|
107
|
+
`${status.state} (${status.present}/${status.total} own strings in ${RULE_LIST_NAMES}${shadow})`);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
return check('permissions', 'warn', `cannot inspect permission rules: ${err.message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
package/cli/doctor.mjs
CHANGED
|
@@ -20,12 +20,15 @@ import {
|
|
|
20
20
|
import { hookChecks } from './doctor-hooks.mjs';
|
|
21
21
|
import { contractStatus, readHostContract } from './host-contract.mjs';
|
|
22
22
|
import { handbackWitnessStatus } from './handback-witness-check.mjs';
|
|
23
|
+
import { dispatcherModelChecks } from './dispatcher-model-check.mjs';
|
|
23
24
|
import { dispatcherContractStatus } from './dispatcher-contract.mjs';
|
|
24
25
|
import { DISPATCHER_CONTRACT_FILE, readDispatcherContract } from './dispatcher-contract-record.mjs';
|
|
25
26
|
import { PROBE_COMMAND } from './host-contract.mjs';
|
|
26
27
|
import { readHandbackWitness } from '../src/home/lib/handback-witness.mjs';
|
|
28
|
+
import { readDispatcherModel } from '../src/home/lib/dispatcher-model.mjs';
|
|
27
29
|
import { brandStateDir } from '../src/home/lib/brand-home.mjs';
|
|
28
30
|
import { readHostObservations } from '../src/home/lib/host-observations.mjs';
|
|
31
|
+
import { activeHostVersions } from './active-hosts.mjs';
|
|
29
32
|
import { otherHostCheck, sessionHostCheck, sessionHostVersions } from './session-hosts.mjs';
|
|
30
33
|
import { liveRunsCheck, projectRunsCheck, retentionCheck } from './doctor-runs.mjs';
|
|
31
34
|
|
|
@@ -83,8 +86,11 @@ export async function diagnose({
|
|
|
83
86
|
currentPackage,
|
|
84
87
|
contractRecord,
|
|
85
88
|
handbackWitnessRecord,
|
|
89
|
+
dispatcherModelRecord,
|
|
86
90
|
dispatcherContractRecord,
|
|
87
91
|
hostVersion,
|
|
92
|
+
activeHosts: injectedActiveHosts,
|
|
93
|
+
now,
|
|
88
94
|
observations,
|
|
89
95
|
} = {}) {
|
|
90
96
|
const checks = [sourceCheck()];
|
|
@@ -147,6 +153,10 @@ export async function diagnose({
|
|
|
147
153
|
const hostObservations = observations === undefined ? readHostObservations({ stateDir }) : observations;
|
|
148
154
|
const observedVersions = sessionHostVersions(hostObservations);
|
|
149
155
|
const detectedHostVersion = hostVersion === undefined ? observedVersions[0] ?? null : hostVersion;
|
|
156
|
+
const witnessRecord = handbackWitnessRecord === undefined ? readHandbackWitness({ stateDir }) : handbackWitnessRecord;
|
|
157
|
+
const modelRecord = dispatcherModelRecord === undefined ? readDispatcherModel({ stateDir }) : dispatcherModelRecord;
|
|
158
|
+
const activeHosts = injectedActiveHosts ?? (hostVersion !== undefined ? [hostVersion].filter(Boolean)
|
|
159
|
+
: activeHostVersions({ observations: hostObservations, witnessRecord, modelRecord, now }));
|
|
150
160
|
checks.push(sessionHostCheck(observedVersions, hostObservations));
|
|
151
161
|
const refusalRecord = contractRecord === undefined ? await readHostContract(host) : contractRecord;
|
|
152
162
|
const hostContract = contractStatus({
|
|
@@ -158,11 +168,16 @@ export async function diagnose({
|
|
|
158
168
|
: hostContract.state === 'ignored' ? 'fail' : 'warn';
|
|
159
169
|
checks.push(check('hostContract', hostContractStatus, hostContract.message));
|
|
160
170
|
const witness = handbackWitnessStatus({
|
|
161
|
-
record:
|
|
162
|
-
|
|
171
|
+
record: witnessRecord,
|
|
172
|
+
activeHosts,
|
|
163
173
|
stateDir,
|
|
164
174
|
});
|
|
165
|
-
checks.push(check('handbackWitness', ['
|
|
175
|
+
checks.push(check('handbackWitness', ['violation', 'unreadable'].includes(witness.state) ? 'warn' : 'ok', witness.message));
|
|
176
|
+
checks.push(...dispatcherModelChecks({
|
|
177
|
+
record: modelRecord,
|
|
178
|
+
activeHosts,
|
|
179
|
+
stateDir,
|
|
180
|
+
}));
|
|
166
181
|
const dispatcherRecord = dispatcherContractRecord === undefined ? readDispatcherContract({ stateDir }) : dispatcherContractRecord;
|
|
167
182
|
checks.push(...dispatcherContractChecks({
|
|
168
183
|
record: dispatcherRecord,
|
|
@@ -190,7 +205,7 @@ export async function diagnose({
|
|
|
190
205
|
exitCode: !record || recordBroken || missingFiles.length || agents.status === 'fail' || rules.status === 'fail'
|
|
191
206
|
|| hostContractStatus === 'fail' || retention.status === 'fail' || conventions.status === 'fail'
|
|
192
207
|
|| projectRuns.status === 'fail'
|
|
193
|
-
|| checks.some((item) => /^(hook|dispatcherContract):/.test(item.key) && item.status === 'fail')
|
|
208
|
+
|| checks.some((item) => /^(hook|dispatcherContract|dispatcherModel):/.test(item.key) && item.status === 'fail')
|
|
194
209
|
|| checks.some((item) => item.key.startsWith('otherHost:') && item.status === 'fail') ? 1 : 0,
|
|
195
210
|
checks,
|
|
196
211
|
record,
|
|
@@ -1,31 +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
|
+
import { entryState } from '../src/home/lib/observation-ledger.mjs';
|
|
4
5
|
|
|
5
|
-
export function handbackWitnessStatus({ record,
|
|
6
|
+
export function handbackWitnessStatus({ record, activeHosts, stateDir }) {
|
|
6
7
|
if (record?.corrupt) {
|
|
7
8
|
return { state: 'unreadable', message: `The handback witness is unreadable; delete ${path.join(stateDir, WITNESS_FILE)} to reset it.` };
|
|
8
9
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (record.lastSeen[hostVersion]) {
|
|
21
|
-
return { state: 'seen', message: `Handback contract last seen on host ${hostVersion} at ${record.lastSeen[hostVersion]}.` };
|
|
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.`;
|
|
22
20
|
}
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
const violations = entries.filter((entry) => entryState(entry) === 'violation'
|
|
22
|
+
&& activeHosts.includes(entry.lastObservation.data.hostVersion));
|
|
23
|
+
if (violations.length > 0) {
|
|
24
|
+
const details = violations.map((entry) => {
|
|
25
|
+
const { data, detail, at } = entry.lastViolation;
|
|
26
|
+
return `host ${data.hostVersion}: ${data.cause}${data.agentType ? ` / ${data.agentType}` : ''}: ${detail} at ${at}`;
|
|
27
|
+
});
|
|
25
28
|
return {
|
|
26
|
-
state: '
|
|
27
|
-
message: `
|
|
29
|
+
state: 'violation',
|
|
30
|
+
message: `Dispatcher violation(s): ${details.join('; ')}; do not trust a dispatcher answer from that time; see Plan_62 D15.${history}`,
|
|
28
31
|
};
|
|
29
32
|
}
|
|
30
|
-
|
|
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 };
|
|
31
41
|
}
|
package/cli/hosts.mjs
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
5
|
+
import { resolveBrandHome } from '../src/home/lib/brand-home.mjs';
|
|
6
|
+
import { claudePaths } from '../src/home/lib/claude-layout.mjs';
|
|
6
7
|
|
|
7
8
|
function repositoryRoot(start) {
|
|
8
9
|
let current = path.resolve(start);
|
|
@@ -14,15 +15,6 @@ function repositoryRoot(start) {
|
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
function claudePaths(root) {
|
|
18
|
-
return {
|
|
19
|
-
agentsDir: path.join(root, 'agents', 'codex-bridge'),
|
|
20
|
-
commandsDir: path.join(root, 'commands', 'codex-bridge'),
|
|
21
|
-
legacyAgentsDir: path.join(root, 'agents', 'codex'),
|
|
22
|
-
legacyCommandsDir: path.join(root, 'commands', 'codex'),
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
18
|
export function resolveHost({
|
|
27
19
|
scope = 'user',
|
|
28
20
|
host,
|
package/cli/install-owners.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { createHash } from 'node:crypto';
|
|
13
13
|
import { normalizeRepoPath } from '../src/home/lib/runner/project-dir.mjs';
|
|
14
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
14
15
|
import {
|
|
15
16
|
fileEntry,
|
|
16
17
|
INSTALL_METHOD_COPY,
|
|
@@ -20,10 +21,6 @@ import {
|
|
|
20
21
|
|
|
21
22
|
const isObject = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
22
23
|
|
|
23
|
-
export function isFormat2(parsed) {
|
|
24
|
-
return isObject(parsed) && parsed.format === 2;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
24
|
export function imageFingerprint(image) {
|
|
28
25
|
const brand = image.fingerprints?.brand;
|
|
29
26
|
if (brand === undefined) return null;
|
package/cli/install-record.mjs
CHANGED
|
@@ -4,7 +4,8 @@ import { randomUUID } from 'node:crypto';
|
|
|
4
4
|
import { createHomeWriter } from '../src/home/lib/home-write.mjs';
|
|
5
5
|
import { readJsonFile } from '../src/home/lib/json-file.mjs';
|
|
6
6
|
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
7
|
-
import { isFormat2
|
|
7
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
8
|
+
import { ownerView, validateFormat2, withOwner, withoutOwner } from './install-owners.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* The record's own spelling of the field saying how the package was installed, and its one value.
|
|
@@ -306,6 +307,11 @@ async function publishInstallRecord(host, next) {
|
|
|
306
307
|
await writer.rename('install-record', temporary, target);
|
|
307
308
|
}
|
|
308
309
|
|
|
310
|
+
/** Plan_67 D11 / A4: changing inventory must not rebuild owners or hide fingerprint lag. */
|
|
311
|
+
export function publishInstallRecordOnly(host, next) {
|
|
312
|
+
return publishInstallRecord(host, next);
|
|
313
|
+
}
|
|
314
|
+
|
|
309
315
|
/**
|
|
310
316
|
* Publishes the record whole through a temporary and a rename: a direct write left a half-written
|
|
311
317
|
* 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,9 +3,10 @@
|
|
|
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
|
-
import { isFormat2 } from '
|
|
9
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
9
10
|
import { readInstallRecordFile } from './install-record.mjs';
|
|
10
11
|
import { normalizedRulesOwner, readRulesRegistry } from './rules-owners.mjs';
|
|
11
12
|
|
|
@@ -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
|
@@ -4,14 +4,13 @@ import path from 'node:path';
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
7
|
-
import {
|
|
8
|
-
renderRequiredInputSummary,
|
|
9
|
-
renderRequiredInputs,
|
|
10
|
-
} from '../src/home/lib/required-inputs.mjs';
|
|
7
|
+
import { renderRequiredInputSummary } from '../src/home/lib/dispatcher-call.mjs';
|
|
11
8
|
import { renderNoSelfExecution } from '../src/home/lib/no-self-execution.mjs';
|
|
9
|
+
import { renderDispatcherProtocol } from '../src/home/lib/dispatcher-protocol.mjs';
|
|
10
|
+
import { ORDER_AGENTS } from '../src/home/lib/order-schema.mjs';
|
|
12
11
|
import { renderStopSummary } from '../src/home/lib/stop-contract.mjs';
|
|
13
12
|
import { readJsonFile } from '../src/home/lib/json-file.mjs';
|
|
14
|
-
import { normalizeFrontmatter } from '
|
|
13
|
+
import { normalizeFrontmatter } from '../src/home/lib/frontmatter.mjs';
|
|
15
14
|
import {
|
|
16
15
|
INSTALL_RECORD_NAME,
|
|
17
16
|
RULES_NAME,
|
|
@@ -77,7 +76,8 @@ export function replacePlaceholders(content, installationRoot) {
|
|
|
77
76
|
const replaced = content
|
|
78
77
|
.replaceAll('{{CODEX_BRIDGE_DIR}}', posix(path.resolve(installationRoot)))
|
|
79
78
|
.replaceAll('{{CODEX_REQUIRED_INPUTS_SUMMARY}}', renderRequiredInputSummary(agentType))
|
|
80
|
-
.replaceAll('{{
|
|
79
|
+
.replaceAll('{{CODEX_DISPATCHER_PROTOCOL}}', ORDER_AGENTS.includes(agentType)
|
|
80
|
+
? renderDispatcherProtocol(agentType) : '{{CODEX_DISPATCHER_PROTOCOL}}')
|
|
81
81
|
.replaceAll('{{CODEX_NO_SELF_EXECUTION}}', renderNoSelfExecution())
|
|
82
82
|
.replaceAll('{{CODEX_STOP_SUMMARY}}', renderStopSummary());
|
|
83
83
|
return normalizeFrontmatter(replaced);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyupro/codex-bridge",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Claude Code-to-Codex delegation with 7 guards,
|
|
3
|
+
"version": "0.6.12",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 7 guards, 3267 tests, zero runtime dependencies, and zero build steps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|