@lyupro/codex-bridge 0.6.8 → 0.6.10

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 (128) hide show
  1. package/CHANGELOG.md +160 -0
  2. package/README.md +6 -4
  3. package/bin/codex-bridge.mjs +35 -177
  4. package/cli/command-help.mjs +49 -0
  5. package/cli/command-registry.mjs +284 -0
  6. package/cli/copy.mjs +42 -5
  7. package/cli/dispatcher-contract-record.mjs +3 -2
  8. package/cli/doctor-host-side.mjs +110 -0
  9. package/cli/doctor.mjs +17 -1
  10. package/cli/home-inspection.mjs +122 -0
  11. package/cli/home-plan-execute.mjs +140 -0
  12. package/cli/home-removal-plan.mjs +61 -0
  13. package/cli/hook-recognizer.mjs +78 -0
  14. package/cli/host-contract.mjs +11 -5
  15. package/cli/host-inspection.mjs +156 -0
  16. package/cli/host-removal.mjs +91 -0
  17. package/cli/image-evidence.mjs +44 -0
  18. package/cli/install-owners.mjs +244 -0
  19. package/cli/install-record.mjs +71 -9
  20. package/cli/install.mjs +88 -75
  21. package/cli/inventory-removal.mjs +84 -0
  22. package/cli/inventory-transition.mjs +66 -0
  23. package/cli/lifecycle-lock-clear.mjs +104 -0
  24. package/cli/lifecycle-lock-inspect.mjs +92 -0
  25. package/cli/lifecycle-lock.mjs +222 -0
  26. package/cli/lifecycle-transaction.mjs +57 -0
  27. package/cli/link-segments.mjs +45 -0
  28. package/cli/model-set.mjs +4 -3
  29. package/cli/model-speed.mjs +9 -2
  30. package/cli/model.mjs +5 -3
  31. package/cli/owner-record.mjs +40 -0
  32. package/cli/owner-sync-apply.mjs +108 -0
  33. package/cli/owner-sync.mjs +115 -0
  34. package/cli/projects.mjs +5 -2
  35. package/cli/prune-args.mjs +9 -4
  36. package/cli/prune.mjs +23 -72
  37. package/cli/purge-live-runs.mjs +98 -0
  38. package/cli/purge-preflight.mjs +103 -0
  39. package/cli/read.mjs +22 -1
  40. package/cli/record-removal.mjs +43 -0
  41. package/cli/removal-outcomes.mjs +115 -0
  42. package/cli/removal-plan.mjs +187 -0
  43. package/cli/remove-layout.mjs +40 -1
  44. package/cli/rules-owners.mjs +4 -2
  45. package/cli/settings-merge.mjs +3 -29
  46. package/cli/terminal-question.mjs +41 -0
  47. package/cli/uninstall-purge.mjs +139 -0
  48. package/cli/uninstall-rules.mjs +72 -0
  49. package/cli/uninstall.mjs +208 -127
  50. package/cli/unlock-lifecycle.mjs +102 -0
  51. package/cli/unlock.mjs +7 -4
  52. package/cli/update.mjs +102 -27
  53. package/package.json +4 -3
  54. package/scripts/home-write-audit.mjs +286 -0
  55. package/scripts/home-write-inventory.mjs +59 -0
  56. package/scripts/home-write-lexer.mjs +182 -0
  57. package/scripts/isolated-roots.mjs +6 -1
  58. package/scripts/open-work/check.mjs +180 -0
  59. package/scripts/open-work/live-thresholds.mjs +155 -0
  60. package/scripts/open-work/register.mjs +56 -0
  61. package/scripts/root-guard/root-entries.mjs +15 -0
  62. package/scripts/run-tests.mjs +26 -1
  63. package/src/agents/codex-advisor.md +23 -10
  64. package/src/agents/codex-build.md +24 -14
  65. package/src/agents/codex-review.md +17 -10
  66. package/src/agents/codex-scout.md +22 -12
  67. package/src/home/conventions.md +9 -0
  68. package/src/home/hooks/guard-tries.mjs +6 -4
  69. package/src/home/hooks/order-gate.mjs +27 -19
  70. package/src/home/hooks/reply-guard.mjs +2 -7
  71. package/src/home/hooks/reply-parser.mjs +1 -1
  72. package/src/home/hooks/worktree-witness.mjs +6 -6
  73. package/src/home/lib/agents.mjs +1 -1
  74. package/src/home/lib/atomic-json.mjs +24 -6
  75. package/src/home/lib/command-path.mjs +32 -0
  76. package/src/home/lib/config-edit.mjs +25 -10
  77. package/src/home/lib/dispatcher-command.mjs +4 -2
  78. package/src/home/lib/dispatcher-state.mjs +25 -10
  79. package/src/home/lib/file-lock.mjs +41 -10
  80. package/src/home/lib/handback-witness.mjs +7 -4
  81. package/src/home/lib/home-registry.mjs +211 -0
  82. package/src/home/lib/home-write.mjs +189 -0
  83. package/src/home/lib/hook-diagnostics.mjs +20 -0
  84. package/src/home/lib/host-observations.mjs +7 -4
  85. package/src/home/lib/kernel-lock.mjs +171 -0
  86. package/src/home/lib/meta/advice-status.mjs +2 -2
  87. package/src/home/lib/meta/advice-verdict.mjs +30 -1
  88. package/src/home/lib/meta/chain.mjs +19 -8
  89. package/src/home/lib/meta/context-only.mjs +43 -0
  90. package/src/home/lib/meta/events.mjs +16 -3
  91. package/src/home/lib/meta/launch-rows.mjs +23 -0
  92. package/src/home/lib/meta/paths.mjs +24 -24
  93. package/src/home/lib/meta/reply.mjs +37 -3
  94. package/src/home/lib/meta/run-snapshots.mjs +29 -0
  95. package/src/home/lib/meta/run-state.mjs +3 -0
  96. package/src/home/lib/meta/snapshot-format.mjs +96 -0
  97. package/src/home/lib/meta/verdict.mjs +30 -24
  98. package/src/home/lib/required-inputs.mjs +48 -8
  99. package/src/home/lib/run-codex.mjs +6 -3
  100. package/src/home/lib/run-config.mjs +10 -1
  101. package/src/home/lib/runner/advise-carry.mjs +2 -1
  102. package/src/home/lib/runner/args.mjs +16 -5
  103. package/src/home/lib/runner/attach.mjs +21 -14
  104. package/src/home/lib/runner/build-evidence.mjs +29 -0
  105. package/src/home/lib/runner/codex-availability.mjs +50 -0
  106. package/src/home/lib/runner/codex-cmd.mjs +2 -19
  107. package/src/home/lib/runner/continuation.mjs +60 -8
  108. package/src/home/lib/runner/exit-codes.mjs +13 -0
  109. package/src/home/lib/runner/flag-baseline.mjs +105 -0
  110. package/src/home/lib/runner/flag-scan.mjs +165 -0
  111. package/src/home/lib/runner/git-paths.mjs +74 -0
  112. package/src/home/lib/runner/git-state.mjs +37 -66
  113. package/src/home/lib/runner/launcher.mjs +233 -273
  114. package/src/home/lib/runner/order-claim.mjs +84 -0
  115. package/src/home/lib/runner/order-owner.mjs +18 -0
  116. package/src/home/lib/runner/pass-gate.mjs +109 -0
  117. package/src/home/lib/runner/preflight.mjs +22 -18
  118. package/src/home/lib/runner/prompts.mjs +42 -4
  119. package/src/home/lib/runner/question-kind.mjs +41 -0
  120. package/src/home/lib/runner/run-env.mjs +2 -1
  121. package/src/home/lib/runner/sandbox-probe.mjs +1 -1
  122. package/src/home/lib/runner/schemas.mjs +4 -1
  123. package/src/home/lib/runner/scope-check.mjs +55 -28
  124. package/src/home/lib/runner/task-input.mjs +11 -2
  125. package/src/home/lib/runner/worker-admission.mjs +43 -0
  126. package/src/home/lib/runner/worker.mjs +19 -18
  127. package/src/home/lib/task-header.mjs +127 -0
  128. package/src/home/lib/write-meta.mjs +17 -5
