@lyupro/codex-bridge 0.6.11 → 0.6.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/README.md +12 -9
  3. package/cli/active-hosts.mjs +44 -0
  4. package/cli/command-registry.mjs +58 -1
  5. package/cli/dispatcher-model-check.mjs +14 -12
  6. package/cli/doctor-host-side.mjs +117 -111
  7. package/cli/doctor-installation.mjs +132 -111
  8. package/cli/doctor-load-witness.mjs +75 -0
  9. package/cli/doctor-runs.mjs +41 -6
  10. package/cli/doctor-sandbox.mjs +64 -0
  11. package/cli/doctor.mjs +43 -18
  12. package/cli/guidance.mjs +42 -0
  13. package/cli/handback-witness-check.mjs +23 -19
  14. package/cli/host-inspection.mjs +1 -0
  15. package/cli/hosts.mjs +4 -3
  16. package/cli/install-record.mjs +9 -2
  17. package/cli/inventory-confirm.mjs +75 -0
  18. package/cli/inventory-transition.mjs +3 -1
  19. package/cli/manifest.mjs +6 -3
  20. package/cli/read.mjs +1 -2
  21. package/cli/removal-outcomes.mjs +5 -2
  22. package/cli/removal-plan.mjs +4 -1
  23. package/cli/remove-layout.mjs +4 -4
  24. package/cli/run-lookup.mjs +14 -2
  25. package/cli/runs-inventory.mjs +10 -2
  26. package/cli/runs-move-copy.mjs +123 -0
  27. package/cli/runs-move-history.mjs +78 -0
  28. package/cli/runs-move-remove.mjs +92 -0
  29. package/cli/runs-move.mjs +96 -0
  30. package/cli/stop.mjs +1 -2
  31. package/cli/uninstall-purge.mjs +1 -0
  32. package/cli/uninstall.mjs +2 -0
  33. package/cli/update.mjs +12 -7
  34. package/package.json +4 -2
  35. package/scripts/home-write-inventory.mjs +17 -2
  36. package/scripts/isolated-roots.mjs +2 -1
  37. package/src/{agents → claude/agents}/codex-advisor.md +8 -7
  38. package/src/{agents → claude/agents}/codex-build.md +7 -6
  39. package/src/{agents → claude/agents}/codex-review.md +6 -5
  40. package/src/{agents → claude/agents}/codex-scout.md +7 -6
  41. package/src/{commands → claude/commands}/usage.md +9 -5
  42. package/src/claude/rules/core.md +29 -0
  43. package/src/{rules → codex/rules}/codex-bridge.rules +1 -1
  44. package/src/home/guidance/advisor.md +42 -0
  45. package/src/home/guidance/budget.md +37 -0
  46. package/src/home/guidance/concurrency.md +17 -0
  47. package/src/home/guidance/order.md +66 -0
  48. package/src/home/guidance/records.md +32 -0
  49. package/src/home/hooks/live-runs.mjs +2 -1
  50. package/src/home/hooks/load-witness.mjs +23 -0
  51. package/src/home/lib/claude-layout.mjs +1 -0
  52. package/src/home/lib/codex-home.mjs +8 -0
  53. package/src/home/lib/home-registry.mjs +21 -0
  54. package/src/home/lib/hook-definitions.mjs +2 -0
  55. package/src/home/lib/host-version.mjs +33 -14
  56. package/src/home/lib/install-owner-roots.mjs +55 -53
  57. package/src/home/lib/load-witness.mjs +113 -0
  58. package/src/home/lib/meta/reply.mjs +1 -1
  59. package/src/home/lib/runner/codex-availability.mjs +11 -2
  60. package/src/home/lib/runner/exit-codes.mjs +1 -1
  61. package/src/home/lib/runner/launcher.mjs +10 -4
  62. package/src/home/lib/runner/preflight.mjs +12 -4
  63. package/src/home/lib/runner/retired-roots.mjs +81 -0
  64. package/src/home/lib/runner/run-autocommit.mjs +109 -0
  65. package/src/home/lib/runner/runs-root.mjs +47 -7
  66. package/src/home/lib/runner/sandbox-diagnosis.mjs +180 -0
  67. package/src/home/lib/runner/sandbox-probe.mjs +29 -1
  68. package/src/home/lib/runner/task-input.mjs +30 -0
  69. package/src/home/lib/runner/worker.mjs +5 -0
  70. /package/src/{commands → claude/commands}/env.md +0 -0
  71. /package/src/{commands → claude/commands}/model.md +0 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,145 @@ 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
