@lyupro/codex-bridge 0.5.0 → 0.5.2
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 +104 -1
- package/README.md +62 -20
- package/bin/codex-bridge.mjs +4 -0
- package/cli/doctor.mjs +118 -19
- package/cli/frontmatter.mjs +137 -0
- package/cli/hook.mjs +2 -2
- package/cli/install-record.mjs +2 -2
- package/cli/install.mjs +14 -2
- package/cli/manifest.mjs +23 -22
- package/cli/permissions.mjs +2 -2
- package/cli/projects.mjs +3 -3
- package/cli/prune-plan.mjs +2 -2
- package/cli/prune.mjs +1 -1
- package/cli/read.mjs +2 -2
- package/cli/rules-owners.mjs +26 -4
- package/cli/run-lookup.mjs +3 -3
- package/cli/runs-inventory.mjs +3 -3
- package/cli/settings-merge.mjs +1 -1
- package/cli/stop.mjs +5 -5
- package/cli/unlock.mjs +7 -7
- package/package.json +2 -2
- package/scripts/check-file-size.mjs +1 -1
- package/src/agents/codex-build.md +265 -231
- package/src/agents/codex-review.md +219 -184
- package/src/agents/codex-scout.md +232 -200
- package/src/{hooks → home/hooks}/guard-tries.mjs +1 -1
- package/src/{hooks → home/hooks}/live-runs.mjs +43 -3
- package/src/home/hooks/order-gate.mjs +135 -0
- package/src/{hooks → home/hooks}/prune-guard.mjs +2 -2
- package/src/{hooks → home/hooks}/reply-guard.mjs +117 -97
- package/src/home/hooks/reply-parser.mjs +27 -0
- package/src/home/hooks/reply-verdicts.mjs +174 -0
- package/src/{hooks → home/hooks}/stop-guard.mjs +3 -3
- package/src/{hooks → home/hooks}/worktree-lock.mjs +3 -3
- package/src/home/lib/dispatcher-order.mjs +36 -0
- package/src/{hook-definitions.mjs → home/lib/hook-definitions.mjs} +1 -1
- package/src/home/lib/host-refusal.mjs +20 -0
- package/src/{json-file.mjs → home/lib/json-file.mjs} +4 -4
- package/src/{meta → home/lib/meta}/paths.mjs +20 -1
- package/src/{meta → home/lib/meta}/reply.mjs +1 -1
- package/src/{meta → home/lib/meta}/verdict.mjs +1 -1
- package/src/{no-self-execution.mjs → home/lib/no-self-execution.mjs} +7 -2
- package/src/{required-inputs.mjs → home/lib/required-inputs.mjs} +56 -5
- package/src/{retention.mjs → home/lib/retention.mjs} +1 -1
- package/src/{run-codex.mjs → home/lib/run-codex.mjs} +39 -11
- package/src/{run-config.mjs → home/lib/run-config.mjs} +2 -2
- package/src/{runner → home/lib/runner}/args.mjs +86 -10
- package/src/{runner → home/lib/runner}/attach.mjs +58 -9
- package/src/{runner → home/lib/runner}/launcher.mjs +33 -21
- package/src/home/lib/runner/order-owner.mjs +28 -0
- package/src/{runner → home/lib/runner}/run-env.mjs +1 -1
- package/src/home/lib/runner/task-file.mjs +92 -0
- package/src/home/lib/runner/task-input.mjs +26 -0
- package/src/{runner → home/lib/runner}/worker.mjs +1 -1
- package/src/home/lib/shell-unsafe.mjs +26 -0
- package/src/{write-meta.mjs → home/lib/write-meta.mjs} +12 -1
- package/src/hooks/order-gate.mjs +0 -73
- /package/src/{run-config.json → home/config.json} +0 -0
- /package/src/{conventions.md → home/conventions.md} +0 -0
- /package/src/{cli-names.mjs → home/lib/cli-names.mjs} +0 -0
- /package/src/{heartbeat.mjs → home/lib/heartbeat.mjs} +0 -0
- /package/src/{meta → home/lib/meta}/chain.mjs +0 -0
- /package/src/{meta → home/lib/meta}/deadline.mjs +0 -0
- /package/src/{meta → home/lib/meta}/environment.mjs +0 -0
- /package/src/{meta → home/lib/meta}/events.mjs +0 -0
- /package/src/{meta → home/lib/meta}/outcome.mjs +0 -0
- /package/src/{meta → home/lib/meta}/pre-start.mjs +0 -0
- /package/src/{meta → home/lib/meta}/reason.mjs +0 -0
- /package/src/{meta → home/lib/meta}/run-state.mjs +0 -0
- /package/src/{meta → home/lib/meta}/startup.mjs +0 -0
- /package/src/{meta → home/lib/meta}/transport.mjs +0 -0
- /package/src/{process-identity.mjs → home/lib/process-identity.mjs} +0 -0
- /package/src/{runner → home/lib/runner}/codex-cmd.mjs +0 -0
- /package/src/{runner → home/lib/runner}/continuation.mjs +0 -0
- /package/src/{runner → home/lib/runner}/conventions.mjs +0 -0
- /package/src/{runner → home/lib/runner}/git-state.mjs +0 -0
- /package/src/{runner → home/lib/runner}/project-dir.mjs +0 -0
- /package/src/{runner → home/lib/runner}/prompts.mjs +0 -0
- /package/src/{runner → home/lib/runner}/run-context.mjs +0 -0
- /package/src/{runner → home/lib/runner}/runs-root.mjs +0 -0
- /package/src/{runner → home/lib/runner}/schemas.mjs +0 -0
- /package/src/{runner → home/lib/runner}/scope-check.mjs +0 -0
- /package/src/{stop-contract.mjs → home/lib/stop-contract.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,110 @@ 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
|
-
## [
|
|
7
|
+
## [0.5.2] - 2026-08-16
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- The guard punished the very refusal the package demands. A dispatcher whose command the host
|
|
12
|
+
denies is told by its own prompt to answer `FAIL`, name its order id and name `codex-bridge
|
|
13
|
+
install`; the guard knew only `RUN=` and `ATTACH=`, so that honest answer read as "no run
|
|
14
|
+
exists" and was blocked three times before the exhausted try budget let it through. A probe on
|
|
15
|
+
2026-08-16 with `Bash(codex-bridge:*)` denied spent 31.5k tokens on an answer worth ten, and the
|
|
16
|
+
counter recorded `{"state": 3}` against it. A host refusal is now a recognised shape beside the
|
|
17
|
+
two run lines: a reply that declares failure, names its order id and names the remedy passes on
|
|
18
|
+
the first try, before any check that looks for a folder on disk. A reply that declares failure and
|
|
19
|
+
names the remedy but omits the order id is blocked with that exact part named — the prompt asked
|
|
20
|
+
for it in prose, and prose is a request a model can silently skip.
|
|
21
|
+
|
|
22
|
+
- No Claude Code host had registered the three dispatchers since 2026-08-10. The stop-guidance
|
|
23
|
+
sentence added that day — `` before `TaskStop`: TaskStop removes the wrapper … `` — was
|
|
24
|
+
substituted into the `description:` line of each agent's frontmatter, and an unquoted YAML scalar
|
|
25
|
+
cannot contain a colon followed by a space: the frontmatter did not parse, so the agents did not
|
|
26
|
+
exist as far as the host was concerned. `doctor` reported every file present the whole time,
|
|
27
|
+
because it counted files instead of reading them. Installation now re-emits frontmatter as valid
|
|
28
|
+
YAML — any value a reader would not take as a plain scalar is quoted, the sentence survives
|
|
29
|
+
verbatim — `doctor` parses each installed definition and checks its `name`, and the suite parses
|
|
30
|
+
everything the installer would write.
|
|
31
|
+
|
|
32
|
+
- A run ordered under another run's order id is no longer answered with that run's verdict. On
|
|
33
|
+
2026-08-15 a build ordered as `plan42-run3` carried the id of `plan42-run2`, and the runner
|
|
34
|
+
printed run2's `OK`, run2's file list and run2's suite over a worktree where nothing had been
|
|
35
|
+
done — only a manual `git status` stopped a commit of work that did not exist. An order id names
|
|
36
|
+
one task: when the id already belongs to a run whose task text differs, the runner now refuses
|
|
37
|
+
with exit code `2` before printing anything, naming the folder that owns the id and the two
|
|
38
|
+
remedies (a new `--order-id`, or `--continue`). Repeats of the same task, continuations, and runs
|
|
39
|
+
recorded before the `task_hash` field attach exactly as before.
|
|
40
|
+
- The same collision is now refused one step earlier and checked one step later. `order-gate` denies
|
|
41
|
+
the dispatcher call itself when the ordered id already belongs to a run with a different task —
|
|
42
|
+
before any quota is spent — and `reply-guard` blocks a reply whose run records a different
|
|
43
|
+
`order_id` than the one the orchestrator ordered, read from the dispatcher's own transcript. Both
|
|
44
|
+
read the single definition of "the same task" the runner uses; a second copy of that rule would
|
|
45
|
+
drift silently, which is the failure this change exists to end. Every uncertain disk state — an
|
|
46
|
+
unreadable task file, a missing runs directory, a run folder written before its `status.json` —
|
|
47
|
+
passes, because a guard nobody can predict is a guard everyone works around.
|
|
48
|
+
- Delegating to a dispatcher no longer stops on a permission prompt. The shipped agent definitions
|
|
49
|
+
started a run by absolute path — `node "<installed dir>/run-codex.mjs" --agent … \ …` — and a host
|
|
50
|
+
matches permission rules against the beginning of a command line, so a rule written for this
|
|
51
|
+
package could never match it. The only way to start a run is now the package command,
|
|
52
|
+
`codex-bridge run`, on a single line: no interpreter, no path, no line continuation. The task
|
|
53
|
+
statement travels as `--task-file`, because a heredoc forces the invocation back into the
|
|
54
|
+
multi-line form that no rule can cover; stdin remains supported for the direct call, and giving
|
|
55
|
+
both channels is a refusal rather than a silent precedence.
|
|
56
|
+
- Installed hooks could not run at all. The installer reshaped the source tree — hooks to one
|
|
57
|
+
directory, their modules to another — so every hook's relative import was correct in the clone and
|
|
58
|
+
false on the host, and all five guards died on `ERR_MODULE_NOT_FOUND` from the day the package
|
|
59
|
+
moved to its branded folder. The suite stayed green because it exercises the clone, and `doctor`
|
|
60
|
+
called a hook healthy on the strength of a line in `settings.json` without ever starting the file.
|
|
61
|
+
The source now contains a literal image of the installed home, `src/home/`, and installation
|
|
62
|
+
copies it one to one.
|
|
63
|
+
- The reply guard no longer sends a refused dispatcher down the one road the host cannot allow.
|
|
64
|
+
Seven of its block texts said "Run run-codex.mjs and return its stdout verbatim" — the sentence
|
|
65
|
+
removed from every prompt in this release, because a command line naming a file can match no
|
|
66
|
+
permission rule. They were the worst place for it to survive: a dispatcher reads them at the one
|
|
67
|
+
moment it is looking for what to do next, having just had its answer rejected. All seven name
|
|
68
|
+
`codex-bridge run`. The invariant behind them was the actual defect and is now stated as a rule
|
|
69
|
+
rather than as a shape: no line of code in a hook may name the runner file, whole-line comments
|
|
70
|
+
excepted. The earlier expression-bound check could not see the text a session is shown when its
|
|
71
|
+
turn is ended, which is where the same sentence would have survived a fourth cleanup.
|
|
72
|
+
- `doctor` no longer calls a host healthy when an installed agent definition is not this package's.
|
|
73
|
+
Drift was advisory — a message, exit code 0 — while an unreadable definition was already fatal. A
|
|
74
|
+
checklist run planted the pre-0.5.2 invocation, a file path instead of the package command, in an
|
|
75
|
+
installed agent: `doctor` printed the drift and still exited 0, so a script or a CI step would
|
|
76
|
+
have called that host healthy while every delegation on it stopped at a permission prompt. Any
|
|
77
|
+
definition that differs from the package now exits 1 and names `codex-bridge update --force`.
|
|
78
|
+
|
|
79
|
+
### Added
|
|
80
|
+
|
|
81
|
+
- `codex-bridge run` — the public entry point for starting a delegated run, with the same flags,
|
|
82
|
+
stdout and exit codes as the historical file invocation.
|
|
83
|
+
- Both lines that identify a run to its caller carry the order they belong to:
|
|
84
|
+
`RUN=<path> order-id=<id>` and `ATTACH=<path> order-id=<id> started=<time>`. A reply naming a run
|
|
85
|
+
cannot be checked against the order that was placed unless the id travels with the folder.
|
|
86
|
+
- Two structural guards that make the above regressions loud instead of silent: the install plan
|
|
87
|
+
must stay isomorphic to the shipped home image, and every registered hook must actually start as
|
|
88
|
+
a child process with its imports resolved.
|
|
89
|
+
- `doctor` compares the version answering a hook against the installed copy, verifies the installed
|
|
90
|
+
agent definitions against the repository, and downgrades to a warning instead of reporting code it
|
|
91
|
+
never ran as healthy.
|
|
92
|
+
|
|
93
|
+
## [0.5.1] - 2026-08-12
|
|
94
|
+
|
|
95
|
+
### Fixed
|
|
96
|
+
|
|
97
|
+
- Installing or uninstalling no longer fails over a rules-registry lock that is busy for one more
|
|
98
|
+
millisecond. Windows answers a taken lock with three codes, and only one of them is `EEXIST`: a
|
|
99
|
+
lock file whose delete is still pending comes back `EPERM`, and a file another process still
|
|
100
|
+
holds comes back `EBUSY` — the acquisition loop treated both as fatal instead of waiting. All
|
|
101
|
+
three now mean "not yours yet", and the decision lives in one exported check, `isLockTaken()`.
|
|
102
|
+
|
|
103
|
+
### Changed
|
|
104
|
+
|
|
105
|
+
- The README's first screen names the problem the package solves, keeps the install command inside
|
|
106
|
+
the first 30 lines, adds a second badge row, and shows `doctor` output from a healthy host in
|
|
107
|
+
place of a screenshot.
|
|
108
|
+
- The reference documentation under `docs/` is English-only now (`overview.ru.md` became
|
|
109
|
+
`overview.md`), and the working folders — plans, checklists and audits — left the repository and
|
|
110
|
+
its history: a fresh clone contains the five reference documents and nothing else.
|
|
8
111
|
|
|
9
112
|
## [0.5.0] - 2026-08-11
|
|
10
113
|
|
package/README.md
CHANGED
|
@@ -6,29 +6,14 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- **Invalid work is rejected before quota is spent.** Scope, order, retry, branch, and continuation checks run before a run folder is created.
|
|
13
|
-
- **Verdicts come from artifacts.** CLI events, git state, and the declared report decide the result, not model claims about itself.
|
|
14
|
-
- **Identical retries attach safely.** Repeating the same command joins the active run instead of starting another one.
|
|
15
|
-
- **5 host guards enforce the boundary.** `reply-guard`, `order-gate`, `worktree-lock`, `prune-guard`, and `stop-guard` cover replies, orders, edits, deletion, and stopping.
|
|
16
|
-
- **Installation is identifiable and reversible.** Writes to `settings.json` are merged and backed up; uninstall removes recorded package files only.
|
|
17
|
-
- **Every run leaves an audit folder.** The verbatim task, scope, before/after git state, events, report, verdict, and reason remain together.
|
|
18
|
-
- **Zero runtime dependencies and zero build steps.** The package is plain `.mjs` on the Node.js standard library.
|
|
19
|
-
|
|
20
|
-
The current suite contains **603 automated tests: 602 passing and 1 skipped**.
|
|
9
|
+
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
10
|
+
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
11
|
+
[](package.json)
|
|
21
12
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- Node.js >= 24
|
|
25
|
-
- An authenticated [Codex CLI](https://developers.openai.com/codex/cli): `codex --version` and `codex login`
|
|
26
|
-
- Claude Code, which hosts the agents, commands, and guards
|
|
13
|
+
Agentic coding spends one model subscription on everything, and implementation is the expensive part. codex-bridge moves that work to a separate [Codex CLI](https://developers.openai.com/codex/cli) subscription: Claude Code plans, dispatches and accepts, Codex executes — and every delegated run is guarded, verified and audited on disk.
|
|
27
14
|
|
|
28
15
|
## Install
|
|
29
16
|
|
|
30
|
-
For a one-off user installation, global access, or a clone entry point:
|
|
31
|
-
|
|
32
17
|
```bash
|
|
33
18
|
npm i -g @lyupro/codex-bridge # puts both command names on PATH
|
|
34
19
|
codex-bridge install # into ~/.claude and ~/.lyupro/.codex-bridge/
|
|
@@ -37,6 +22,10 @@ codexb install # the short name, same command
|
|
|
37
22
|
|
|
38
23
|
Two other entry points reach the same command: `npx @lyupro/codex-bridge install` for a one-off run without a global install, and `node bin/codex-bridge.mjs install` from a clone. Every command below works through all three.
|
|
39
24
|
|
|
25
|
+
Shipped agents invoke delegated runs through `codex-bridge run`. This is a permission contract,
|
|
26
|
+
not a style choice: the stable package command lets host permission rules match the invocation
|
|
27
|
+
without exposing an installation-specific absolute path and prompting on every delegation.
|
|
28
|
+
|
|
40
29
|
Use `--scope project` to install the Claude Code-facing files under `<repo>/.claude`; the default user scope uses `~/.claude`.
|
|
41
30
|
|
|
42
31
|
Installation has two roots:
|
|
@@ -44,19 +33,71 @@ Installation has two roots:
|
|
|
44
33
|
- `~/.lyupro/.codex-bridge/` contains package runtime files, guards, configuration, conventions, and the installation record.
|
|
45
34
|
- The selected Claude Code root (`~/.claude/` or `<repo>/.claude/`) contains the three agents, two slash commands, merged hook registrations in `settings.json`, and run artifacts under `codex-runs/`.
|
|
46
35
|
|
|
36
|
+
The repository's `src/home/` directory is the literal image copied into the branded root: its
|
|
37
|
+
`hooks/`, `lib/`, `config.json`, and `conventions.md` paths are the host paths without remapping.
|
|
38
|
+
Only npm's `package.json` is copied into that root from outside the image.
|
|
39
|
+
|
|
47
40
|
Before every `settings.json` write, the existing file is backed up. Existing settings and foreign hooks are preserved. `--dry-run` prints the plan without writing.
|
|
48
41
|
|
|
49
42
|
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.
|
|
50
43
|
|
|
51
44
|
`codex-runs/` contains operator data. Uninstall never removes it.
|
|
52
45
|
|
|
46
|
+
## Requirements
|
|
47
|
+
|
|
48
|
+
- Node.js >= 24
|
|
49
|
+
- An authenticated [Codex CLI](https://developers.openai.com/codex/cli): `codex --version` and `codex login`
|
|
50
|
+
- Claude Code, which hosts the agents, commands, and guards
|
|
51
|
+
|
|
52
|
+
## What it gives you
|
|
53
|
+
|
|
54
|
+
- **Runs survive the caller.** A detached worker keeps the run and its artifacts alive after the dispatcher exits.
|
|
55
|
+
- **Invalid work is rejected before quota is spent.** Scope, order, retry, branch, and continuation checks run before a run folder is created.
|
|
56
|
+
- **Verdicts come from artifacts.** CLI events, git state, and the declared report decide the result, not model claims about itself.
|
|
57
|
+
- **Identical retries attach safely.** Repeating the same command joins the active run instead of starting another one.
|
|
58
|
+
- **5 host guards enforce the boundary.** `reply-guard`, `order-gate`, `worktree-lock`, `prune-guard`, and `stop-guard` cover replies, orders, edits, deletion, and stopping.
|
|
59
|
+
- **Installation is identifiable and reversible.** Writes to `settings.json` are merged and backed up; uninstall removes recorded package files only.
|
|
60
|
+
- **Every run leaves an audit folder.** The verbatim task, scope, before/after git state, events, report, verdict, and reason remain together.
|
|
61
|
+
- **Zero runtime dependencies and zero build steps.** The package is plain `.mjs` on the Node.js standard library.
|
|
62
|
+
|
|
63
|
+
The current suite contains **705 automated tests: 704 passing and 1 skipped**.
|
|
64
|
+
|
|
53
65
|
## Verify
|
|
54
66
|
|
|
55
67
|
```bash
|
|
56
68
|
codex-bridge doctor
|
|
57
69
|
```
|
|
58
70
|
|
|
59
|
-
|
|
71
|
+
Output on a healthy host (trimmed):
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
[ok] host: ~/.claude (user, exists)
|
|
75
|
+
[ok] installation: @lyupro/codex-bridge@0.5.1 (matches package)
|
|
76
|
+
[ok] files: 53 installed file(s) present
|
|
77
|
+
[ok] rules: ~/.codex/rules/codex-bridge.rules (matches record)
|
|
78
|
+
[ok] permissions: installed (24/24 own strings in allow/deny)
|
|
79
|
+
[ok] hook:SubagentStop: matcher * -> ~/.lyupro/.codex-bridge/hooks/reply-guard.mjs
|
|
80
|
+
[ok] hook:PreToolUse: matcher Agent|Task -> ~/.lyupro/.codex-bridge/hooks/order-gate.mjs
|
|
81
|
+
[ok] hook:PreToolUse: matcher Write|Edit|MultiEdit|NotebookEdit -> ~/.lyupro/.codex-bridge/hooks/worktree-lock.mjs
|
|
82
|
+
[ok] hook:PreToolUse: matcher Bash|PowerShell -> ~/.lyupro/.codex-bridge/hooks/prune-guard.mjs
|
|
83
|
+
[ok] hook:PreToolUse: matcher TaskStop -> ~/.lyupro/.codex-bridge/hooks/stop-guard.mjs
|
|
84
|
+
[warn] retention: Automatic cleanup is ON — run transport older than 30 days is removed to reclaim disk space.
|
|
85
|
+
[ok] conventions: ~/.lyupro/.codex-bridge/conventions.md (found)
|
|
86
|
+
[ok] codex: codex-cli 0.146.1
|
|
87
|
+
[ok] node: 24.18.0 (requires >=24)
|
|
88
|
+
[ok] runsRoot: ~/.claude/codex-runs
|
|
89
|
+
[ok] liveRuns: 0 runs working right now
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`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. 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.
|
|
93
|
+
|
|
94
|
+
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:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
codex-bridge run --agent codex-build --repo <repository> --order-id <id> --scope <globs> --task-file <absolute path>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The trade-off is explicit: `order-gate` and `reply-guard` sit on the dispatcher call, so a direct run is unguarded and its result is judged by the worktree and the suite.
|
|
60
101
|
|
|
61
102
|
## Update
|
|
62
103
|
|
|
@@ -86,6 +127,7 @@ Transport files from runs at least **30 days** old are pruned automatically when
|
|
|
86
127
|
| `permissions [add\|remove] [--scope user\|project] [--host <path>]` | Inspect or manage optional shell rules. |
|
|
87
128
|
| `uninstall [--scope user\|project] [--host <path>] [--dry-run]` | Remove recorded package files while preserving run artifacts. |
|
|
88
129
|
| `doctor [--scope user\|project] [--host <path>]` | Diagnose the selected host and Codex connection. |
|
|
130
|
+
| `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. |
|
|
89
131
|
| `projects [<name>] [--json]` | List projects or runs in the run store. |
|
|
90
132
|
| `prune <project> [<run>] [--purge] [--older-than <age>] [-f] [--json]` | Plan or perform operator-confirmed cleanup. |
|
|
91
133
|
| `unlock [<project>\|--all]` | Close records whose runner is gone. |
|
package/bin/codex-bridge.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { resolveHost } from '../cli/hosts.mjs';
|
|
|
7
7
|
import { install } from '../cli/install.mjs';
|
|
8
8
|
import { projects } from '../cli/projects.mjs';
|
|
9
9
|
import { read } from '../cli/read.mjs';
|
|
10
|
+
import { runCodexCommand } from '../src/home/lib/run-codex.mjs';
|
|
10
11
|
import { packageInfo } from '../cli/manifest.mjs';
|
|
11
12
|
import { permissions } from '../cli/permissions.mjs';
|
|
12
13
|
import { prune } from '../cli/prune.mjs';
|
|
@@ -23,6 +24,7 @@ Usage:
|
|
|
23
24
|
codex-bridge permissions [add|remove] [--scope user|project] [--host <path>]
|
|
24
25
|
codex-bridge uninstall [--scope user|project] [--host <path>] [--dry-run]
|
|
25
26
|
codex-bridge doctor [--scope user|project] [--host <path>]
|
|
27
|
+
codex-bridge run <runner options> --task-file <path>
|
|
26
28
|
codex-bridge projects [<name>] [--json]
|
|
27
29
|
codex-bridge prune <project> [<run>] [--purge] [--older-than <age>] [-f] [--json]
|
|
28
30
|
codex-bridge prune --all-projects [--older-than <age>] [-f] [--json]
|
|
@@ -40,6 +42,7 @@ Commands:
|
|
|
40
42
|
permissions Show, add, or remove optional shell permission rules
|
|
41
43
|
uninstall Remove installed files while preserving run artifacts
|
|
42
44
|
doctor Diagnose the selected Claude Code host
|
|
45
|
+
run Start or attach to a delegated Codex run
|
|
43
46
|
projects List projects or runs from the run store
|
|
44
47
|
prune Remove archived transport, or purge selected run folders
|
|
45
48
|
unlock Close running records whose runner is gone
|
|
@@ -84,6 +87,7 @@ export async function main(argv, io = console) {
|
|
|
84
87
|
io.log((await packageInfo()).version);
|
|
85
88
|
return 0;
|
|
86
89
|
}
|
|
90
|
+
if (command === 'run') return runCodexCommand(rest);
|
|
87
91
|
if (command === 'doctor') {
|
|
88
92
|
const host = resolveHost(commandOptions(command, rest));
|
|
89
93
|
const result = await diagnose({ host });
|
package/cli/doctor.mjs
CHANGED
|
@@ -2,22 +2,26 @@
|
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import {
|
|
7
|
+
buildInstallPlan,
|
|
8
|
+
contentFingerprint,
|
|
7
9
|
fileFingerprint,
|
|
8
10
|
HOOK_DEFINITIONS,
|
|
9
11
|
readInstallRecord,
|
|
10
12
|
packageInfo,
|
|
11
13
|
} from './manifest.mjs';
|
|
14
|
+
import { plannedContent } from './copy.mjs';
|
|
12
15
|
import { recordTarget } from './install-record.mjs';
|
|
13
16
|
import { inspectPermissions } from './permissions.mjs';
|
|
14
17
|
import { commandFor, inspectHook } from './settings-merge.mjs';
|
|
15
18
|
import { readRulesRegistry } from './rules-owners.mjs';
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { parseFrontmatter } from './frontmatter.mjs';
|
|
20
|
+
import { readRunConfig, retentionNotice } from '../src/home/lib/run-config.mjs';
|
|
21
|
+
import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
|
|
22
|
+
import { resolveProjectRunsDir } from '../src/home/lib/runner/project-dir.mjs';
|
|
23
|
+
import { allLiveRuns } from '../src/home/hooks/live-runs.mjs';
|
|
24
|
+
import { STOP_COMMAND_TEMPLATE } from '../src/home/lib/stop-contract.mjs';
|
|
21
25
|
|
|
22
26
|
const WARNING = '\u001b[33m';
|
|
23
27
|
const RESET = '\u001b[0m';
|
|
@@ -39,6 +43,44 @@ async function isFile(target) {
|
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
async function agentsCheck(host, record) {
|
|
47
|
+
if (!record) return check('agents', 'warn', 'Agent definitions were not checked; run codex-bridge install');
|
|
48
|
+
const plan = await buildInstallPlan(host);
|
|
49
|
+
const agents = plan.filter((item) => /[\\/]src[\\/]agents[\\/][^\\/]+\.md$/.test(item.source));
|
|
50
|
+
const mismatches = [];
|
|
51
|
+
const unreadable = [];
|
|
52
|
+
for (const item of agents) {
|
|
53
|
+
const expected = contentFingerprint(await plannedContent(item, host.brandRoot));
|
|
54
|
+
if (await fileFingerprint(item.target) !== expected) mismatches.push(path.basename(item.target));
|
|
55
|
+
const expectedName = path.basename(item.source, '.md');
|
|
56
|
+
try {
|
|
57
|
+
const frontmatter = parseFrontmatter(await fs.readFile(item.target, 'utf8'));
|
|
58
|
+
if (!frontmatter) throw new Error('frontmatter is missing');
|
|
59
|
+
if (frontmatter.name !== expectedName) {
|
|
60
|
+
throw new Error(`name is ${JSON.stringify(frontmatter.name)}, expected ${JSON.stringify(expectedName)}`);
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
unreadable.push(`${path.basename(item.target)} (${err.message})`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// The 2026-08-10 registration failure left files present but unreadable by Claude Code. Drift was
|
|
67
|
+
// advisory until a 2026-08-16 checklist run planted the pre-Plan_41 invocation — a path instead of
|
|
68
|
+
// the package command — in an installed definition: doctor kept exit 0 while every delegation
|
|
69
|
+
// would stop on a permission prompt. A definition that is not this package's is not judged healthy.
|
|
70
|
+
if (unreadable.length) {
|
|
71
|
+
return check('agents', 'fail', `Installed agent definitions cannot be read: ${unreadable.join(', ')}; run codex-bridge update --force`);
|
|
72
|
+
}
|
|
73
|
+
return mismatches.length
|
|
74
|
+
? check('agents', 'fail', `Installed agent definitions differ from this package: ${mismatches.join(', ')}; run codex-bridge update --force`)
|
|
75
|
+
: check('agents', 'ok', `${agents.length} installed agent definition(s) match this package`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function bridgeCommandCheck(result) {
|
|
79
|
+
return result.available
|
|
80
|
+
? check('command', 'ok', `codex-bridge resolves on PATH (${result.value})`)
|
|
81
|
+
: check('command', 'warn', `codex-bridge does not resolve on PATH (${result.value}); run npm i -g @lyupro/codex-bridge`);
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
function check(key, status, value) {
|
|
43
85
|
return { key, status, value };
|
|
44
86
|
}
|
|
@@ -74,15 +116,22 @@ async function conventionsCheck(host) {
|
|
|
74
116
|
* the host as seen by THIS copy. An operator comparing two diagnoses has no other way to tell them
|
|
75
117
|
* apart.
|
|
76
118
|
*/
|
|
77
|
-
function
|
|
119
|
+
function packageSource() {
|
|
78
120
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
79
|
-
|
|
80
|
-
|
|
121
|
+
return {
|
|
122
|
+
root,
|
|
123
|
+
kind: root.split(/[\\/]/).includes('node_modules') ? 'installed copy' : 'clone',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sourceCheck() {
|
|
128
|
+
const source = packageSource();
|
|
129
|
+
return check('source', 'ok', `${source.root} (${source.kind === 'installed copy' ? 'installed package' : source.kind})`);
|
|
81
130
|
}
|
|
82
131
|
|
|
83
132
|
/**
|
|
84
133
|
* The runner asks git for the repository root before it picks a runs folder, so doctor has to
|
|
85
|
-
* ask the same question: run from `src/runner`, a plain cwd would name the folder `runner` and
|
|
134
|
+
* ask the same question: run from `src/home/lib/runner`, a plain cwd would name the folder `runner` and
|
|
86
135
|
* report a location no run will ever use.
|
|
87
136
|
*/
|
|
88
137
|
function repoRoot(cwd) {
|
|
@@ -137,16 +186,52 @@ export function probeCodexBridge() {
|
|
|
137
186
|
return { available: true, value: (result.stdout || result.stderr).trim() };
|
|
138
187
|
}
|
|
139
188
|
|
|
140
|
-
function hookVersion(command, record,
|
|
189
|
+
function hookVersion(command, record, bridgeResult) {
|
|
141
190
|
if (command.startsWith('codex-bridge hook ')) {
|
|
142
|
-
|
|
143
|
-
|
|
191
|
+
return bridgeResult.available
|
|
192
|
+
? `global command ${bridgeResult.value}`
|
|
193
|
+
: `global command unavailable (${bridgeResult.value})`;
|
|
144
194
|
}
|
|
145
195
|
return `installed copy ${record.name}@${record.version}`;
|
|
146
196
|
}
|
|
147
197
|
|
|
148
|
-
|
|
149
|
-
return
|
|
198
|
+
function probedVersion(result) {
|
|
199
|
+
return result.available ? result.value.match(/(\d+\.\d+\.\d+(?:[-+][^\s]+)?)\s*$/)?.[1] : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function startHook(target) {
|
|
203
|
+
return new Promise((resolve) => {
|
|
204
|
+
const child = spawn(process.execPath, [target], {
|
|
205
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
206
|
+
windowsHide: true,
|
|
207
|
+
});
|
|
208
|
+
let stderr = '';
|
|
209
|
+
child.stderr.setEncoding('utf8');
|
|
210
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
211
|
+
child.on('error', (error) => resolve({ status: null, stderr: error.message }));
|
|
212
|
+
child.on('close', (status) => resolve({ status, stderr }));
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function startFailure(definition, result) {
|
|
217
|
+
if (result.status === 0) return null;
|
|
218
|
+
const lines = result.stderr.trim().split(/\r?\n/);
|
|
219
|
+
const resolver = lines.find((line) => /^Error \[ERR_MODULE_NOT_FOUND\]|Cannot find module/.test(line));
|
|
220
|
+
return `${definition.file} did not start: ${resolver || lines.find(Boolean) || `exit code ${result.status}`}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function hookChecks(host, record, bridgeProbe, ownPackage) {
|
|
224
|
+
let bridgeResult;
|
|
225
|
+
const bridge = () => {
|
|
226
|
+
bridgeResult ??= bridgeProbe();
|
|
227
|
+
return bridgeResult;
|
|
228
|
+
};
|
|
229
|
+
const starts = record ? await Promise.all(HOOK_DEFINITIONS.map(async (definition) => {
|
|
230
|
+
const recorded = record.hooks.find((hook) => hook.event === definition.event
|
|
231
|
+
&& path.basename(hook.path) === definition.file);
|
|
232
|
+
return recorded ? startHook(recordTarget(host, recorded)) : null;
|
|
233
|
+
})) : [];
|
|
234
|
+
return Promise.all(HOOK_DEFINITIONS.map(async (definition, index) => {
|
|
150
235
|
const key = `hook:${definition.event}`;
|
|
151
236
|
if (!record) return check(key, 'warn', 'cannot check before installation');
|
|
152
237
|
// The file, not just the event: PreToolUse carries two hooks of this package, and matching
|
|
@@ -174,11 +259,20 @@ async function hookChecks(host, record, bridgeProbe) {
|
|
|
174
259
|
const reason = form === 'short'
|
|
175
260
|
? 'PATH command uses the globally installed package'
|
|
176
261
|
: 'full path uses the copy placed by the last install';
|
|
177
|
-
|
|
262
|
+
const problems = [startFailure(definition, starts[index])].filter(Boolean);
|
|
263
|
+
const commandVersion = form === 'short' ? bridge() : null;
|
|
264
|
+
const globalVersion = probedVersion(commandVersion || { available: false });
|
|
265
|
+
if (globalVersion && globalVersion !== ownPackage.version) {
|
|
266
|
+
problems.push(`global PATH package version ${globalVersion} differs from ${packageSource().kind} version ${ownPackage.version}`);
|
|
267
|
+
}
|
|
268
|
+
const health = problems.length ? `; ${problems.join('; ')}` : '';
|
|
269
|
+
return check(key, problems.length ? 'warn' : 'ok', `${definition.event} matcher ${definition.matcher} -> ${expected} (${form} command; ${reason}; ${hookVersion(command, record, commandVersion)}${health})`);
|
|
178
270
|
}
|
|
179
271
|
const command = recorded.command || fullCommand;
|
|
180
272
|
const form = command.startsWith('codex-bridge hook ') ? 'short' : 'path';
|
|
181
|
-
|
|
273
|
+
const failure = startFailure(definition, starts[index]);
|
|
274
|
+
const commandVersion = form === 'short' ? bridge() : null;
|
|
275
|
+
return check(key, 'warn', `${definition.event} matcher ${definition.matcher} does not point to the installed ${definition.file} (${form} command; ${hookVersion(command, record, commandVersion)}${failure ? `; ${failure}` : ''})`);
|
|
182
276
|
} catch (err) {
|
|
183
277
|
const value = err.code === 'ENOENT' ? 'settings.json is absent' : `settings.json is invalid: ${err.message}`;
|
|
184
278
|
return check(key, 'warn', `${definition.event} hook: ${value}`);
|
|
@@ -256,10 +350,14 @@ export async function diagnose({ host, codexProbe = probeCodex, bridgeProbe = pr
|
|
|
256
350
|
!record ? 'warn' : missingFiles.length ? 'fail' : 'ok',
|
|
257
351
|
!record ? 'not checked' : missingFiles.length ? `missing: ${missingFiles.join(', ')}` : `${record.files.length} installed file(s) present`,
|
|
258
352
|
));
|
|
353
|
+
const agents = await agentsCheck(host, record);
|
|
354
|
+
checks.push(agents);
|
|
259
355
|
const rules = await rulesCheck(host, record);
|
|
260
356
|
checks.push(rules);
|
|
261
357
|
checks.push(await permissionsCheck(host));
|
|
262
|
-
|
|
358
|
+
const bridge = bridgeProbe();
|
|
359
|
+
checks.push(bridgeCommandCheck(bridge));
|
|
360
|
+
checks.push(...await hookChecks(host, record, () => bridge, ownPackage));
|
|
263
361
|
const retention = retentionCheck(host);
|
|
264
362
|
checks.push(retention);
|
|
265
363
|
const conventions = await conventionsCheck(host);
|
|
@@ -275,7 +373,7 @@ export async function diagnose({ host, codexProbe = probeCodex, bridgeProbe = pr
|
|
|
275
373
|
checks.push(projectRuns);
|
|
276
374
|
|
|
277
375
|
return {
|
|
278
|
-
exitCode: !record || recordBroken || missingFiles.length || rules.status === 'fail'
|
|
376
|
+
exitCode: !record || recordBroken || missingFiles.length || agents.status === 'fail' || rules.status === 'fail'
|
|
279
377
|
|| retention.status === 'fail' || conventions.status === 'fail' || projectRuns.status === 'fail' ? 1 : 0,
|
|
280
378
|
checks,
|
|
281
379
|
record,
|
|
@@ -285,6 +383,7 @@ export async function diagnose({ host, codexProbe = probeCodex, bridgeProbe = pr
|
|
|
285
383
|
|
|
286
384
|
export function renderDoctor(result) {
|
|
287
385
|
return result.checks.map(({ key, status, value }) => {
|
|
386
|
+
if ((key === 'agents' || key === 'command') && status !== 'ok') return value;
|
|
288
387
|
const rendered = `[${status}] ${key}: ${value}`;
|
|
289
388
|
return ['retention', 'conventions', 'permissions', 'liveRuns'].includes(key) && status === 'warn'
|
|
290
389
|
? `${WARNING}${rendered}${RESET}`
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads and normalizes the flat frontmatter shipped by this package.
|
|
3
|
+
*
|
|
4
|
+
* The 2026-08-10 agent-registration incident showed that placeholder text can turn a previously
|
|
5
|
+
* plain YAML value into invalid syntax. Keeping this boundary deliberately flat makes host
|
|
6
|
+
* readability testable without adding a general YAML parser or changing markdown bodies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const PLAIN_START = /^(?:[-?:](?:[ \t]|$)|[,\[\]{}#&*!|>'"%@`])/;
|
|
10
|
+
|
|
11
|
+
function locateFrontmatter(content) {
|
|
12
|
+
const firstBreak = content.indexOf('\n');
|
|
13
|
+
if (firstBreak === -1 || content.slice(0, firstBreak).replace(/\r$/, '') !== '---') return null;
|
|
14
|
+
|
|
15
|
+
const blockStart = firstBreak + 1;
|
|
16
|
+
let lineStart = blockStart;
|
|
17
|
+
while (lineStart <= content.length) {
|
|
18
|
+
const nextBreak = content.indexOf('\n', lineStart);
|
|
19
|
+
const lineEnd = nextBreak === -1 ? content.length : nextBreak;
|
|
20
|
+
if (content.slice(lineStart, lineEnd).replace(/\r$/, '') === '---') {
|
|
21
|
+
return { blockStart, blockEnd: lineStart };
|
|
22
|
+
}
|
|
23
|
+
if (nextBreak === -1) break;
|
|
24
|
+
lineStart = nextBreak + 1;
|
|
25
|
+
}
|
|
26
|
+
throw new Error('frontmatter closing delimiter is missing');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function blockLines(block) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
let start = 0;
|
|
32
|
+
while (start < block.length) {
|
|
33
|
+
const nextBreak = block.indexOf('\n', start);
|
|
34
|
+
const end = nextBreak === -1 ? block.length : nextBreak;
|
|
35
|
+
const hasCarriageReturn = block[end - 1] === '\r';
|
|
36
|
+
lines.push({
|
|
37
|
+
text: block.slice(start, hasCarriageReturn ? end - 1 : end),
|
|
38
|
+
ending: nextBreak === -1 ? '' : hasCarriageReturn ? '\r\n' : '\n',
|
|
39
|
+
});
|
|
40
|
+
if (nextBreak === -1) break;
|
|
41
|
+
start = nextBreak + 1;
|
|
42
|
+
}
|
|
43
|
+
return lines;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function splitEntry(line) {
|
|
47
|
+
const match = /^([A-Za-z0-9_-]+):(.*)$/.exec(line);
|
|
48
|
+
if (!match) throw new Error(`frontmatter line is not a flat key/value entry: ${line}`);
|
|
49
|
+
const remainder = match[2];
|
|
50
|
+
if (remainder && !/^[ \t]/.test(remainder)) {
|
|
51
|
+
throw new Error(`frontmatter value for ${match[1]} must follow YAML separator whitespace`);
|
|
52
|
+
}
|
|
53
|
+
return { key: match[1], rawValue: remainder ? remainder.slice(1) : '' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function decodeDoubleQuoted(value) {
|
|
57
|
+
if (!value.endsWith('"')) return null;
|
|
58
|
+
let decoded = '';
|
|
59
|
+
for (let index = 1; index < value.length - 1; index += 1) {
|
|
60
|
+
const character = value[index];
|
|
61
|
+
if (character === '"') return null;
|
|
62
|
+
if (character !== '\\') {
|
|
63
|
+
decoded += character;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const escaped = value[index + 1];
|
|
67
|
+
if (escaped !== '\\' && escaped !== '"') return null;
|
|
68
|
+
decoded += escaped;
|
|
69
|
+
index += 1;
|
|
70
|
+
}
|
|
71
|
+
return decoded;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function decodeSingleQuoted(value) {
|
|
75
|
+
if (!value.endsWith("'")) return null;
|
|
76
|
+
const inner = value.slice(1, -1);
|
|
77
|
+
if (/(^|[^'])'(?:[^']|$)/.test(inner)) return null;
|
|
78
|
+
return inner.replaceAll("''", "'");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function quotedValue(value) {
|
|
82
|
+
if (value.startsWith('"')) return decodeDoubleQuoted(value);
|
|
83
|
+
if (value.startsWith("'")) return decodeSingleQuoted(value);
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function plainScalarIsSafe(value) {
|
|
88
|
+
return value.length > 0
|
|
89
|
+
&& value === value.trim()
|
|
90
|
+
&& !PLAIN_START.test(value)
|
|
91
|
+
&& !/:[ \t]/.test(value)
|
|
92
|
+
&& !/[ \t]#/.test(value);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseValue(rawValue) {
|
|
96
|
+
const decoded = quotedValue(rawValue);
|
|
97
|
+
if (decoded !== undefined) {
|
|
98
|
+
if (decoded === null) throw new Error('frontmatter contains an invalid quoted value');
|
|
99
|
+
return decoded;
|
|
100
|
+
}
|
|
101
|
+
if (!plainScalarIsSafe(rawValue)) {
|
|
102
|
+
throw new Error(`frontmatter contains an invalid plain scalar: ${rawValue}`);
|
|
103
|
+
}
|
|
104
|
+
return rawValue;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function encodeValue(value) {
|
|
108
|
+
const decoded = quotedValue(value);
|
|
109
|
+
const intended = decoded === undefined || decoded === null ? value : decoded;
|
|
110
|
+
if (decoded === undefined && plainScalarIsSafe(value)) return value;
|
|
111
|
+
return `"${intended.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function parseFrontmatter(content) {
|
|
115
|
+
const bounds = locateFrontmatter(content);
|
|
116
|
+
if (!bounds) return null;
|
|
117
|
+
const values = {};
|
|
118
|
+
for (const { text } of blockLines(content.slice(bounds.blockStart, bounds.blockEnd))) {
|
|
119
|
+
const { key, rawValue } = splitEntry(text);
|
|
120
|
+
if (Object.hasOwn(values, key)) throw new Error(`frontmatter contains duplicate key: ${key}`);
|
|
121
|
+
values[key] = parseValue(rawValue);
|
|
122
|
+
}
|
|
123
|
+
return values;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function normalizeFrontmatter(content) {
|
|
127
|
+
const bounds = locateFrontmatter(content);
|
|
128
|
+
if (!bounds) return content;
|
|
129
|
+
const keys = new Set();
|
|
130
|
+
const normalized = blockLines(content.slice(bounds.blockStart, bounds.blockEnd)).map(({ text, ending }) => {
|
|
131
|
+
const { key, rawValue } = splitEntry(text);
|
|
132
|
+
if (keys.has(key)) throw new Error(`frontmatter contains duplicate key: ${key}`);
|
|
133
|
+
keys.add(key);
|
|
134
|
+
return `${key}: ${encodeValue(rawValue)}${ending}`;
|
|
135
|
+
}).join('');
|
|
136
|
+
return `${content.slice(0, bounds.blockStart)}${normalized}${content.slice(bounds.blockEnd)}`;
|
|
137
|
+
}
|
package/cli/hook.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/** Dispatches a named host hook without changing the guard's stdin or process lifecycle. */
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
-
import { HOOK_DEFINITIONS } from '../src/hook-definitions.mjs';
|
|
4
|
+
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
5
5
|
|
|
6
|
-
const HOOKS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'hooks');
|
|
6
|
+
const HOOKS_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'src', 'home', 'hooks');
|
|
7
7
|
const VALID_NAMES = HOOK_DEFINITIONS.map(({ name }) => name);
|
|
8
8
|
|
|
9
9
|
function validNames() {
|
package/cli/install-record.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Reads, validates, and writes the installation record shared by installer commands. */
|
|
2
2
|
import fs from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { readJsonFile } from '../src/json-file.mjs';
|
|
5
|
-
import { HOOK_DEFINITIONS } from '../src/hook-definitions.mjs';
|
|
4
|
+
import { readJsonFile } from '../src/home/lib/json-file.mjs';
|
|
5
|
+
import { HOOK_DEFINITIONS } from '../src/home/lib/hook-definitions.mjs';
|
|
6
6
|
|
|
7
7
|
export const INSTALL_RECORD_NAME = '.installed.json';
|
|
8
8
|
export const LEGACY_INSTALL_RECORD_NAME = '.codex-bridge-install.json';
|