@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.
- package/CHANGELOG.md +160 -0
- package/README.md +6 -4
- package/bin/codex-bridge.mjs +35 -177
- package/cli/command-help.mjs +49 -0
- package/cli/command-registry.mjs +284 -0
- package/cli/copy.mjs +42 -5
- package/cli/dispatcher-contract-record.mjs +3 -2
- package/cli/doctor-host-side.mjs +110 -0
- package/cli/doctor.mjs +17 -1
- package/cli/home-inspection.mjs +122 -0
- package/cli/home-plan-execute.mjs +140 -0
- package/cli/home-removal-plan.mjs +61 -0
- package/cli/hook-recognizer.mjs +78 -0
- package/cli/host-contract.mjs +11 -5
- package/cli/host-inspection.mjs +156 -0
- package/cli/host-removal.mjs +91 -0
- package/cli/image-evidence.mjs +44 -0
- package/cli/install-owners.mjs +244 -0
- package/cli/install-record.mjs +71 -9
- package/cli/install.mjs +88 -75
- package/cli/inventory-removal.mjs +84 -0
- package/cli/inventory-transition.mjs +66 -0
- package/cli/lifecycle-lock-clear.mjs +104 -0
- package/cli/lifecycle-lock-inspect.mjs +92 -0
- package/cli/lifecycle-lock.mjs +222 -0
- package/cli/lifecycle-transaction.mjs +57 -0
- package/cli/link-segments.mjs +45 -0
- package/cli/model-set.mjs +4 -3
- package/cli/model-speed.mjs +9 -2
- package/cli/model.mjs +5 -3
- package/cli/owner-record.mjs +40 -0
- package/cli/owner-sync-apply.mjs +108 -0
- package/cli/owner-sync.mjs +115 -0
- package/cli/projects.mjs +5 -2
- package/cli/prune-args.mjs +9 -4
- package/cli/prune.mjs +23 -72
- package/cli/purge-live-runs.mjs +98 -0
- package/cli/purge-preflight.mjs +103 -0
- package/cli/read.mjs +22 -1
- package/cli/record-removal.mjs +43 -0
- package/cli/removal-outcomes.mjs +115 -0
- package/cli/removal-plan.mjs +187 -0
- package/cli/remove-layout.mjs +40 -1
- package/cli/rules-owners.mjs +4 -2
- package/cli/settings-merge.mjs +3 -29
- package/cli/terminal-question.mjs +41 -0
- package/cli/uninstall-purge.mjs +139 -0
- package/cli/uninstall-rules.mjs +72 -0
- package/cli/uninstall.mjs +208 -127
- package/cli/unlock-lifecycle.mjs +102 -0
- package/cli/unlock.mjs +7 -4
- package/cli/update.mjs +102 -27
- package/package.json +4 -3
- package/scripts/home-write-audit.mjs +286 -0
- package/scripts/home-write-inventory.mjs +59 -0
- package/scripts/home-write-lexer.mjs +182 -0
- package/scripts/isolated-roots.mjs +6 -1
- package/scripts/open-work/check.mjs +180 -0
- package/scripts/open-work/live-thresholds.mjs +155 -0
- package/scripts/open-work/register.mjs +56 -0
- package/scripts/root-guard/root-entries.mjs +15 -0
- package/scripts/run-tests.mjs +26 -1
- package/src/agents/codex-advisor.md +23 -10
- package/src/agents/codex-build.md +24 -14
- package/src/agents/codex-review.md +17 -10
- package/src/agents/codex-scout.md +22 -12
- package/src/home/conventions.md +9 -0
- package/src/home/hooks/guard-tries.mjs +6 -4
- package/src/home/hooks/order-gate.mjs +27 -19
- package/src/home/hooks/reply-guard.mjs +2 -7
- package/src/home/hooks/reply-parser.mjs +1 -1
- package/src/home/hooks/worktree-witness.mjs +6 -6
- package/src/home/lib/agents.mjs +1 -1
- package/src/home/lib/atomic-json.mjs +24 -6
- package/src/home/lib/command-path.mjs +32 -0
- package/src/home/lib/config-edit.mjs +25 -10
- package/src/home/lib/dispatcher-command.mjs +4 -2
- package/src/home/lib/dispatcher-state.mjs +25 -10
- package/src/home/lib/file-lock.mjs +41 -10
- package/src/home/lib/handback-witness.mjs +7 -4
- package/src/home/lib/home-registry.mjs +211 -0
- package/src/home/lib/home-write.mjs +189 -0
- package/src/home/lib/hook-diagnostics.mjs +20 -0
- package/src/home/lib/host-observations.mjs +7 -4
- package/src/home/lib/kernel-lock.mjs +171 -0
- package/src/home/lib/meta/advice-status.mjs +2 -2
- package/src/home/lib/meta/advice-verdict.mjs +30 -1
- package/src/home/lib/meta/chain.mjs +19 -8
- package/src/home/lib/meta/context-only.mjs +43 -0
- package/src/home/lib/meta/events.mjs +16 -3
- package/src/home/lib/meta/launch-rows.mjs +23 -0
- package/src/home/lib/meta/paths.mjs +24 -24
- package/src/home/lib/meta/reply.mjs +37 -3
- package/src/home/lib/meta/run-snapshots.mjs +29 -0
- package/src/home/lib/meta/run-state.mjs +3 -0
- package/src/home/lib/meta/snapshot-format.mjs +96 -0
- package/src/home/lib/meta/verdict.mjs +30 -24
- package/src/home/lib/required-inputs.mjs +48 -8
- package/src/home/lib/run-codex.mjs +6 -3
- package/src/home/lib/run-config.mjs +10 -1
- package/src/home/lib/runner/advise-carry.mjs +2 -1
- package/src/home/lib/runner/args.mjs +16 -5
- package/src/home/lib/runner/attach.mjs +21 -14
- package/src/home/lib/runner/build-evidence.mjs +29 -0
- package/src/home/lib/runner/codex-availability.mjs +50 -0
- package/src/home/lib/runner/codex-cmd.mjs +2 -19
- package/src/home/lib/runner/continuation.mjs +60 -8
- package/src/home/lib/runner/exit-codes.mjs +13 -0
- package/src/home/lib/runner/flag-baseline.mjs +105 -0
- package/src/home/lib/runner/flag-scan.mjs +165 -0
- package/src/home/lib/runner/git-paths.mjs +74 -0
- package/src/home/lib/runner/git-state.mjs +37 -66
- package/src/home/lib/runner/launcher.mjs +233 -273
- package/src/home/lib/runner/order-claim.mjs +84 -0
- package/src/home/lib/runner/order-owner.mjs +18 -0
- package/src/home/lib/runner/pass-gate.mjs +109 -0
- package/src/home/lib/runner/preflight.mjs +22 -18
- package/src/home/lib/runner/prompts.mjs +42 -4
- package/src/home/lib/runner/question-kind.mjs +41 -0
- package/src/home/lib/runner/run-env.mjs +2 -1
- package/src/home/lib/runner/sandbox-probe.mjs +1 -1
- package/src/home/lib/runner/schemas.mjs +4 -1
- package/src/home/lib/runner/scope-check.mjs +55 -28
- package/src/home/lib/runner/task-input.mjs +11 -2
- package/src/home/lib/runner/worker-admission.mjs +43 -0
- package/src/home/lib/runner/worker.mjs +19 -18
- package/src/home/lib/task-header.mjs +127 -0
- 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 **
|
|
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
|
|
package/bin/codex-bridge.mjs
CHANGED
|
@@ -1,89 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/** Dispatches codex-bridge CLI arguments
|
|
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 {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export const HELP =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
codex-bridge
|
|
31
|
-
codex-bridge
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
99
|
-
if (
|
|
100
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
io.log(
|
|
163
|
-
return
|
|
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
|
-
|
|
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
|
+
}
|