+
112
+ ## [0.6.12] - 2026-10-07
113
+
114
+ ### Added
115
+
116
+ - `codex-bridge inventory confirm [--scope user|project] [--host <path>] [--dry-run]` lets the
117
+ operator declare that the recorded hosts are all the hosts using this home. An old installation
118
+ record migrated to format 2 without a terminal stays `inventory: incomplete`: later updates do
119
+ not ask again, and since 0.6.11 every dispatcher model observation leaves the pin undetermined.
120
+ The package cannot prove that no unrecorded host uses the home; only the operator can.
121
+ The command shows the home, recorded host roots and Codex rules registry hints, marking missing
122
+ folders because the registry can retain deleted scratchpads. Enroll an unlisted host with
123
+ `codex-bridge install --host "<path>"` before confirming. A terminal "yes" changes only
124
+ `inventory` to `complete` and removes `legacy`; owners, fingerprints and every other field are
125
+ left as read. This permits ordinary shared-image cleanup when the last recorded host uninstalls;
126
+ it does not record a model match or clear a violation. No, EOF or a non-TTY exits 1 without changes;
127
+ Ctrl+C exits 130; a valid dry run shows the declaration without writing and exits 0. Confirmation
128
+ and an already-complete inventory without `legacy` exit 0. The `doctor` owner rows and the
129
+ incomplete-inventory pin reason now name this command.
130
+
131
+ ### Changed
132
+
133
+ - `doctor` judges handback witness and dispatcher model state for every host whose dispatchers
134
+ ran in the last 24 hours. On 2026-10-07 two VS Code windows ran 2.1.291 and 2.1.292: an observation
135
+ on the non-newest host appeared only as history, and a model violation there would have printed
136
+ `ok`. Active witness violations identify their host with `host <version>:`, and each active
137
+ host's model entries have their own rows with that prefix. A model `fail` on any active host
138
+ reaches exit code 1. Older unresolved entries remain in one history sentence, without active
139
+ warning rows. The newest-host selector still serves contracts, `otherHost:*` and install.
140
+
141
+ ### Fixed
142
+
143
+ - Dispatcher model rows no longer print a doubled period after a pin reason: the live `doctor`
144
+ output had added its own period to a reason that already ended with one.
145
+
7
146
  ## [0.6.11] - 2026-10-07
8
147
 