package/CHANGELOG.md CHANGED
@@ -4,6 +4,166 @@ 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.10] - 2026-10-04
8
+
9
+ ### Added
10
+
11
+ - `UNAVAILABLE` is a status of its own, with exit code `5`. Before any quota is spent, a bounded
12
+ `codex --version` and `codex login status` tell a missing or signed-out Codex from an unclear answer.
13
+ Only a missing or signed-out Codex refuses as `UNAVAILABLE`, without a run folder. After the start,
14
+ only a measured sample (a final `401 Unauthorized` turn failure) ends a run `UNAVAILABLE`. Any other
15
+ error stays `FAIL` with its raw text, so a lost subscription is never mistaken for a failed task.
16
+ - A failed pass can be repeated once: `retry: <failed run> — <reason>` in the task header, with `--continue`.
17
+ The retry is accepted only for the last run of the chain, with a finished non-`OK` verdict and a worker
18
+ proven dead. It keeps the order id, agent, phase and continuation base, and does not spend another
19
+ continuation of the order. The reply says `Attempt: N of this pass`. Refusals met at a failed last run
20
+ print a ready `retry:` line.
21
+ - One order id admits one paid worker. A launcher takes a kernel-held claim on the project's run store and
22
+ the exact order id (named pipe on Windows, abstract socket on Linux). It holds the claim until its worker is
23
+ registered, and the worker admits itself under the same claim before Codex starts. A second launch of the
24
+ same order attaches to the first run instead of paying again. A claim that stays busy for 60 seconds is a
25
+ free refusal, never a second start. On 2026-09-24 two launchers of one order started 2.8 s apart and
26
+ both paid.
27
+ - A scout question can be marked `[context-only]` by its orderer. Such questions are about startup
28
+ context and pass without commands. An answer must cite `startup:<source>`, and a repository `path:line`
29
+ read by no command fails as an unread address. `questions.json` records `{id, text, kind}`.
30
+ - The build reply adds a `Flags coverage: incomplete` line when the start content of a file could not be
31
+ judged. The status stays as it is.
32
+
33
+ ### Changed
34
+
35
+ - Every runner exit code comes from one table, and a guard ties it to the docs and the prompts:
36
+ `0` OK, `1` FAIL, `2` usage error, `3` LIMIT, `4` still running or no run (`--no-wait`), `5` UNAVAILABLE.
37
+ - Service lines of a task file live only in its header: consecutive `advice:`, `continue:` and `retry:`
38
+ lines from the first line. One parser serves the runner, the task hash and the `order-gate` hook. The task
39
+ hash covers the body only, and new runs record `task_hash_scheme: 2`. A label line below the header that
40
+ looks like metadata is refused for free, with its line number and the repair. **Orchestrators that write
41
+ `advice:` further down a build task must move it to the top.**
42
+ - TODO, FIXME, skip and only flags judge only the lines a build run added. Before the run, the start content
43
+ of every dirty and untracked file is recorded, and only that run's additions are compared. Before this
44
+ change, a run was accused of markers it found in the tree.
45
+ - A scope refusal names the flag the pattern came from (`--scope` or `--scope-new`). When git cannot list
46
+ the folder the run started in, the refusal says so instead of claiming a tracked file does not exist.
47
+
48
+ ### Fixed
49
+
50
+ - A grant line no longer changes the task hash, so a run named in `continue:` is found again under a new
51
+ order id (it used to answer "the current last run is none").
52
+ - An order owned by a run from before the task header is refused truthfully, not as a different task.
53
+ - `codex-bridge read` on a run refused before Codex started now names that refusal. It no longer says
54
+ the run predates the event stream.
55
+ - A reply no longer repeats its verdict (`OK — OK —`) when the model began its own summary with one.
56
+ - An unparsable scope pattern is named in its refusal instead of printing `undefined`.
57
+
58
+ ## [0.6.9] - 2026-10-02
59
+
60
+ ### Added
61
+
62
+ - `uninstall --purge` can remove operator data and the home folder after one preflight under the lifecycle lock:
63
+ no live runs, a readable installation record and no other recorded owners, before permissions, hooks or files change.
64
+ A live run is named with its stop command; an unreadable run folder refuses because it cannot be proven idle.
65
+ - Purge asks separately whether the host inventory is complete and whether it may remove the listed operator data,
66
+ because a record cannot prove that nobody else uses the home and inventory consent is not deletion consent.
67
+ Without a terminal both answers are no; a refusal or cancellation changes nothing.
68
+ - `uninstall --purge --dry-run` reports every refusal together and lists removals under
69
+ `If you confirm both questions:`, so an inspection never implies consent. It takes no lock and asks no questions;
70
+ refusals and unsafe entries exit 1, while the two pending confirmations alone do not.
71
+ - The "Uninstall and purge" section in `docs/overview.md` explains removal and its limits, because a run can start
72
+ during purge and Node cannot prevent an ancestor being swapped between its check and removal.
73
+ - The installation record keeps one owner per host sharing the home, because installing a second host used to erase
74
+ the first host's record. A new home starts with a complete inventory; an old record cannot prove all its owners.
75
+ - Interactive install and update ask once whether this host is the home's only owner when migrating an old record
76
+ or recording a pre-existing image, because those transitions leave other users unknown. Uninstall asks again when
77
+ the last known owner of an incomplete inventory leaves; no keeps the image, and Ctrl+C cancels before changes.
78
+ - `doctor` prints `host files` and `home owners` lines showing package files and hooks, files uninstall would keep,
79
+ recorded hosts and inventory completeness, because "not installed" had hidden files in an unrecorded host.
80
+ - The `doctor` line `owners in sync` names hosts verified against an older shared image and gives their update commands,
81
+ because a warning printed only during install was easily lost. It reports recorded verification, not a fresh
82
+ inspection of those hosts, and an incomplete inventory never reads as all in sync.
83
+ - Install, update and uninstall take the home's lifecycle lock before their first read, because concurrent hosts
84
+ could interleave image copies and record writes. Windows uses a named pipe and Linux an abstract socket released
85
+ by the kernel when the holder dies; macOS uses a lock file requiring explicit recovery. Dry runs take no lock.
86
+ - `codex-bridge unlock --lifecycle` shows whether the home is free, held or unverified, with the holder's command,
87
+ host and pid when available, because a busy-home refusal previously pointed to a command that did not exist.
88
+ - `codex-bridge unlock --lifecycle --clear` removes only a proven-dead holder's macOS lock file, because automatic
89
+ takeover could remove a live or replacement holder's lock. Windows and Linux have no lock file to clear.
90
+ - Every command and its actions answer `-h` and `--help` from one command registry, because separate usage text had
91
+ omitted model actions and subcommands refused help. A flag that needs a value still refuses `--host --help`.
92
+ - The model table ends with `codex-bridge model set <role> <model> [effort]` and `codex-bridge model list`, because
93
+ the profiles alone did not tell the operator how to change one.
94
+
95
+ ### Changed
96
+
97
+ - Install and update bring other recorded hosts' package files and hooks along with the shared image, even when the
98
+ initiating host is already current, because those hosts had kept old agents without the guards the new image needs.
99
+ A host left behind is named with its update command and makes the command exit 1.
100
+ - Other hosts' edited, missing or linked files remain conflicts that the initiating host's `--force` cannot override,
101
+ because replacing the shared image must not erase another host's edits. Synchronization leaves permissions, seeds
102
+ and Codex rules ownership alone; unreachable host roots are named rather than created.
103
+ - `uninstall` removes only the departing host's owner entry while other hosts still use the home, because removing
104
+ the whole record and image had broken their installations. The shared image goes only after the last owner's
105
+ hooks are gone and the inventory is complete or the operator confirms nobody else uses it.
106
+ - The installer and uninstall touch only declared files in the package home, because undeclared writes and copied
107
+ removal lists had left files that removal could not account for.
108
+ - Ordinary `uninstall` and its dry run use one home removal plan, and the real summary counts actual outcomes and
109
+ distinguishes an owner removed from a record removed, because planned removals were reported as completed even
110
+ when files stayed. Folders are removed only when empty, never recursively.
111
+ - Ordinary `uninstall` names each operator file it keeps for `--purge`, such as `config.json` and `conventions.md`,
112
+ because a hand-written preservation sentence missed files. Runs in `codex-runs` remain outside both uninstall
113
+ and purge.
114
+ - Advisor default budgets are now 10 minutes for `scope` and 25 minutes for `advise`, because measured advise runs
115
+ approached or exceeded the previous 15-minute deadline.
116
+ - Worktree snapshots use format v2 with exact file names, because quoted and trimmed legacy names could change the
117
+ verdict. A legacy clean snapshot still reads as clean; missing, damaged or incompatible snapshot pairs fail the
118
+ verdict rather than count as a clean tree, including a run started before and judged after this format update.
119
+
120
+ ### Fixed
121
+
122
+ - `uninstall` recognizes host files and hooks by the package's marks and deletes files only when content matches
123
+ that host's recorded fingerprint or the current package, because edited agents were lost and unrecorded hosts
124
+ could not be cleaned safely. Edited, unknown and seed files, links and unreadable entries are kept and named.
125
+ - Host hooks are removed before host files, and a host whose hooks cannot be detached keeps its files, rules
126
+ ownership, record entry and shared image, because a broken `settings.json` had left hooks pointing at removed files.
127
+ - Edited image files stay and are named by uninstall and purge unless their content proves they are package files,
128
+ because recorded paths alone had authorized deletion. Links are never crossed or removed; unreadable entries are
129
+ kept, and a link, inspection error or removal failure exits 1 instead of claiming complete removal.
130
+ - Purge re-checks the home after removal, names files that appeared with `Appeared during purge:` and exits 1,
131
+ because a run started during purge can recreate a file after it was removed.
132
+ - Repeating `uninstall` for a home with no recorded owners asks whether anyone still uses it, because declining
133
+ removal once had left a home that the next uninstall incorrectly called "not installed".
134
+ - A migrated old record is no longer treated as every unrecorded host's own installation, because update and
135
+ uninstall had operated on another host's files. Install or update can record an unrecorded host from its
136
+ package marks.
137
+ - Install and update refresh the host's stale image verification even when its files are current, because the
138
+ "up to date" exit had left the `doctor` warning in place after the prescribed update.
139
+ - Command usage refusals point to the command's `-h`, including inside the JSON error from `prune --json`,
140
+ because malformed arguments had failed without showing where to find the correct syntax.
141
+ - `model speed` refuses dash arguments before reading config or fetching the catalogue and points to
142
+ `model speed -h`, because `model speed build --fast` had treated the flag as a tier identifier.
143
+ - Advisor prose containing clock times, ratios or host:port no longer fails as a file citation, because `00:05`
144
+ had been read as a file and line number. Invented paths and citations outside the allowed files still fail.
145
+ - Advisor recommendations can contain full text and paragraphs, because hidden length and line-break limits had
146
+ cut answers mid-citation or caused broken text during decoding. Remaining array bounds are stated in the prompt.
147
+ - Broken advisor text now fails with a named contract error, and reply previews preserve whole characters,
148
+ because a NUL-containing answer had passed and UTF-16 shortening could split an emoji.
149
+ - An advisor `scope` with `--continue` is refused before starting and asks for a new order id, because continuing
150
+ scope spent the order's only continuation and made its advise phase unreachable.
151
+ - Scope checks, snapshots and verdicts read Git file names byte-exact, so Cyrillic and other non-ASCII names work,
152
+ because Git's quoted output had refused valid scopes and judged in-scope edits outside them. Invalid UTF-8 names
153
+ fail instead of being silently replaced, and scope patterns with line breaks are refused before starting.
154
+ - An untracked file edit that keeps the same size now appears in the snapshot comparison, because recording size
155
+ alone had made such edits invisible; files that disappear or cannot be read are recorded as missing or unreadable.
156
+ - An unreadable `config.json` fails with its path and reason in the runner and `doctor`, because permission errors
157
+ or a directory in its place had silently selected default models and budgets. Only a missing file uses defaults.
158
+ - `prune` checks links from the trusted root down and keeps the target on any inspection error, because checking
159
+ from the leaf up had crossed links first and treated unreadable ancestors as permission to delete.
160
+
161
+ After `npm i -g @lyupro/codex-bridge@0.6.9`, run `codex-bridge update` once to migrate the installation record.
162
+ An interactive update asks once whether this host is the home's only owner; answer it, because it decides whether a
163
+ later uninstall may remove the shared image. Every host sharing the home should run 0.6.9: older CLI copies do not take
164
+ the lifecycle lock. `codex-bridge doctor` from 0.6.8 cannot read a record written by 0.6.9 and reports
165
+ `[fail] installation: broken record`; installing 0.6.9 clears that report.
166
+
7
167
  ## [0.6.8] - 2026-09-25
