@lyupro/codex-bridge 0.6.12 → 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 +105 -0
- package/README.md +10 -8
- package/cli/command-registry.mjs +59 -18
- 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 +26 -8
- package/cli/guidance.mjs +42 -0
- package/cli/host-inspection.mjs +1 -0
- package/cli/hosts.mjs +4 -3
- package/cli/install-record.mjs +4 -2
- 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/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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,111 @@ 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.14] - 2026-10-11
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Install and update place an always-loaded delegation core in the Claude Code host's
|
|
12
|
+
`rules/codex-bridge/core.md`: under `~/.claude/` for user scope or `<project>/.claude/` for
|
|
13
|
+
project scope. It has no `paths` frontmatter and stays within 2,400 characters, so the short
|
|
14
|
+
contract reaches every session without loading all the detailed guidance. This is separate
|
|
15
|
+
from the Codex execpolicy file. Uninstall removes package or recorded core bytes and the empty
|
|
16
|
+
package subfolder; edited or unknown files and links are kept and named, and the host's own
|
|
17
|
+
`rules/` folder stays.
|
|
18
|
+
- The `doctor` row `claudeRules` checks the installed core against the package: missing or
|
|
19
|
+
differing bytes are `fail`, with `codex-bridge update --force` as the remedy; before installation
|
|
20
|
+
it is `warn`. A deleted or edited core previously left the installation looking healthy.
|
|
21
|
+
- An observing `InstructionsLoaded` hook records session-start core paths and hashes in
|
|
22
|
+
`<home>/state/load-witness.json` without blocking host actions. The `doctor` rows
|
|
23
|
+
`claudeRulesLoad:<host version>` require the latest load at this owner's exact core path to
|
|
24
|
+
match the package hash, using the version from that same session's transcript. Missing, stale,
|
|
25
|
+
unreadable or oversized evidence is `warn`, never proof of a load failure; open a new Claude
|
|
26
|
+
Code session and rerun `doctor`. A newer differing or oversized load cannot be cleared by an
|
|
27
|
+
older match, and core reads are bounded to 7,200 bytes plus one overflow byte.
|
|
28
|
+
- Each run row in `codex-bridge projects <project> --json` includes `path`, the absolute run
|
|
29
|
+
folder, so orchestrators can find earlier artifacts and write an `advice:` address without
|
|
30
|
+
deriving the run store from the package's installation directory.
|
|
31
|
+
- Maintainers prove before a release, with
|
|
32
|
+
`npm run release-check:rules-load-claude -- --model <model>`, that both `claude` on `PATH` and
|
|
33
|
+
the VS Code extension's binary load a real project install's core. The script lives in the
|
|
34
|
+
repository and is not part of the npm package. Each target gets an isolated throwaway project; its exact core path, frozen hash and session host
|
|
35
|
+
version must match, and the check guards the operator's installation and cleans up its trees.
|
|
36
|
+
Attempts are saved to `docs/audits/release-checks/rules-load-claude.json` by default. Exit 0
|
|
37
|
+
means both targets verified, 1 means a failure, and 2 means inconclusive without a failure;
|
|
38
|
+
missing evidence blocks the release, and an oversized core fails it. The check does not start
|
|
39
|
+
Codex, because the earlier manual check raised a UAC prompt while trying to measure Claude.
|
|
40
|
+
- `doctor` probes `sandbox:read-only` and `sandbox:workspace-write` sequentially before an
|
|
41
|
+
order first discovers a dead Codex sandbox: `ok` means alive, `fail` means dead with exit 1,
|
|
42
|
+
and inconclusive or unsupported checks are `warn`. Both a dead-sandbox row and an order's
|
|
43
|
+
pre-start refusal print the diagnosis from Codex's sandbox log and `setup_error.json`, because
|
|
44
|
+
the TradeForge refusal said only "setup refresh had errors" while the log named a locked
|
|
45
|
+
runtime file. Only lines attributed to the probe justify a repair signature for a locked
|
|
46
|
+
runtime file or corrupt deny-read state; other evidence stays raw with "No verified repair
|
|
47
|
+
signature". Unreadable logs do not change the verdict, and the package does not repair the host.
|
|
48
|
+
|
|
49
|
+
- `codex-bridge guidance [<topic>]` prints the package's detailed rules for one topic; without a
|
|
50
|
+
topic it lists `order`, `advisor`, `budget`, `records` and `concurrency`. The rules core sends
|
|
51
|
+
the orchestrator here before it writes a task file and tells it to stop and show the output if
|
|
52
|
+
the command fails, so orchestrator prompts need no change. Text comes from the installed home
|
|
53
|
+
rather than the clone, because the seeded-config incident exposed that difference; unknown
|
|
54
|
+
topics or extra arguments exit 2, and an unreadable topic exits 1 and names `codex-bridge update`.
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
- **Breaking for orchestrator prompts:** `UNAVAILABLE` now means a measured sign-out, not a
|
|
59
|
+
missing `codex` on `PATH`. A lookup miss is checked again after about a second; a second miss
|
|
60
|
+
is a free pre-start refusal starting "Codex CLI readiness unconfirmed", with exit 1 and no
|
|
61
|
+
run folder. Migration cost: replace the missing-CLI skip rule with checks of `codex --version`
|
|
62
|
+
for up to about two minutes, then repeat the same order exactly once if it answers. Otherwise,
|
|
63
|
+
or if that repeat gets the same refusal, hand the order to the next executor and name the
|
|
64
|
+
handoff in the summary; no second repeat. An npm update briefly rewrote the Codex shim and
|
|
65
|
+
caused an orchestrator to drop the bridge for the whole session.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
|
|
69
|
+
- **Breaking for operators:** update refuses an unrecorded file at a newly planned path instead
|
|
70
|
+
of silently overwriting operator text, including the host rules core. The refusal names the
|
|
71
|
+
`unowned` path. Migration cost: review that file and use `--force` only if replacement is
|
|
72
|
+
intended; identical bytes are adopted without it. The existing migration for the package's
|
|
73
|
+
Codex rules file remains available to records from before rules were recorded.
|
|
74
|
+
- Sandbox checks are bounded, and an inconclusive read-only check skips the workspace-write
|
|
75
|
+
check so a timed-out setup cannot race another probe. `codex --version` is also bounded.
|
|
76
|
+
Diagnosis reads only regular files in a real `.sandbox` folder, rejecting linked folders,
|
|
77
|
+
linked logs and special files that could redirect evidence or hang before a probe starts.
|
|
78
|
+
|
|
79
|
+
## [0.6.13] - 2026-10-08
|
|
80
|
+
|
|
81
|
+
### Added
|
|
82
|
+
|
|
83
|
+
- `codex-bridge runs move [--dry-run]` moves run records from `~/.claude/codex-runs/` to
|
|
84
|
+
`~/.lyupro/.codex-bridge/runs/`. It refuses while a run is live, copies every record, verifies
|
|
85
|
+
each file by SHA-256, imports the old repository's `codex-runs` git history, and writes
|
|
86
|
+
`state/runs-root.json` last. Only this command writes the move record. It offers to remove the
|
|
87
|
+
old folder only when every file in it has an identical copy in the new store. Like
|
|
88
|
+
`uninstall --purge --dry-run`, a dry run names every refusal and still shows the counts, since
|
|
89
|
+
some project is nearly always running when the operator plans the move.
|
|
90
|
+
- A finished run autocommits its own folder and the project marker when the git repository top
|
|
91
|
+
is the run store or package home. It never commits into a foreign repository such as
|
|
92
|
+
`~/.claude`, and a commit failure does not change the run verdict. On 2026-10-07 run records
|
|
93
|
+
left thousands of uncommitted files in that foreign repository; a session-close gate listed
|
|
94
|
+
each one.
|
|
95
|
+
- Paths under a retired run root are refused before anything is opened or created, including
|
|
96
|
+
`advice:`, `codex-bridge read`, `stop`, and a stale `CODEX_RUNS_ROOT` in the launcher. The refusal
|
|
97
|
+
names the equivalent new path; nothing is remapped and no quota is spent.
|
|
98
|
+
- The `repository:` task header must name an existing absolute directory without any path
|
|
99
|
+
segment ending in a dot or space.
|
|
100
|
+
|
|
101
|
+
### Changed
|
|
102
|
+
|
|
103
|
+
- The default run root is now `~/.lyupro/.codex-bridge/runs/` after a move or on a fresh install
|
|
104
|
+
where `~/.claude/codex-runs/` does not exist. A trimmed, non-empty `CODEX_RUNS_ROOT` takes
|
|
105
|
+
precedence; otherwise an existing old folder remains the root while the move is pending.
|
|
106
|
+
- The `doctor` `runsRoot` line shows the selected root, its source, and warnings for a pending
|
|
107
|
+
move, an old folder still present, or a root inside a foreign git repository. `.git` is not
|
|
108
|
+
counted as a project.
|
|
109
|
+
- Run records are user data. Uninstall and `--purge` preserve the package home's `runs/` folder
|
|
110
|
+
and the old `~/.claude/codex-runs/` folder.
|
|
111
|
+
|
|
7
112
|
## [0.6.12] - 2026-10-07
|
|
8
113
|
|
|
9
114
|
### Added
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codex-bridge
|
|
2
2
|
|
|
3
|
-
**Delegate implementation, reconnaissance, review and design advice from Claude Code to Codex —
|
|
3
|
+
**Delegate implementation, reconnaissance, review and design advice from Claude Code to Codex — eight host guards, zero runtime dependencies, zero build steps.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
6
6
|
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
@@ -32,8 +32,8 @@ Use `--scope project` to install the Claude Code-facing files under `<repo>/.cla
|
|
|
32
32
|
|
|
33
33
|
Installation has two roots:
|
|
34
34
|
|
|
35
|
-
- `~/.lyupro/.codex-bridge/` contains package runtime files, guards, configuration, conventions,
|
|
36
|
-
- The selected Claude Code root (`~/.claude/` or `<repo>/.claude/`) contains the four agents (`codex-scout`, `codex-build`, `codex-review`, `codex-advisor`), two slash commands, merged hook registrations in `settings.json
|
|
35
|
+
- `~/.lyupro/.codex-bridge/` contains package runtime files, guards, configuration, conventions, the installation record, and run records under `runs/` as user data.
|
|
36
|
+
- The selected Claude Code root (`~/.claude/` or `<repo>/.claude/`) contains the four agents (`codex-scout`, `codex-build`, `codex-review`, `codex-advisor`), two slash commands, and merged hook registrations in `settings.json`. Before the move, existing run records remain in `~/.claude/codex-runs/`.
|
|
37
37
|
|
|
38
38
|
The repository's `src/home/` directory is the literal image copied into the branded root: its
|
|
39
39
|
`hooks/`, `lib/`, `config.json`, and `conventions.md` paths are the host paths without remapping.
|
|
@@ -43,7 +43,7 @@ Before every `settings.json` write, the existing file is backed up. Existing set
|
|
|
43
43
|
|
|
44
44
|
If a global install and a clone coexist, you have two package copies. `codex-bridge update`, `codexb update`, and the clone entry point copy files from whichever copy launched the command; using the wrong one can replace a newer host installation with older files.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
Run records in `~/.lyupro/.codex-bridge/runs/` (or `~/.claude/codex-runs/` before the move) are user data. Uninstall, including `--purge`, never removes either folder.
|
|
47
47
|
|
|
48
48
|
## Requirements
|
|
49
49
|
|
|
@@ -57,12 +57,12 @@ If a global install and a clone coexist, you have two package copies. `codex-bri
|
|
|
57
57
|
- **Invalid work is rejected before quota is spent.** Scope, order, retry, branch, and continuation checks run before a run folder is created.
|
|
58
58
|
- **Verdicts come from artifacts.** CLI events, git state, and the declared report decide the result, not model claims about itself.
|
|
59
59
|
- **Identical retries attach safely.** Repeating the same command joins the active run instead of starting another one.
|
|
60
|
-
- **
|
|
60
|
+
- **8 host guards enforce and witness the boundary.** `reply-guard`, `order-gate`, `worktree-lock`, `prune-guard`, and `stop-guard` cover replies, orders, edits, deletion, and stopping; `worktree-witness` stands behind the lock and names a write that reached the tree anyway; `dispatcher-gate` lets a dispatcher run only the one runner command of its order and hands its caller the runner's own words; `load-witness` only observes — it records that the host loaded the package's rules core, the one proof `doctor` accepts that the rules are in context.
|
|
61
61
|
- **Installation is identifiable and reversible.** Writes to `settings.json` are merged and backed up; uninstall removes recorded package files only.
|
|
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 **3654 automated tests: 3651 passing and 3 skipped**.
|
|
66
66
|
|
|
67
67
|
## Verify
|
|
68
68
|
|
|
@@ -87,7 +87,7 @@ Output on a healthy host (trimmed):
|
|
|
87
87
|
[ok] conventions: ~/.lyupro/.codex-bridge/conventions.md (found)
|
|
88
88
|
[ok] codex: codex-cli 0.146.1
|
|
89
89
|
[ok] node: 24.18.0 (requires >=24)
|
|
90
|
-
[ok] runsRoot: ~/.
|
|
90
|
+
[ok] runsRoot: ~/.lyupro/.codex-bridge/runs (default)
|
|
91
91
|
[ok] liveRuns: 0 runs working right now
|
|
92
92
|
```
|
|
93
93
|
|
|
@@ -140,11 +140,13 @@ Transport files from runs at least **30 days** old are pruned automatically when
|
|
|
140
140
|
| `install [--scope user\|project] [--host <path>] [--dry-run] [--force]` | Install into a Claude Code host. |
|
|
141
141
|
| `update [--scope user\|project] [--host <path>] [--dry-run] [--force]` | Refresh a recorded installation. |
|
|
142
142
|
| `permissions [add\|remove] [--scope user\|project] [--host <path>]` | Inspect or manage optional shell rules. |
|
|
143
|
-
| `uninstall [--scope user\|project] [--host <path>] [--dry-run] [--purge]` | Remove recorded package files while preserving run artifacts; `--purge` also deletes
|
|
143
|
+
| `uninstall [--scope user\|project] [--host <path>] [--dry-run] [--purge]` | Remove recorded package files while preserving run artifacts; `--purge` also deletes other operator data after one preflight and two consents, preserving `runs/` and its containing home (see [Uninstall and purge](docs/overview.md#uninstall-and-purge)). |
|
|
144
144
|
| `inventory confirm [--scope user\|project] [--host <path>] [--dry-run]` | Confirm in a terminal that the recorded hosts are all the hosts using this home; until then the dispatcher model pin stays undetermined and `doctor` names this command. Changes only the inventory mark of the installation record. |
|
|
145
|
+
| `runs move [--dry-run]` | Copy every run record from `~/.claude/codex-runs/` into `~/.lyupro/.codex-bridge/runs/`, verify each file by SHA-256, import the old repository's `codex-runs` git history, and write the move record last. Refused while a run is live (`--dry-run` names the refusal and still previews); offers to remove the old folder only when every file has an identical copy in the new store. |
|
|
145
146
|
| `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. |
|
|
146
147
|
| `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. |
|
|
147
148
|
| `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. |
|
|
149
|
+
| `guidance [<topic>]` | Print the package's detailed rules for one topic from the installed home; without a topic, list the topics. The always-loaded rules core routes the orchestrator here. |
|
|
148
150
|
| `projects [<name>] [--json]` | List projects or runs in the run store. |
|
|
149
151
|
| `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. |
|
|
150
152
|
| `unlock [<project>\|--all]`<br>`unlock --lifecycle [--clear]` | Close records whose runner is gone, or show who holds the lock that serializes install, update and uninstall of this home; `--clear` removes that lock only when it is a file left by a holder proven dead (macOS; Windows and Linux locks are released by the kernel). |
|
package/cli/command-registry.mjs
CHANGED
|
@@ -4,17 +4,21 @@
|
|
|
4
4
|
* dispatch and usage had separate sources. One registry keeps invocation and help together.
|
|
5
5
|
*/
|
|
6
6
|
import { diagnose, renderDoctor } from './doctor.mjs';
|
|
7
|
+
import { sandboxChecks } from './doctor-sandbox.mjs';
|
|
7
8
|
import { probeContract } from './probe-contract.mjs';
|
|
8
9
|
import { resolveProbeTarget } from './probe-target.mjs';
|
|
9
10
|
import { brandStateDir } from '../src/home/lib/brand-home.mjs';
|
|
10
11
|
import { hook } from './hook.mjs';
|
|
12
|
+
import { guidance } from './guidance.mjs';
|
|
11
13
|
import { resolveHost } from './hosts.mjs';
|
|
12
|
-
import { install } from './install.mjs';
|
|
14
|
+
import { install } from './install.mjs';
|
|
13
15
|
import { inventoryConfirm } from './inventory-confirm.mjs';
|
|
14
16
|
import { model } from './model.mjs';
|
|
15
17
|
import { projects } from './projects.mjs';
|
|
16
18
|
import { read } from './read.mjs';
|
|
17
19
|
import { runCodex } from './run-launcher.mjs';
|
|
20
|
+
import { runsMove } from './runs-move.mjs';
|
|
21
|
+
import { removeOldStore } from './runs-move-remove.mjs';
|
|
18
22
|
import { permissions } from './permissions.mjs';
|
|
19
23
|
import { prune } from './prune.mjs';
|
|
20
24
|
import { stop } from './stop.mjs';
|
|
@@ -26,8 +30,8 @@ import { update } from './update.mjs';
|
|
|
26
30
|
export function commandOptions(command, argv) {
|
|
27
31
|
const options = {};
|
|
28
32
|
const booleanFlags = command === 'install' || command === 'update' ? new Set(['--dry-run', '--force'])
|
|
29
|
-
: command === 'uninstall' ? new Set(['--dry-run', '--purge'])
|
|
30
|
-
: command === 'inventory' ? new Set(['--dry-run'])
|
|
33
|
+
: command === 'uninstall' ? new Set(['--dry-run', '--purge'])
|
|
34
|
+
: command === 'inventory' || command === 'runs' ? new Set(['--dry-run'])
|
|
31
35
|
: command === 'doctor' ? new Set(['--probe-contract']) : new Set();
|
|
32
36
|
const flagNames = new Map([
|
|
33
37
|
['--dry-run', 'dryRun'],
|
|
@@ -41,6 +45,7 @@ export function commandOptions(command, argv) {
|
|
|
41
45
|
options[flagNames.get(arg)] = true;
|
|
42
46
|
continue;
|
|
43
47
|
}
|
|
48
|
+
if (command === 'runs') throw new Error(`unknown runs option "${arg}"`);
|
|
44
49
|
if (arg !== '--scope' && arg !== '--host' && !(command === 'doctor' && arg === '--probe-executable')) throw new Error(`unknown ${command} option "${arg}"`);
|
|
45
50
|
const value = argv[index + 1];
|
|
46
51
|
if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
|
|
@@ -125,20 +130,44 @@ export const COMMANDS = [
|
|
|
125
130
|
},
|
|
126
131
|
},
|
|
127
132
|
{
|
|
128
|
-
name: 'inventory',
|
|
129
|
-
summary: 'Confirm that the recorded hosts are all the hosts using this home',
|
|
130
|
-
usage: ['codex-bridge inventory confirm [--scope user|project] [--host <path>] [--dry-run]'],
|
|
131
|
-
section: 'public',
|
|
132
|
-
async handler(argv, io) {
|
|
133
|
-
if (argv[0] !== 'confirm') throw new Error(`unknown inventory action "${argv[0] ?? ''}"`);
|
|
134
|
-
const options = commandOptions('inventory', argv.slice(1));
|
|
135
|
-
const host = resolveHost(options);
|
|
136
|
-
const result = await inventoryConfirm({ host, dryRun: options.dryRun });
|
|
137
|
-
io.log(result.output);
|
|
138
|
-
return result.exitCode;
|
|
139
|
-
},
|
|
140
|
-
},
|
|
141
|
-
{
|
|
133
|
+
name: 'inventory',
|
|
134
|
+
summary: 'Confirm that the recorded hosts are all the hosts using this home',
|
|
135
|
+
usage: ['codex-bridge inventory confirm [--scope user|project] [--host <path>] [--dry-run]'],
|
|
136
|
+
section: 'public',
|
|
137
|
+
async handler(argv, io) {
|
|
138
|
+
if (argv[0] !== 'confirm') throw new Error(`unknown inventory action "${argv[0] ?? ''}"`);
|
|
139
|
+
const options = commandOptions('inventory', argv.slice(1));
|
|
140
|
+
const host = resolveHost(options);
|
|
141
|
+
const result = await inventoryConfirm({ host, dryRun: options.dryRun });
|
|
142
|
+
io.log(result.output);
|
|
143
|
+
return result.exitCode;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'runs',
|
|
148
|
+
summary: 'Move run records into the package home (Plan_77)',
|
|
149
|
+
usage: ['codex-bridge runs move [--dry-run]'],
|
|
150
|
+
section: 'public',
|
|
151
|
+
actions: {
|
|
152
|
+
move: {
|
|
153
|
+
summary: 'Move run records into the package home (Plan_77)',
|
|
154
|
+
usage: ['codex-bridge runs move [--dry-run]'],
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
async handler(argv, io) {
|
|
158
|
+
if (argv[0] !== 'move') throw new Error(`unknown runs action "${argv[0] ?? ''}"`);
|
|
159
|
+
const options = commandOptions('runs', argv.slice(1));
|
|
160
|
+
const result = runsMove({ dryRun: options.dryRun });
|
|
161
|
+
io.log(result.output);
|
|
162
|
+
if (result.exitCode === 0 && result.oldStore) {
|
|
163
|
+
const removal = await removeOldStore({ from: result.oldStore, to: result.homeRoot });
|
|
164
|
+
io.log(removal.output);
|
|
165
|
+
return removal.exitCode;
|
|
166
|
+
}
|
|
167
|
+
return result.exitCode;
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
142
171
|
name: 'doctor',
|
|
143
172
|
summary: 'Diagnose the selected Claude Code host (--probe-contract measures on a live host)',
|
|
144
173
|
usage: ['codex-bridge doctor [--scope user|project] [--host <path>] [--probe-contract] [--probe-executable <path>]'],
|
|
@@ -160,7 +189,7 @@ export const COMMANDS = [
|
|
|
160
189
|
io.log(`probe: ${name} ${verdict.result} — ${verdict.detail}`);
|
|
161
190
|
}
|
|
162
191
|
}
|
|
163
|
-
const result = await diagnose({ host });
|
|
192
|
+
const result = await diagnose({ host, sandbox: sandboxChecks });
|
|
164
193
|
io.log(renderDoctor(result));
|
|
165
194
|
// An inconclusive probe wrote nothing and measured nothing; exiting 0 would let a failed
|
|
166
195
|
// measurement pass silently in a script (Plan_52 D26).
|
|
@@ -248,6 +277,18 @@ export const COMMANDS = [
|
|
|
248
277
|
return result.exitCode;
|
|
249
278
|
},
|
|
250
279
|
},
|
|
280
|
+
{
|
|
281
|
+
name: 'guidance',
|
|
282
|
+
summary: "Print the package's detailed rules for one topic",
|
|
283
|
+
usage: ['codex-bridge guidance [<topic>]'],
|
|
284
|
+
section: 'public',
|
|
285
|
+
async handler(argv, io) {
|
|
286
|
+
const result = guidance(argv);
|
|
287
|
+
if (result.exitCode === 2) io.error(result.output);
|
|
288
|
+
else io.log(result.output);
|
|
289
|
+
return result.exitCode;
|
|
290
|
+
},
|
|
291
|
+
},
|
|
251
292
|
{
|
|
252
293
|
name: 'read',
|
|
253
294
|
summary: "Render a run's structured event stream",
|
|
@@ -1,111 +1,132 @@
|
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return check('
|
|
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
|
+
// Selected by where the file lands, not by where it comes from: Plan_64 B1 moved the sources
|
|
36
|
+
// under src/claude/, a source-path pattern matched nothing, and this check reported
|
|
37
|
+
// "0 installed agent definition(s) match" as ok.
|
|
38
|
+
const agents = plan.filter((item) => path.dirname(item.target) === host.agentsDir && item.target.endsWith('.md'));
|
|
39
|
+
if (agents.length === 0) return check('agents', 'fail', 'This package plans no agent definitions; the package tree is broken');
|
|
40
|
+
const mismatches = [];
|
|
41
|
+
const unreadable = [];
|
|
42
|
+
for (const item of agents) {
|
|
43
|
+
const expected = contentFingerprint(await plannedContent(item, host.brandRoot));
|
|
44
|
+
if (await fileFingerprint(item.target) !== expected) mismatches.push(path.basename(item.target));
|
|
45
|
+
const expectedName = path.basename(item.source, '.md');
|
|
46
|
+
try {
|
|
47
|
+
const frontmatter = parseFrontmatter(await fs.readFile(item.target, 'utf8'));
|
|
48
|
+
if (!frontmatter) throw new Error('frontmatter is missing');
|
|
49
|
+
if (frontmatter.name !== expectedName) {
|
|
50
|
+
throw new Error(`name is ${JSON.stringify(frontmatter.name)}, expected ${JSON.stringify(expectedName)}`);
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
unreadable.push(`${path.basename(item.target)} (${err.message})`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// The 2026-08-10 registration failure left files present but unreadable by Claude Code. Drift was
|
|
57
|
+
// advisory until a 2026-08-16 checklist run planted the pre-Plan_41 invocation — a path instead of
|
|
58
|
+
// the package command — in an installed definition: doctor kept exit 0 while every delegation
|
|
59
|
+
// would stop on a permission prompt. A definition that is not this package's is not judged healthy.
|
|
60
|
+
if (unreadable.length) {
|
|
61
|
+
return check('agents', 'fail', `Installed agent definitions cannot be read: ${unreadable.join(', ')}; run codex-bridge update --force`);
|
|
62
|
+
}
|
|
63
|
+
return mismatches.length
|
|
64
|
+
? check('agents', 'fail', `Installed agent definitions differ from this package: ${mismatches.join(', ')}; run codex-bridge update --force`)
|
|
65
|
+
: check('agents', 'ok', `${agents.length} installed agent definition(s) match this package`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function claudeRulesCheck(host, record) {
|
|
69
|
+
if (!record) return check('claudeRules', 'warn', 'Host rules were not checked; run codex-bridge install');
|
|
70
|
+
const plan = await buildInstallPlan(host);
|
|
71
|
+
// Plan_64 B1: source-path selection hid an empty plan as a healthy installation.
|
|
72
|
+
const rules = plan.filter((item) => path.dirname(item.target) === host.rulesDir);
|
|
73
|
+
if (rules.length === 0) return check('claudeRules', 'fail', 'This package plans no host rules; the package tree is broken');
|
|
74
|
+
const mismatches = [];
|
|
75
|
+
for (const item of rules) {
|
|
76
|
+
const expected = contentFingerprint(await plannedContent(item, host.brandRoot));
|
|
77
|
+
if (await fileFingerprint(item.target) !== expected) mismatches.push(path.basename(item.target));
|
|
78
|
+
}
|
|
79
|
+
// Plan_64 B3: equal bytes imply no paths frontmatter; tests/claude-rules-core.test.mjs guards the package core.
|
|
80
|
+
return mismatches.length
|
|
81
|
+
? check('claudeRules', 'fail', `Host rules differ from this package or are missing: ${mismatches.join(', ')}; run codex-bridge update --force`)
|
|
82
|
+
: check('claudeRules', 'ok', `${rules.length} host rules file(s) match this package`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function conventionsCheck(host) {
|
|
86
|
+
const file = host.brandConventionsPath;
|
|
87
|
+
let content;
|
|
88
|
+
try {
|
|
89
|
+
content = await fs.readFile(file, 'utf8');
|
|
90
|
+
} catch (err) {
|
|
91
|
+
if (err.code === 'ENOENT') return check('conventions', 'ok', `${file} (not found; optional)`);
|
|
92
|
+
return check('conventions', 'fail', `cannot read ${file}: ${err.message}`);
|
|
93
|
+
}
|
|
94
|
+
return content.trim()
|
|
95
|
+
? check('conventions', 'ok', `${file} (found)`)
|
|
96
|
+
: check('conventions', 'warn', `${file} (found but empty)`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function rulesCheck(host, record) {
|
|
100
|
+
if (!record) return check('rules', 'warn', 'cannot check before installation');
|
|
101
|
+
if (!record.rules) {
|
|
102
|
+
return check('rules', 'warn', 'rules were not installed by this installation; install or update will add them');
|
|
103
|
+
}
|
|
104
|
+
let registry;
|
|
105
|
+
try {
|
|
106
|
+
registry = await readRulesRegistry(host);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Keep every diagnostic visible: package removal on a broken registry left the host without its watchdog.
|
|
109
|
+
return check('rules', 'fail', err.message);
|
|
110
|
+
}
|
|
111
|
+
const ownerNote = registry?.owners.length > 1 ? `; ${registry.owners.length} owners` : '';
|
|
112
|
+
const fingerprint = await fileFingerprint(record.rules.path);
|
|
113
|
+
if (!fingerprint) return check('rules', 'fail', `${record.rules.path}${ownerNote ? ` (${registry.owners.length} owners)` : ''}`);
|
|
114
|
+
return fingerprint === record.rules.fingerprint
|
|
115
|
+
? check('rules', 'ok', `${record.rules.path} (matches record${ownerNote})`)
|
|
116
|
+
: check('rules', 'warn', `${record.rules.path} (modified after installation${ownerNote})`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export async function permissionsCheck(host) {
|
|
120
|
+
try {
|
|
121
|
+
const status = await inspectPermissions(host.settingsPath);
|
|
122
|
+
// Permission rules are an optional operator action; Plan_22 keeps their absence a warning so
|
|
123
|
+
// doctor does not turn a healthy installation red merely because hardening was not requested.
|
|
124
|
+
// The ask count is part of the line because a full set shadowed by `ask` reads as working and
|
|
125
|
+
// is not: the live run of Plan_22-1 found this line saying `installed (24/24)` over it.
|
|
126
|
+
const shadow = status.askCount ? `, ${status.askCount} shadowed by ask` : '';
|
|
127
|
+
return check('permissions', status.state === 'installed' ? 'ok' : 'warn',
|
|
128
|
+
`${status.state} (${status.present}/${status.total} own strings in ${RULE_LIST_NAMES}${shadow})`);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
return check('permissions', 'warn', `cannot inspect permission rules: ${err.message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|