9
148
  ### Changed
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 — seven host guards, zero runtime dependencies, zero build steps.**
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
  [![npm version](https://img.shields.io/npm/v/@lyupro/codex-bridge)](https://www.npmjs.com/package/@lyupro/codex-bridge)
6
6
  [![Node.js](https://img.shields.io/node/v/@lyupro/codex-bridge)](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, and the installation record.
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`, and run artifacts under `codex-runs/`.
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
- `codex-runs/` contains operator data. Uninstall never removes it.
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
- - **7 host guards enforce 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.
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 **3199 automated tests: 3198 passing and 1 skipped**.
65
+ The current suite contains **3654 automated tests: 3651 passing and 3 skipped**.
66
66
 
67
67
  ## Verify
68
68
 
@@ -87,13 +87,13 @@ 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: ~/.claude/codex-runs
90
+ [ok] runsRoot: ~/.lyupro/.codex-bridge/runs (default)
91
91
  [ok] liveRuns: 0 runs working right now
92
92
  ```
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`, `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.
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. Witness and model checks cover every host whose dispatchers ran in the last 24 hours; two VS Code windows on 2.1.291 and 2.1.292 on 2026-10-07 showed why the newest host alone could hide an active host's violation in history. The witness warns for unresolved causes on any active 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. Each active host's model entries have their own rows prefixed `host <version>:`, comparing transcript-observed families with the installed contract: a latest violation on any active host is `fail` with exit 1, uncertainty is `warn`, and a match, including recovery, is `ok`; no model entries for active hosts is also `ok`. Hosts outside the 24-hour window and recovered incidents remain history, not permanent warnings. Contracts, `otherHost:*` and install still use the newest observed host selector. 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
 
@@ -140,10 +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 your data and the home after one preflight and two consents (see [Uninstall and purge](docs/overview.md#uninstall-and-purge)). |
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
+ | `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. |
144
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. |
145
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. |
146
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. |
147
150
  | `projects [<name>] [--json]` | List projects or runs in the run store. |
148
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. |
149
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). |
@@ -0,0 +1,44 @@
1
+ /** Names the hosts whose dispatchers ran in the last 24 hours, the hosts whose witness and model state is active;
2
+ * Plan_67 D12: concurrent VS Code hosts 2.1.291 and 2.1.292 on 2026-10-07 must both be judged, regardless of session liveness.
3
+ */
4
+ export const ACTIVE_HOST_WINDOW_MS = 24 * 60 * 60 * 1000;
5
+
6
+ export function activeHostVersions({ observations, witnessRecord, modelRecord, now = new Date() }) {
7
+ const currentTime = new Date(now).getTime();
8
+ const activity = new Map();
9
+ function recordActivity(version, at) {
10
+ if (typeof version !== 'string' || !version || version === 'unknown') return;
11
+ if (typeof at !== 'string'
12
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(at)) return;
13
+ const time = Date.parse(at);
14
+ const date = Date.parse(`${at.slice(0, 10)}T00:00:00Z`);
15
+ if (!Number.isFinite(time) || !Number.isFinite(date)
16
+ || new Date(date).toISOString().slice(0, 10) !== at.slice(0, 10)) return;
17
+ // Plan_67 R2: future evidence must not hide a valid past source for the same host.
18
+ if (time > currentTime) return;
19
+ if (!activity.has(version) || time > activity.get(version)) activity.set(version, time);
20
+ }
21
+ function ledgerActivity(ledger) {
22
+ if (!ledger || ledger.corrupt) return;
23
+ for (const entry of Object.values(ledger.entries || {})) {
24
+ if (!entry || entry.corrupt || entry.lastObservation?.corrupt) continue;
25
+ const observation = entry.lastObservation;
26
+ recordActivity(observation?.data?.hostVersion, observation?.at);
27
+ }
28
+ }
29
+ if (observations && !observations.corrupt) {
30
+ for (const [version, host] of Object.entries(observations.hosts || {})) {
31
+ if (host && !host.corrupt) recordActivity(version, host.lastSeen);
32
+ }
33
+ for (const session of Object.values(observations.sessions || {})) {
34
+ if (session && !session.corrupt) recordActivity(session.version, session.at);
35
+ }
36
+ }
37
+ if (witnessRecord && !witnessRecord.corrupt) {
38
+ for (const [version, at] of Object.entries(witnessRecord.intercepted || {})) recordActivity(version, at);
39
+ ledgerActivity(witnessRecord.ledger);
40
+ }
41
+ ledgerActivity(modelRecord);
42
+ return [...activity].filter(([, at]) => currentTime - at >= 0 && currentTime - at <= ACTIVE_HOST_WINDOW_MS)
43
+ .sort((a, b) => b[1] - a[1]).map(([version]) => version);
44
+ }
@@ -4,16 +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
14
  import { install } from './install.mjs';
15
+ import { inventoryConfirm } from './inventory-confirm.mjs';
13
16
  import { model } from './model.mjs';
14
17
  import { projects } from './projects.mjs';
15
18
  import { read } from './read.mjs';
16
19
  import { runCodex } from './run-launcher.mjs';
20
+ import { runsMove } from './runs-move.mjs';
21
+ import { removeOldStore } from './runs-move-remove.mjs';
17
22
  import { permissions } from './permissions.mjs';
18
23
  import { prune } from './prune.mjs';
19
24
  import { stop } from './stop.mjs';
@@ -26,6 +31,7 @@ export function commandOptions(command, argv) {
26
31
  const options = {};
27
32
  const booleanFlags = command === 'install' || command === 'update' ? new Set(['--dry-run', '--force'])
28
33
  : command === 'uninstall' ? new Set(['--dry-run', '--purge'])
34
+ : command === 'inventory' || command === 'runs' ? new Set(['--dry-run'])
29
35
  : command === 'doctor' ? new Set(['--probe-contract']) : new Set();
30
36
  const flagNames = new Map([
31
37
  ['--dry-run', 'dryRun'],
@@ -39,6 +45,7 @@ export function commandOptions(command, argv) {
39
45
  options[flagNames.get(arg)] = true;
40
46
  continue;
41
47
  }
48
+ if (command === 'runs') throw new Error(`unknown runs option "${arg}"`);
42
49
  if (arg !== '--scope' && arg !== '--host' && !(command === 'doctor' && arg === '--probe-executable')) throw new Error(`unknown ${command} option "${arg}"`);
43
50
  const value = argv[index + 1];
44
51
  if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
@@ -122,6 +129,44 @@ export const COMMANDS = [
122
129
  return result.exitCode;
123
130
  },
124
131
  },
132
+ {
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
+ },
125
170
  {
126
171
  name: 'doctor',
127
172
  summary: 'Diagnose the selected Claude Code host (--probe-contract measures on a live host)',
@@ -144,7 +189,7 @@ export const COMMANDS = [
144
189
  io.log(`probe: ${name} ${verdict.result} — ${verdict.detail}`);
145
190
  }
146
191
  }
147
- const result = await diagnose({ host });
192
+ const result = await diagnose({ host, sandbox: sandboxChecks });
148
193
  io.log(renderDoctor(result));
149
194
  // An inconclusive probe wrote nothing and measured nothing; exiting 0 would let a failed
150
195
  // measurement pass silently in a script (Plan_52 D26).
@@ -232,6 +277,18 @@ export const COMMANDS = [
232
277
  return result.exitCode;
233
278
  },
234
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
+ },
235
292
  {
236
293
  name: 'read',
237
294
  summary: "Render a run's structured event stream",
@@ -1,4 +1,4 @@
1
- /** turns the dispatcher-model ledger into doctor rows; Plan_67 D4/D8 and D8 clarification 2026-10-07. */
1
+ /** turns the dispatcher-model ledger into doctor rows; Plan_67 D4/D8/D12 judge every recently active host. */
2
2
  import path from 'node:path';
3
3
  import { DISPATCHER_MODEL_FILE } from '../src/home/lib/dispatcher-model.mjs';
4
4
  import { entryState } from '../src/home/lib/observation-ledger.mjs';
@@ -12,48 +12,50 @@ function entryCheck(entry) {
12
12
  const observation = entry.lastObservation;
13
13
  const { data, verdict, at } = observation;
14
14
  const key = `dispatcherModel:${data.agentType}`;
15
+ const prefix = `host ${data.hostVersion}: `;
15
16
  const state = entryState(entry);
16
17
  // D4: only the latest confirmed violation fails; D8 keeps uncertainty from erasing history.
17
18
  if (verdict === 'violation') {
18
- return check(key, 'fail', `${observationSummary(observation)}; Claude quota was spent on this dispatcher; see Plan_67.`);
19
+ return check(key, 'fail', `${prefix}${observationSummary(observation)}; Claude quota was spent on this dispatcher; see Plan_67.`);
19
20
  }
20
21
  if (verdict === 'undetermined') {
21
22
  const reason = data.pinReasons?.length
22
- ? data.pinReasons.join('; ') : 'no model family could be read from the transcript';
23
+ // Reasons are sentences ending in a period; the row adds its own (live doctor printed "record..").
24
+ ? data.pinReasons.map((text) => text.replace(/\.$/, '')).join('; ') : 'no model family could be read from the transcript';
23
25
  const unresolved = state === 'violation'
24
26
  ? `; earlier violation (${observationSummary(entry.lastViolation)}) is not yet disproved` : '';
25
- return check(key, 'warn', `${data.agentType}: model observation undetermined at ${at}: ${reason}${unresolved}.`);
27
+ return check(key, 'warn', `${prefix}${data.agentType}: model observation undetermined at ${at}: ${reason}${unresolved}.`);
26
28
  }
27
29
  const history = state === 'recovered'
28
30
  ? `; recovered from earlier violation (${observationSummary(entry.lastViolation)})` : '';
29
- return check(key, 'ok', `${observationSummary(observation)}; matches the installed contract${history}.`);
31
+ return check(key, 'ok', `${prefix}${observationSummary(observation)}; matches the installed contract${history}.`);
30
32
  }
31
33
 
32
- export function dispatcherModelChecks({ record, hostVersion, stateDir }) {
34
+ export function dispatcherModelChecks({ record, activeHosts, stateDir }) {
33
35
  if (record?.corrupt) {
34
36
  return [check('dispatcherModel', 'warn',
35
37
  `The dispatcher model record is unreadable; delete ${path.join(stateDir, DISPATCHER_MODEL_FILE)} to reset it.`)];
36
38
  }
37
39
  const entries = Object.values(record?.entries || {});
38
- const current = entries.filter((entry) => entry.lastObservation.data.hostVersion === hostVersion);
40
+ const current = entries.filter((entry) => activeHosts.includes(entry.lastObservation.data.hostVersion));
39
41
  const rows = current.map(entryCheck);
40
42
  if (!rows.length) {
41
- rows.push(check('dispatcherModel', 'ok', 'Not observed yet — recorded when a dispatcher stops on this host.'));
43
+ rows.push(check('dispatcherModel', 'ok', 'Not observed in the last 24 hours — recorded when a dispatcher stops.'));
42
44
  }
43
45
  const hosts = new Map();
44
46
  for (const entry of entries) {
45
47
  const version = entry.lastObservation.data.hostVersion;
46
- if (version === hostVersion) continue;
48
+ if (activeHosts.includes(version)) continue;
47
49
  const counts = hosts.get(version) || { entries: 0, violations: 0 };
48
50
  counts.entries += 1;
49
51
  if (entryState(entry) === 'violation') counts.violations += 1;
50
52
  hosts.set(version, counts);
51
53
  }
52
- // D8: other hosts are one history sentence, never additional active warning rows.
54
+ // D12: inactive hosts are one history sentence, never additional active warning rows.
53
55
  if (hosts.size) {
54
56
  const history = [...hosts].map(([version, counts]) =>
55
57
  `${version ?? 'unknown host'}: ${counts.entries} entries, ${counts.violations} unresolved violations`);
56
- rows[0].value += ` History on other hosts (historical, not current): ${history.join('; ')}.`;
58
+ rows[0].value += ` History on hosts without dispatcher activity in the last 24 hours (historical, not current): ${history.join('; ')}.`;
57
59
  }
58
60
  return rows;
59
- }
61
+ }