8
168
 
9
169
  ### 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 **1667 automated tests: 1666 passing and 1 skipped**.
65
+ The current suite contains **2848 automated tests: 2847 passing and 1 skipped**.
66
66
 
67
67
  ## Verify
68
68
 
@@ -129,17 +129,19 @@ Transport files from runs at least **30 days** old are pruned automatically when
129
129
  | `install [--scope user\|project] [--host <path>] [--dry-run] [--force]` | Install into a Claude Code host. |
130
130
  | `update [--scope user\|project] [--host <path>] [--dry-run] [--force]` | Refresh a recorded installation. |
131
131
  | `permissions [add\|remove] [--scope user\|project] [--host <path>]` | Inspect or manage optional shell rules. |
132
- | `uninstall [--scope user\|project] [--host <path>] [--dry-run]` | Remove recorded package files while preserving run artifacts. |
132
+ | `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
133
  | `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
134
  | `run <runner options> --task-file <abs path>` | Start or attach to a delegated run through the permission-stable package command. The task file carries the statement, the scout questions and the verification command; its path must be absolute. Piping the statement on stdin is the alternative channel, and the two cannot be combined. |
135
135
  | `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
136
  | `projects [<name>] [--json]` | List projects or runs in the run store. |
137
137
  | `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. |
138
- | `unlock [<project>\|--all]` | Close records whose runner is gone. |
138
+ | `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). |
139
139
  | `read <run>` | Render a run's structured event stream. |
140
140
  | `stop <run>` | Stop a run and record a `FAIL` verdict. |
141
141
 
142
- Both `codex-bridge` and `codexb` invoke the same command. Run `codex-bridge --help` for exact forms.
142
+ Both `codex-bridge` and `codexb` invoke the same command. Run `codex-bridge --help` for exact forms, or `-h` after
143
+ any command or action for its own usage (`codex-bridge model -h`, `codex-bridge model set -h`); a refusal of an unknown
144
+ option or an extra argument ends with that pointer.
143
145
 
144
146
  ### Development
145
147
 
@@ -1,89 +1,33 @@
1
1
  #!/usr/bin/env node
2
- /** Dispatches codex-bridge CLI arguments to focused command modules. */
3
- import { diagnose, renderDoctor } from '../cli/doctor.mjs';
4
- import { probeContract } from '../cli/probe-contract.mjs';
5
- import { resolveProbeTarget } from '../cli/probe-target.mjs';
6
- import { brandStateDir } from '../src/home/lib/brand-home.mjs';
2
+ /** Dispatches codex-bridge CLI arguments through the command registry (Plan_71 D1). */
7
3
  import { isInvokedDirectly } from '../cli/invoked-directly.mjs';
8
- import { hook } from '../cli/hook.mjs';
9
- import { resolveHost } from '../cli/hosts.mjs';
10
- import { install } from '../cli/install.mjs';
11
- import { model } from '../cli/model.mjs';
12
- import { projects } from '../cli/projects.mjs';
13
- import { read } from '../cli/read.mjs';
14
- import { runCodex } from '../cli/run-launcher.mjs';
15
4
  import { packageInfo } from '../cli/manifest.mjs';
16
- import { permissions } from '../cli/permissions.mjs';
17
- import { prune } from '../cli/prune.mjs';
18
- import { stop } from '../cli/stop.mjs';
19
- import { unlock } from '../cli/unlock.mjs';
20
- import { uninstall } from '../cli/uninstall.mjs';
21
- import { update } from '../cli/update.mjs';
22
-
23
- export const HELP = `codex-bridge — Claude Code dispatchers for Codex
24
-
25
- Usage:
26
- codex-bridge install [--scope user|project] [--host <path>] [--dry-run] [--force]
27
- codex-bridge update [--scope user|project] [--host <path>] [--dry-run] [--force]
28
- codex-bridge permissions [add|remove] [--scope user|project] [--host <path>]
29
- codex-bridge uninstall [--scope user|project] [--host <path>] [--dry-run]
30
- codex-bridge doctor [--scope user|project] [--host <path>] [--probe-contract] [--probe-executable <path>]
31
- codex-bridge run <runner options> --task-file <path>
32
- codex-bridge model [list]
33
- codex-bridge projects [<name>] [--json]
34
- codex-bridge prune <project> [<run>] [--purge] [--older-than <age>] [-f] [--json]
35
- codex-bridge prune --all-projects [--older-than <age>] [-f] [--json]
36
- codex-bridge unlock [<project>|--all]
37
- codex-bridge read <run>
38
- codex-bridge stop <run>
39
- codex-bridge hook <name>
40
- codexb <same command forms as codex-bridge>
41
- codex-bridge --help
42
- codex-bridge --version
43
-
44
- Commands:
45
- install Install codex-bridge into the selected Claude Code host
46
- update Update a recorded codex-bridge installation
47
- permissions Show, add, or remove optional shell permission rules
48
- uninstall Remove installed files while preserving run artifacts
49
- doctor Diagnose the selected Claude Code host (--probe-contract measures on a live host)
50
- run Start or attach to a delegated Codex run
51
- model Show machine-wide model profiles or list the live catalogue
52
- projects List projects or runs from the run store
53
- prune Remove archived transport, or purge selected run folders
54
- unlock Close running records whose runner is gone
55
- read Render a run's structured event stream
56
- stop Stop a running Codex run and record FAIL
57
-
58
- Hook dispatch:
59
- codex-bridge hook <name> Dispatch a registered guard with stdin unchanged`;
60
-
61
- const HOOK_COMMAND = 'hook';
62
-
63
- export function commandOptions(command, argv) {
64
- const options = {};
65
- const booleanFlags = command === 'install' || command === 'update' ? new Set(['--dry-run', '--force'])
66
- : command === 'uninstall' ? new Set(['--dry-run'])
67
- : command === 'doctor' ? new Set(['--probe-contract']) : new Set();
68
- const flagNames = new Map([
69
- ['--dry-run', 'dryRun'],
70
- ['--force', 'force'],
71
- ['--probe-contract', 'probeContract'],
72
- ]);
73
- for (let index = 0; index < argv.length; index += 1) {
74
- const arg = argv[index];
75
- if (booleanFlags.has(arg)) {
76
- options[flagNames.get(arg)] = true;
77
- continue;
78
- }
79
- if (arg !== '--scope' && arg !== '--host' && !(command === 'doctor' && arg === '--probe-executable')) throw new Error(`unknown ${command} option "${arg}"`);
80
- const value = argv[index + 1];
81
- if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`);
82
- options[arg === '--scope' ? 'scope' : arg === '--host' ? 'host' : 'probeExecutable'] = value;
83
- index += 1;
84
- }
85
- return options;
86
- }
5
+ import { COMMANDS } from '../cli/command-registry.mjs';
6
+ import { helpRequest, renderCommandHelp } from '../cli/command-help.mjs';
7
+
8
+ export { commandOptions } from '../cli/command-registry.mjs';
9
+
10
+ const publicCommands = COMMANDS.filter((entry) => entry.section === 'public');
11
+ const commandWidth = Math.max(...publicCommands.map((entry) => entry.name.length));
12
+ export const HELP = [
13
+ 'codex-bridge — Claude Code dispatchers for Codex',
14
+ '',
15
+ 'Usage:',
16
+ ...publicCommands.flatMap((entry) => [entry, ...Object.values(entry.actions ?? {})]
17
+ .flatMap((section) => section.usage.map((line) => ` ${line}`))),
18
+ ' codexb <same command forms as codex-bridge>',
19
+ ' codex-bridge --help',
20
+ ' codex-bridge --version',
21
+ '',
22
+ 'Commands:',
23
+ ...publicCommands.map((entry) => ` ${entry.name.padEnd(commandWidth)} ${entry.summary}`),
24
+ '',
25
+ 'Hook dispatch:',
26
+ ...COMMANDS.filter((entry) => entry.section === 'machine')
27
+ .flatMap((entry) => entry.usage.map((line) => ` ${line} ${entry.summary}`)),
28
+ '',
29
+ 'Run codex-bridge <command> -h for one command.',
30
+ ].join('\n');
87
31
 
