@lyupro/codex-bridge 0.6.10 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +15 -4
- package/cli/dispatcher-model-check.mjs +59 -0
- package/cli/doctor-host-side.mjs +2 -1
- package/cli/doctor-installation.mjs +111 -111
- package/cli/doctor.mjs +13 -5
- package/cli/handback-witness-check.mjs +24 -18
- package/cli/hosts.mjs +2 -10
- package/cli/install-owners.mjs +1 -4
- package/cli/install-record.mjs +2 -1
- package/cli/inventory-transition.mjs +1 -1
- package/cli/manifest.mjs +6 -6
- package/package.json +2 -2
- package/src/agents/codex-advisor.md +25 -119
- package/src/agents/codex-build.md +15 -133
- package/src/agents/codex-review.md +10 -109
- package/src/agents/codex-scout.md +11 -130
- package/src/home/hooks/dispatcher-gate.mjs +14 -9
- package/src/home/hooks/live-runs.mjs +0 -41
- package/src/home/hooks/order-gate.mjs +59 -40
- package/src/home/hooks/reply-guard.mjs +52 -65
- package/src/home/hooks/reply-identity.mjs +64 -0
- package/src/home/hooks/reply-verdicts.mjs +31 -27
- package/src/home/hooks/reply-witness.mjs +48 -0
- package/src/home/lib/claude-layout.mjs +14 -0
- package/src/home/lib/dispatcher-call.mjs +171 -0
- package/src/home/lib/dispatcher-command.mjs +18 -56
- package/src/home/lib/dispatcher-gate.mjs +4 -0
- package/src/home/lib/dispatcher-model.mjs +112 -0
- package/src/home/lib/dispatcher-order.mjs +21 -22
- package/src/home/lib/dispatcher-pin.mjs +60 -0
- package/src/home/lib/dispatcher-protocol.mjs +91 -0
- package/src/home/lib/dispatcher-stop.mjs +11 -2
- package/src/home/lib/handback-witness.mjs +137 -48
- package/src/home/lib/home-registry.mjs +7 -0
- package/src/home/lib/install-owner-roots.mjs +53 -0
- package/src/home/lib/meta/paths.mjs +1 -1
- package/src/home/lib/meta/reply.mjs +2 -1
- package/src/home/lib/model-family.mjs +38 -0
- package/src/home/lib/observation-ledger.mjs +145 -0
- package/src/home/lib/order-schema.mjs +204 -0
- package/src/home/lib/order-values.mjs +46 -0
- package/src/home/lib/runner/args.mjs +140 -231
- package/src/home/lib/runner/attach.mjs +3 -3
- package/src/home/lib/runner/continuation.mjs +174 -192
- package/src/home/lib/runner/launcher.mjs +9 -9
- package/src/home/lib/runner/order-options.mjs +30 -0
- package/src/home/lib/runner/order-owner.mjs +2 -1
- package/src/home/lib/runner/pass-gate.mjs +11 -9
- package/src/home/lib/runner/preflight.mjs +4 -3
- package/src/home/lib/runner/scope-check.mjs +11 -10
- package/src/home/lib/runner/task-input.mjs +23 -10
- package/src/home/lib/subagent-model-env.mjs +44 -0
- package/src/home/lib/task-header.mjs +158 -127
- package/src/home/lib/untyped-stop.mjs +30 -0
- package/src/home/lib/required-inputs.mjs +0 -301
- /package/{cli → src/home/lib}/frontmatter.mjs +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,87 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.6.11] - 2026-10-07
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Breaking for orchestrator prompts:** the order gate refuses an explicit `model` on a dispatcher
|
|
12
|
+
call, even one matching the installed pin or `inherit`. Migration cost: drop `model` from dispatcher
|
|
13
|
+
calls in orchestrator prompts; the installed agent definition owns that setting. It also refuses a
|
|
14
|
+
foreign-family `CLAUDE_CODE_SUBAGENT_MODEL` on hosts where the environment overrides the pin
|
|
15
|
+
(before 2.1.251, or with `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` from 2.1.257). Unknown host versions or
|
|
16
|
+
unparseable families are left to the stop observation. These checks precede prompt and grant exits,
|
|
17
|
+
so a continuation cannot bypass them.
|
|
18
|
+
|
|
19
|
+
- **Breaking for orchestrator prompts:** a dispatcher call is exactly one line, `task file: <absolute
|
|
20
|
+
path>`. The order — `order id`, `scope`, `scope new`, `phase`, `continue`/`retry`, `slug`, `effort`,
|
|
21
|
+
`changeset`, `repository` — goes in the header at the top of that file, and the dispatcher runs
|
|
22
|
+
`codex-bridge run --agent <type> --task-file "<path>"`. Any former label in the call is refused for
|
|
23
|
+
free with "moved to the task-file header" and the agent's header template. Migration cost: move the
|
|
24
|
+
label lines from the call into the task file's first lines. A task-file path containing `$`, `\\` or a
|
|
25
|
+
final `\` is refused, because the shell would rewrite it inside the command's double quotes and the
|
|
26
|
+
runner would open a different file than the one the gate checked; write it with forward slashes.
|
|
27
|
+
|
|
28
|
+
- The four dispatcher prompts share one protocol section rendered at install from the same command
|
|
29
|
+
assembler the order gate uses: the call is `task file:` only and the command is
|
|
30
|
+
`codex-bridge run --agent <type> --task-file "<path>"`. The hand-written label-to-flag tables and the
|
|
31
|
+
instruction to add a continuation flag are gone; the header labels are listed in each agent's
|
|
32
|
+
description. Migration cost: none beyond `codex-bridge update`, which reinstalls the prompts. A
|
|
33
|
+
first call that prints no `RUN=` line (a refusal or `UNAVAILABLE`) is itself the dispatcher's answer.
|
|
34
|
+
|
|
35
|
+
- A dispatcher call contains only exact `label: value` lines from the agent's input registry, in any
|
|
36
|
+
order, with blank lines allowed. Prose, unknown or another agent's labels, decorated or hyphenated
|
|
37
|
+
spellings, `--flag value` lines, duplicates, empty values, missing required labels, placeholders,
|
|
38
|
+
relative task-file paths and shell-unsafe values are refused for free before the dispatcher starts.
|
|
39
|
+
The refusal names the line, the exact spelling and every label of the agent, and says "Free text
|
|
40
|
+
belongs in the task file." Of 405 measured past calls, 47 began with prose, 112 carried lines outside
|
|
41
|
+
the registry, 18 passed values as `--order-id x`, and 1 duplicated a label.
|
|
42
|
+
Migration cost: a call written in an older form gets one free refusal naming the fix.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- The reply guard observes `message.model` in the dispatcher's assistant transcript entries at
|
|
47
|
+
`SubagentStop`, before its handback exits, and compares model families against the installed
|
|
48
|
+
contract agreed by every recorded owner. On 2026-09-25 a hook's `(inherit)` label was mistaken for
|
|
49
|
+
the model used, and the VS Code panel showed the parent model while transcripts showed the pinned
|
|
50
|
+
family; neither display is model evidence. A mismatch raises an alarm in the same turn and enters
|
|
51
|
+
`state/dispatcher-model.json`.
|
|
52
|
+
The order gate warns on every later call of that dispatcher type until a later match on any host;
|
|
53
|
+
uncertainty does not clear the warning. It warns rather than refusing work for a leak whose cause
|
|
54
|
+
may be another hook or the host.
|
|
55
|
+
- `doctor` reports `dispatcherModel:<type>` rows for the current observed session host: the latest
|
|
56
|
+
violation is `fail` with exit 1, an undetermined observation is `warn`, and a match is `ok`, including
|
|
57
|
+
recovery after a violation. No observations is `ok`; recovered incidents and other hosts remain
|
|
58
|
+
history, without a permanent warning. An incomplete, unreadable or disagreeing installed contract
|
|
59
|
+
makes the comparison undetermined, rather than guessing which agent definition the host selected.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- The handback witness keeps per-cause observations and violation history without a permanent
|
|
64
|
+
warning. Only later evidence disproving that cause clears it, and only the current observed session
|
|
65
|
+
host contributes active warnings. Intercepted handbacks, including refused attempts, are not proof
|
|
66
|
+
of recovery. Existing witness files migrate on their next write: old untyped alarms become
|
|
67
|
+
`legacy-untyped-unverified` history, not confirmed violations; `doctor` reads them without rewriting
|
|
68
|
+
the file. This fixes the 2026-09-27 case where clean handbacks on a newer host still left an old alarm
|
|
69
|
+
as the only visible signal.
|
|
70
|
+
- A subagent stop without `agent_type` alarms only with dispatcher evidence bound to that stop:
|
|
71
|
+
healthy, active dispatcher-gate state or an assistant Bash call running the standalone runner command
|
|
72
|
+
for a registered role. The 13 older alarms across nine host versions lacked that evidence. Without
|
|
73
|
+
it, a stop is undetermined, neither an accusation nor recovery, and a command's `--agent` does not
|
|
74
|
+
invent the missing host type.
|
|
75
|
+
|
|
76
|
+
- The reply guard identifies a dispatcher's run by a receipt — the run folder the runner itself
|
|
77
|
+
printed to that dispatcher, recorded once by the dispatcher gate — instead of the order id in the
|
|
78
|
+
call or the newest run on disk. A reply naming another folder, a reply with a verdict but no
|
|
79
|
+
receipt, and two different folders reported to one dispatcher are blocked; the last one also turns
|
|
80
|
+
the handback into a `FAIL`.
|
|
81
|
+
- `changeset:` reaches the runner, so `base:<branch>` and `commit:<sha>` review the requested changes
|
|
82
|
+
instead of silently reviewing uncommitted work.
|
|
83
|
+
- `scope-new:` is refused with the exact `scope new:` spelling; it can no longer be dropped or read as
|
|
84
|
+
`scope`.
|
|
85
|
+
- A `Repository root:` line is refused before the haiku dispatcher starts. It used to make the
|
|
86
|
+
dispatcher prepend `cd … &&` and leave without delegating.
|
|
87
|
+
|
|
7
88
|
## [0.6.10] - 2026-10-04
|
|
8
89
|
|
|
9
90
|
### Added
|
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ If a global install and a clone coexist, you have two package copies. `codex-bri
|
|
|
62
62
|
- **Every run leaves an audit folder.** The verbatim task, scope, before/after git state, events, report, verdict, and reason remain together.
|
|
63
63
|
- **Zero runtime dependencies and zero build steps.** The package is plain `.mjs` on the Node.js standard library.
|
|
64
64
|
|
|
65
|
-
The current suite contains **
|
|
65
|
+
The current suite contains **3199 automated tests: 3198 passing and 1 skipped**.
|
|
66
66
|
|
|
67
67
|
## Verify
|
|
68
68
|
|
|
@@ -93,12 +93,23 @@ Output on a healthy host (trimmed):
|
|
|
93
93
|
|
|
94
94
|
`doctor` reports the selected host, package source, installed files and guards, Codex availability, retention policy, permissions, live runs, and the current repository's run folder. It judges contracts against the newest host version observed in a session transcript, not `claude` on `PATH`: on 2026-09-25 the VS Code extension ran sessions on 2.1.282 while `PATH` resolved to 2.1.281, and the extension's `CLAUDE_CODE_EXECPATH` and `CLAUDE_AGENT_SDK_VERSION` were inherited by every descendant `claude`. The transcript entry's `version` identifies the host. The dispatcher gate records it once per session in `state/host-observations.json` (sessions kept 7 days, hosts 30); `doctor` and `install` judge the newest observed host. `doctor` prints `sessionHost` and one `otherHost:<version>` line for each other observed version, with `ok`, `warn` and a probe command, or `fail` with exit 1. If no session has been observed, they say so instead of using `PATH`. Refusal and dispatcher contract records keep one verdict per host version, so a measurement of version V applies to every host of version V; older single-version records count only for their recorded version.
|
|
95
95
|
|
|
96
|
-
`doctor` also reports `handbackWitness` and the four dispatcher contract lines (`dispatcherContract:agentIdentity`, `dispatcherContract:shellStdout`, `dispatcherContract:shellFailure`, and `dispatcherContract:agentTranscript`): verified contracts are `ok`, unknown, unprobed, or stale contracts are `warn`, and a measured change is `fail` with exit 1.
|
|
96
|
+
`doctor` also reports `handbackWitness`, `dispatcherModel:<type>` rows, and the four dispatcher contract lines (`dispatcherContract:agentIdentity`, `dispatcherContract:shellStdout`, `dispatcherContract:shellFailure`, and `dispatcherContract:agentTranscript`): verified contracts are `ok`, unknown, unprobed, or stale contracts are `warn`, and a measured change is `fail` with exit 1. The witness warns only for unresolved causes on the current observed session host; later disproving evidence clears the warning while retaining history. Intercepted handbacks are not recovery evidence, and old untyped alarms without dispatcher evidence remain unverified history. A stop missing `agent_type` alarms only with dispatcher evidence for that stop. Dispatcher model rows compare transcript-observed families with the installed contract: a latest violation is `fail` with exit 1, uncertainty is `warn`, and a match, including recovery, is `ok`; no observations is also `ok`. Other hosts and recovered incidents are history, not permanent warnings. See [Response guard](docs/overview.md#response-guard) for the evidence rules. The `agents` line is a read, not a file count: each installed definition is parsed and its `name` checked against the dispatcher it claims to be, because a definition the host cannot read is a dispatcher that does not exist while every other line still says `[ok]`. Content that drifts from the packaged definition is a failure, not a warning: `doctor` exits 1 and names `codex-bridge update --force`, because a host running someone else's agent files may be calling the runner in a form no permission rule matches.
|
|
97
97
|
|
|
98
98
|
If a session answers `Agent type 'codex-build' not found`, a run can still be started without the dispatcher — the agent is a wrapper that types one command:
|
|
99
99
|
|
|
100
|
+
The task file starts with the order header, a blank line, and the task text:
|
|
101
|
+
|
|
102
|
+
```text
|
|
103
|
+
order id: order-42
|
|
104
|
+
repository: /abs/path/to/repository
|
|
105
|
+
scope: src/auth/**,tests/auth/**
|
|
106
|
+
advice: mechanical
|
|
107
|
+
|
|
108
|
+
Implement the authentication change and verify it with the tests named in the task.
|
|
109
|
+
```
|
|
110
|
+
|
|
100
111
|
```bash
|
|
101
|
-
codex-bridge run --agent codex-build --
|
|
112
|
+
codex-bridge run --agent codex-build --task-file "/abs/path/to/task.md"
|
|
102
113
|
```
|
|
103
114
|
|
|
104
115
|
The trade-off is explicit: `order-gate` and `reply-guard` sit on the dispatcher call, so a direct run is unguarded and its result is judged by the worktree and the suite.
|
|
@@ -131,7 +142,7 @@ Transport files from runs at least **30 days** old are pruned automatically when
|
|
|
131
142
|
| `permissions [add\|remove] [--scope user\|project] [--host <path>]` | Inspect or manage optional shell rules. |
|
|
132
143
|
| `uninstall [--scope user\|project] [--host <path>] [--dry-run] [--purge]` | Remove recorded package files while preserving run artifacts; `--purge` also deletes your data and the home after one preflight and two consents (see [Uninstall and purge](docs/overview.md#uninstall-and-purge)). |
|
|
133
144
|
| `doctor [--scope user\|project] [--host <path>] [--probe-contract] [--probe-executable <path>]` | Diagnose the selected host and Codex connection, including the handback witness and four dispatcher contract lines. `--probe-contract` targets the newest observed host, accepting `CLAUDE_CODE_EXECPATH` or `claude` on `PATH` only when each reports that version with `--version`; use `--probe-executable <path>` to name an executable (`--host` selects the Claude Code configuration). The probe prints its target and records nothing if the completed host run reports another transcript version. |
|
|
134
|
-
| `run <
|
|
145
|
+
| `run --agent <type> --task-file "<abs path>" [--no-wait]` | Start or attach to a delegated run through the permission-stable package command. The task-file header carries the whole order; its body carries the statement, scout questions and verification command. Its path must be absolute and use forward slashes; `$`, doubled backslashes and a final backslash are refused. Every other runner flag is a free refusal; stdin is not an input channel. |
|
|
135
146
|
| `model [list\|set\|unset\|speed]` | Show the model, effort, pinned speed tier and provenance of each role, with the config path; `model list` prints the live catalogue from Codex, hidden models included and marked; `model set <role> <model> [effort]` checks the pair and any retained speed tier against that model's catalogue entry, and `model unset <role>` removes the profile. `model speed <role> <tier>` previews a live accelerated tier and its quoted cost description; repeat with trailing `confirm` to pin it. `model speed <role> unset` removes only speed, without confirmation. The profile is machine-wide, shared by every project on this machine. |
|
|
136
147
|
| `projects [<name>] [--json]` | List projects or runs in the run store. |
|
|
137
148
|
| `prune <project> [<run>] [--purge] [--older-than <age>] [-f] [--json]`<br>`prune --all-projects [--older-than <age>] [-f] [--json]` | Plan or perform operator-confirmed cleanup, for one project or across the whole run store. |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** turns the dispatcher-model ledger into doctor rows; Plan_67 D4/D8 and D8 clarification 2026-10-07. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DISPATCHER_MODEL_FILE } from '../src/home/lib/dispatcher-model.mjs';
|
|
4
|
+
import { entryState } from '../src/home/lib/observation-ledger.mjs';
|
|
5
|
+
import { check } from './doctor-format.mjs';
|
|
6
|
+
|
|
7
|
+
function observationSummary({ data, at }) {
|
|
8
|
+
return `${data.agentType}: observed families ${data.parsed.join(', ')}, pinned family ${data.pinFamily} at ${at}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function entryCheck(entry) {
|
|
12
|
+
const observation = entry.lastObservation;
|
|
13
|
+
const { data, verdict, at } = observation;
|
|
14
|
+
const key = `dispatcherModel:${data.agentType}`;
|
|
15
|
+
const state = entryState(entry);
|
|
16
|
+
// D4: only the latest confirmed violation fails; D8 keeps uncertainty from erasing history.
|
|
17
|
+
if (verdict === 'violation') {
|
|
18
|
+
return check(key, 'fail', `${observationSummary(observation)}; Claude quota was spent on this dispatcher; see Plan_67.`);
|
|
19
|
+
}
|
|
20
|
+
if (verdict === 'undetermined') {
|
|
21
|
+
const reason = data.pinReasons?.length
|
|
22
|
+
? data.pinReasons.join('; ') : 'no model family could be read from the transcript';
|
|
23
|
+
const unresolved = state === 'violation'
|
|
24
|
+
? `; earlier violation (${observationSummary(entry.lastViolation)}) is not yet disproved` : '';
|
|
25
|
+
return check(key, 'warn', `${data.agentType}: model observation undetermined at ${at}: ${reason}${unresolved}.`);
|
|
26
|
+
}
|
|
27
|
+
const history = state === 'recovered'
|
|
28
|
+
? `; recovered from earlier violation (${observationSummary(entry.lastViolation)})` : '';
|
|
29
|
+
return check(key, 'ok', `${observationSummary(observation)}; matches the installed contract${history}.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function dispatcherModelChecks({ record, hostVersion, stateDir }) {
|
|
33
|
+
if (record?.corrupt) {
|
|
34
|
+
return [check('dispatcherModel', 'warn',
|
|
35
|
+
`The dispatcher model record is unreadable; delete ${path.join(stateDir, DISPATCHER_MODEL_FILE)} to reset it.`)];
|
|
36
|
+
}
|
|
37
|
+
const entries = Object.values(record?.entries || {});
|
|
38
|
+
const current = entries.filter((entry) => entry.lastObservation.data.hostVersion === hostVersion);
|
|
39
|
+
const rows = current.map(entryCheck);
|
|
40
|
+
if (!rows.length) {
|
|
41
|
+
rows.push(check('dispatcherModel', 'ok', 'Not observed yet — recorded when a dispatcher stops on this host.'));
|
|
42
|
+
}
|
|
43
|
+
const hosts = new Map();
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
const version = entry.lastObservation.data.hostVersion;
|
|
46
|
+
if (version === hostVersion) continue;
|
|
47
|
+
const counts = hosts.get(version) || { entries: 0, violations: 0 };
|
|
48
|
+
counts.entries += 1;
|
|
49
|
+
if (entryState(entry) === 'violation') counts.violations += 1;
|
|
50
|
+
hosts.set(version, counts);
|
|
51
|
+
}
|
|
52
|
+
// D8: other hosts are one history sentence, never additional active warning rows.
|
|
53
|
+
if (hosts.size) {
|
|
54
|
+
const history = [...hosts].map(([version, counts]) =>
|
|
55
|
+
`${version ?? 'unknown host'}: ${counts.entries} entries, ${counts.violations} unresolved violations`);
|
|
56
|
+
rows[0].value += ` History on other hosts (historical, not current): ${history.join('; ')}.`;
|
|
57
|
+
}
|
|
58
|
+
return rows;
|
|
59
|
+
}
|
package/cli/doctor-host-side.mjs
CHANGED
|
@@ -10,7 +10,8 @@ import path from 'node:path';
|
|
|
10
10
|
import { normalizeRepoPath } from '../src/home/lib/runner/project-dir.mjs';
|
|
11
11
|
import { check } from './doctor-format.mjs';
|
|
12
12
|
import { hasPackageMarks } from './host-inspection.mjs';
|
|
13
|
-
import {
|
|
13
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
14
|
+
import { imageFingerprint } from './install-owners.mjs';
|
|
14
15
|
|
|
15
16
|
// Format 1 is not migrated here: asFormat2 would record this host as the owner, and doctor only reads.
|
|
16
17
|
export function ownerEntry(rawRecord, host) {
|
|
@@ -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
|
@@ -19,11 +19,13 @@ import {
|
|
|
19
19
|
} from './doctor-installation.mjs';
|
|
20
20
|
import { hookChecks } from './doctor-hooks.mjs';
|
|
21
21
|
import { contractStatus, readHostContract } from './host-contract.mjs';
|
|
22
|
-
import { handbackWitnessStatus } from './handback-witness-check.mjs';
|
|
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
|
-
import { readHandbackWitness } from '../src/home/lib/handback-witness.mjs';
|
|
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';
|
|
29
31
|
import { otherHostCheck, sessionHostCheck, sessionHostVersions } from './session-hosts.mjs';
|
|
@@ -82,7 +84,8 @@ export async function diagnose({
|
|
|
82
84
|
launcherProbe,
|
|
83
85
|
currentPackage,
|
|
84
86
|
contractRecord,
|
|
85
|
-
handbackWitnessRecord,
|
|
87
|
+
handbackWitnessRecord,
|
|
88
|
+
dispatcherModelRecord,
|
|
86
89
|
dispatcherContractRecord,
|
|
87
90
|
hostVersion,
|
|
88
91
|
observations,
|
|
@@ -162,7 +165,12 @@ export async function diagnose({
|
|
|
162
165
|
hostVersion: detectedHostVersion,
|
|
163
166
|
stateDir,
|
|
164
167
|
});
|
|
165
|
-
checks.push(check('handbackWitness', ['
|
|
168
|
+
checks.push(check('handbackWitness', ['violation', 'unreadable'].includes(witness.state) ? 'warn' : 'ok', witness.message));
|
|
169
|
+
checks.push(...dispatcherModelChecks({
|
|
170
|
+
record: dispatcherModelRecord === undefined ? readDispatcherModel({ stateDir }) : dispatcherModelRecord,
|
|
171
|
+
hostVersion: detectedHostVersion,
|
|
172
|
+
stateDir,
|
|
173
|
+
}));
|
|
166
174
|
const dispatcherRecord = dispatcherContractRecord === undefined ? readDispatcherContract({ stateDir }) : dispatcherContractRecord;
|
|
167
175
|
checks.push(...dispatcherContractChecks({
|
|
168
176
|
record: dispatcherRecord,
|
|
@@ -190,7 +198,7 @@ export async function diagnose({
|
|
|
190
198
|
exitCode: !record || recordBroken || missingFiles.length || agents.status === 'fail' || rules.status === 'fail'
|
|
191
199
|
|| hostContractStatus === 'fail' || retention.status === 'fail' || conventions.status === 'fail'
|
|
192
200
|
|| projectRuns.status === 'fail'
|
|
193
|
-
|| checks.some((item) => /^(hook|dispatcherContract):/.test(item.key) && item.status === 'fail')
|
|
201
|
+
|| checks.some((item) => /^(hook|dispatcherContract|dispatcherModel):/.test(item.key) && item.status === 'fail')
|
|
194
202
|
|| checks.some((item) => item.key.startsWith('otherHost:') && item.status === 'fail') ? 1 : 0,
|
|
195
203
|
checks,
|
|
196
204
|
record,
|
|
@@ -1,31 +1,37 @@
|
|
|
1
1
|
/** Surfaces handback drift; 2026-09-25 established exact host transcript versions as identity. */
|
|
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
6
|
export function handbackWitnessStatus({ record, hostVersion, 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
|
-
|
|
10
|
+
const entries = Object.values(record?.ledger?.entries || {});
|
|
11
|
+
const violations = entries.filter((entry) => entryState(entry) === 'violation'
|
|
12
|
+
&& entry.lastObservation.data.hostVersion === hostVersion);
|
|
13
|
+
if (violations.length > 0) {
|
|
14
|
+
const details = violations.map((entry) => {
|
|
15
|
+
const { data, detail, at } = entry.lastViolation;
|
|
16
|
+
return `${data.cause}${data.agentType ? ` / ${data.agentType}` : ''}: ${detail} at ${at}`;
|
|
17
|
+
});
|
|
11
18
|
return {
|
|
12
|
-
state: '
|
|
13
|
-
message:
|
|
19
|
+
state: 'violation',
|
|
20
|
+
message: `Dispatcher violation(s) on host ${hostVersion ?? 'unknown host'}: ${details.join('; ')}; do not trust a dispatcher answer from that time; see Plan_62 D15.`,
|
|
14
21
|
};
|
|
15
22
|
}
|
|
16
|
-
|
|
17
|
-
|
|
23
|
+
const intercepted = record?.intercepted?.[hostVersion || 'unknown'];
|
|
24
|
+
let message = intercepted
|
|
25
|
+
? `Newest intercepted handback on host ${hostVersion ?? 'unknown host'} at ${intercepted}.`
|
|
26
|
+
: 'Not observed yet — a handback is seen only when a dispatcher runs in an interactive session.';
|
|
27
|
+
const recovered = entries.filter((entry) => entryState(entry) === 'recovered').length;
|
|
28
|
+
const historical = entries.filter((entry) => entryState(entry) === 'violation'
|
|
29
|
+
&& entry.lastObservation.data.hostVersion !== hostVersion).length;
|
|
30
|
+
const legacy = record?.legacy || [];
|
|
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.`;
|
|
18
35
|
}
|
|
19
|
-
|
|
20
|
-
if (record.lastSeen[hostVersion]) {
|
|
21
|
-
return { state: 'seen', message: `Handback contract last seen on host ${hostVersion} at ${record.lastSeen[hostVersion]}.` };
|
|
22
|
-
}
|
|
23
|
-
const [newest, iso] = Object.entries(record.lastSeen).sort((left, right) => right[1].localeCompare(left[1]))[0];
|
|
24
|
-
if (newest) {
|
|
25
|
-
return {
|
|
26
|
-
state: 'stale',
|
|
27
|
-
message: `Handback contract last seen on host ${newest} at ${iso}, not yet on host ${hostVersion}; it is recorded the next time a dispatcher runs in an interactive session on that host.`,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
return { state: 'unobserved', message: 'Not observed yet — a handback is seen only when a dispatcher runs in an interactive session.' };
|
|
36
|
+
return { state: 'ok', message };
|
|
31
37
|
}
|
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.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import fs from 'node:fs/promises';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { askYesNo, isInteractive } from './terminal-question.mjs';
|
|
8
|
-
import { isFormat2 } from '
|
|
8
|
+
import { isFormat2 } from '../src/home/lib/install-owner-roots.mjs';
|
|
9
9
|
import { readInstallRecordFile } from './install-record.mjs';
|
|
10
10
|
import { normalizedRulesOwner, readRulesRegistry } from './rules-owners.mjs';
|
|
11
11
|
|