88
32
  export async function main(argv, io = console) {
89
33
  const [command, ...rest] = argv;
@@ -95,103 +39,17 @@ export async function main(argv, io = console) {
95
39
  io.log((await packageInfo()).version);
96
40
  return 0;
97
41
  }
98
- if (command === 'run') return runCodex(rest);
99
- if (command === 'doctor') {
100
- const options = commandOptions(command, rest);
101
- if (options.probeExecutable && !options.probeContract) throw new Error('--probe-executable requires --probe-contract');
102
- const host = resolveHost(options);
103
- // The probe runs BEFORE the diagnosis so the `hostContract` line below carries the verdict just
104
- // measured. Printing the diagnosis first and the measurement after would answer one question
105
- // twice in one output, with the older answer on top (Plan_52 D25).
106
- let probe = null;
107
- if (options.probeContract) {
108
- const target = resolveProbeTarget({ stateDir: brandStateDir(host.brandRoot), executable: options.probeExecutable });
109
- if (!target.error) io.log(`probe: target ${target.version} at ${target.executable} (${target.source})`);
110
- probe = await probeContract({ host, target });
111
- io.log(`probe: ${probe.message}`);
112
- for (const [name, verdict] of Object.entries(probe.dispatcher ?? {})) {
113
- io.log(`probe: ${name} ${verdict.result} — ${verdict.detail}`);
114
- }
115
- }
116
- const result = await diagnose({ host });
117
- io.log(renderDoctor(result));
118
- // An inconclusive probe wrote nothing and measured nothing; exiting 0 would let a failed
119
- // measurement pass silently in a script (Plan_52 D26).
120
- return probe && probe.state === 'inconclusive' ? 2 : result.exitCode;
121
- }
122
- if (command === 'stop') {
123
- if (rest.length !== 1) {
124
- io.error('codex-bridge stop requires exactly one run folder (full path or bare name).');
125
- return 2;
126
- }
127
- const result = await stop({ run: rest[0] });
128
- io.log(result.output);
129
- return result.exitCode;
130
- }
131
- if (command === HOOK_COMMAND) return hook(rest, io);
132
- if (command === 'read') {
133
- if (rest.length !== 1) {
134
- io.error('codex-bridge read requires exactly one run folder (full path or bare name).');
135
- return 2;
136
- }
137
- const result = read({ run: rest[0] });
138
- io.log(result.output);
139
- return result.exitCode;
140
- }
141
- if (command === 'projects') {
142
- const result = projects(rest);
143
- io.log(result.output);
144
- return result.exitCode;
145
- }
146
- if (command === 'model') {
147
- const result = await model(rest);
148
- io.log(result.output);
149
- return result.exitCode;
150
- }
151
- if (command === 'prune') {
152
- const result = await prune(rest);
153
- io.log(result.output);
154
- return result.exitCode;
155
- }
156
- if (command === 'sweep') {
157
- io.error('codex-bridge sweep was renamed to codex-bridge unlock; use the new command.');
42
+ const entry = COMMANDS.find((candidate) => candidate.name === command);
43
+ if (!entry) {
44
+ io.error(`codex-bridge: unknown command "${command}"\nRun codex-bridge --help for usage.`);
158
45
  return 2;
159
46
  }
160
- if (command === 'unlock') {
161
- const result = unlock(rest);
162
- io.log(result.output);
163
- return result.exitCode;
164
- }
165
- if (command === 'permissions') {
166
- let action;
167
- let optionArgs = rest;
168
- if (rest[0] && !rest[0].startsWith('-')) {
169
- action = rest[0];
170
- optionArgs = rest.slice(1);
171
- }
172
- if (action && !['add', 'remove'].includes(action)) {
173
- throw new Error(`unknown permissions action "${action}"`);
174
- }
175
- const options = commandOptions(command, optionArgs);
176
- const host = resolveHost(options);
177
- const result = await permissions({ host, action });
178
- io.log(result.output);
179
- return result.exitCode;
180
- }
181
- if (command === 'install' || command === 'update' || command === 'uninstall') {
182
- const options = commandOptions(command, rest);
183
- const host = resolveHost(options);
184
- const handlers = {
185
- install: () => install({ host, dryRun: options.dryRun, force: options.force }),
186
- update: () => update({ host, dryRun: options.dryRun, force: options.force }),
187
- uninstall: () => uninstall({ host, dryRun: options.dryRun }),
188
- };
189
- const result = await handlers[command]();
190
- io.log(result.output);
191
- return result.exitCode;
47
+ const asked = helpRequest(rest, Object.keys(entry.actions ?? {}));
48
+ if (asked && entry.section !== 'renamed') {
49
+ io.log(renderCommandHelp(entry, asked.action));
50
+ return 0;
192
51
  }
193
- io.error(`codex-bridge: unknown command "${command}"\nRun codex-bridge --help for usage.`);
194
- return 2;
52
+ return entry.handler(rest, io);
195
53
  }
196
54
 
197
55
  if (isInvokedDirectly(process.argv[1], import.meta.url)) {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Decides what a help request is and how one command's help reads (Plan_71 D1).
3
+ * `codex-bridge model -h` answered "unexpected argument" and no subcommand answered -h at all
4
+ * (2026-09-29), so the operator could not learn how to set a model. Only exact argv shapes count
5
+ * (advice A1): a scan for --help would turn `--host --help` — today a "requires a value" refusal —
6
+ * into a help answer and hide the parser's error.
7
+ */
8
+
9
+ export function helpRequest(argv, actions = []) {
10
+ const isHelp = (value) => value === '-h' || value === '--help';
11
+ if (argv.length === 1 && isHelp(argv[0])) return { action: null };
12
+ if (argv.length === 2 && actions.includes(argv[0]) && isHelp(argv[1])) {
13
+ return { action: argv[0] };
14
+ }
15
+ return null;
16
+ }
17
+
18
+ export function withCommandHelp(command, message) {
19
+ const hint = `Run codex-bridge ${command} -h for usage.`;
20
+ if (message === hint || message.endsWith(`\n${hint}`)) return message;
21
+ return `${message}\n${hint}`;
22
+ }
23
+
24
+ export function renderCommandHelp(entry, action = null) {
25
+ const prefix = `codex-bridge ${entry.name}`;
26
+ const actions = Object.entries(entry.actions ?? {});
27
+ // Registry typos must fail even when the malformed action is not the requested one.
28
+ for (const section of [entry, ...actions.map(([, details]) => details)]) {
29
+ for (const line of section.usage) {
30
+ if (line !== prefix && !line.startsWith(`${prefix} `)) {
31
+ throw new Error(`Help usage must start with "${prefix}": ${line}`);
32
+ }
33
+ }
34
+ }
35
+
36
+ let section = entry;
37
+ if (action !== null) {
38
+ const match = actions.find(([name]) => name === action);
39
+ if (!match) throw new Error(`Unknown help action for ${entry.name}: ${action}`);
40
+ section = match[1];
41
+ }
42
+ const lines = ['Usage:', ...section.usage.map((line) => ` ${line}`), '', section.summary];
43
+ if (action === null && entry.actions) {
44
+ const width = Math.max(...actions.map(([name]) => name.length));
45
+ lines.push('', 'Actions:', ...actions.map(([name, details]) => ` ${name.padEnd(width)} ${details.summary}`));
46
+ lines.push(`Run codex-bridge ${entry.name} <action> -h for one action.`);
47
+ }
48
+ return lines.join('\n');
49
+ }