@lyupro/codex-bridge 0.6.4 → 0.6.5
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 +45 -0
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/agents/codex-advisor.md +187 -0
- package/src/agents/codex-build.md +4 -0
- package/src/commands/env.md +5 -4
- package/src/home/lib/agents.mjs +3 -0
- package/src/home/lib/budgets.mjs +64 -0
- package/src/home/lib/config-validate.mjs +3 -39
- package/src/home/lib/meta/advice-status.mjs +61 -0
- package/src/home/lib/meta/advice-verdict.mjs +226 -0
- package/src/home/lib/meta/advisor-task.mjs +69 -0
- package/src/home/lib/meta/reply.mjs +41 -2
- package/src/home/lib/meta/run-state.mjs +2 -0
- package/src/home/lib/meta/verdict.mjs +7 -0
- package/src/home/lib/required-inputs.mjs +25 -4
- package/src/home/lib/run-codex.mjs +1 -0
- package/src/home/lib/run-config.mjs +5 -1
- package/src/home/lib/runner/args.mjs +1 -0
- package/src/home/lib/runner/codex-args.mjs +4 -3
- package/src/home/lib/runner/codex-cmd.mjs +1 -1
- package/src/home/lib/runner/launcher.mjs +17 -4
- package/src/home/lib/runner/preflight.mjs +75 -1
- package/src/home/lib/runner/prompts.mjs +39 -0
- package/src/home/lib/runner/schemas.mjs +137 -1
- package/src/home/lib/runner/scope-check.mjs +9 -1
- package/src/home/lib/runner/worker-order.mjs +4 -2
- package/src/home/lib/runner/worker.mjs +2 -2
- package/src/home/lib/write-meta.mjs +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,51 @@ 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.5] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- A fourth dispatcher, `codex-advisor`, answers "which option is right" before any code is written.
|
|
12
|
+
The other three answer what is: what the code contains, make the change, what is wrong with a
|
|
13
|
+
finished diff. An orchestrator asked for advice four times in a row only after the fact, each time
|
|
14
|
+
convinced its own design was sound — so the advisor is read-only, runs on the Codex subscription,
|
|
15
|
+
and its answer is judged by the package rather than trusted. It works in two phases of one order:
|
|
16
|
+
`scope` (5 minutes) says whether the listed files are enough and names the missing ones, and
|
|
17
|
+
`advise` (15 minutes) continues that run with `--continue` and picks one option. An insufficient
|
|
18
|
+
scope is an `OK` run with `sufficient: false` and the missing paths in `meta.json`, never `LIMIT`:
|
|
19
|
+
`LIMIT` means the quota is gone and calls for the opposite reaction.
|
|
20
|
+
- Agreement is made structurally hard to express. The task lists options as `- id: description`
|
|
21
|
+
with no preference marker — a task that says which option the orchestrator leans towards is refused
|
|
22
|
+
before the run starts, because an advisor that knows the expected answer agrees with it. The answer
|
|
23
|
+
names exactly one option id (or `none-of-these` with an unlisted option, and then every listed
|
|
24
|
+
option is rejected with its cost), a non-empty `rejected` list with costs, the strongest
|
|
25
|
+
counterargument, assumptions rated `VERIFIED | REASONABLE | FRAGILE`, a pre-mortem whose early
|
|
26
|
+
checks are a test, a command or an inspectable address, and what the advisor checked on its own.
|
|
27
|
+
- Every `path:line` in the advice is checked: it must exist in the worktree, fall within the file,
|
|
28
|
+
and lie inside the listed paths or the paths phase 1 declared missing. The sandbox cannot restrict
|
|
29
|
+
what Codex reads, so the boundary is held on the output — an invented or out-of-scope citation fails
|
|
30
|
+
the run.
|
|
31
|
+
- A run phase is a runner concept, not an advisor detail. `--phase` is checked against the phases the
|
|
32
|
+
role declares, travels through `worker.json`, `status.json` and `meta.json`, and the budget resolves
|
|
33
|
+
by role and phase in one place. `budgets.<role>` in the config accepts a number for single-phase
|
|
34
|
+
roles and a phase map (`{"scope": 5, "advise": 15}`, partial maps allowed) for roles with phases; a
|
|
35
|
+
bare number for a role with phases is refused on reading the config instead of silently erasing
|
|
36
|
+
its phases. Existing configs stay valid without edits.
|
|
37
|
+
- Every `codex-build` task file carries exactly one `advice:` line: the folder of the advisor run
|
|
38
|
+
behind the order, or one of `mechanical | revert | docs-only | test-only` for work that needs no
|
|
39
|
+
second opinion. Anything else is refused before the run starts, and the value lands in `meta.json`,
|
|
40
|
+
so skipped advice is measurable instead of remembered.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- A scope pattern inside a service directory is refused before the run starts. The verdict fails
|
|
45
|
+
every change under `.git/`, `.claude/`, `.codex/`, `.omx/`, `.omc/` and `node_modules/` whatever the
|
|
46
|
+
scope says, but the preflight check let such a pattern through: an order scoped to
|
|
47
|
+
`.claude/context/architecture.md` worked 22 minutes, wrote every file correctly and was failed for
|
|
48
|
+
one of them. The check now uses the verdict's own definition of a service directory, so the gate and
|
|
49
|
+
the judge cannot become two measures of one tree. A glob that can also match ordinary files
|
|
50
|
+
(`**/*.md`) is still accepted.
|
|
51
|
+
|
|
7
52
|
## [0.6.4] - 2026-09-21
|
|
8
53
|
|
|
9
54
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# codex-bridge
|
|
2
2
|
|
|
3
|
-
**Delegate implementation, reconnaissance and
|
|
3
|
+
**Delegate implementation, reconnaissance, review and design advice from Claude Code to Codex — six host guards, zero runtime dependencies, zero build steps.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
6
6
|
[](https://www.npmjs.com/package/@lyupro/codex-bridge)
|
|
@@ -33,7 +33,7 @@ Use `--scope project` to install the Claude Code-facing files under `<repo>/.cla
|
|
|
33
33
|
Installation has two roots:
|
|
34
34
|
|
|
35
35
|
- `~/.lyupro/.codex-bridge/` contains package runtime files, guards, configuration, conventions, and the installation record.
|
|
36
|
-
- The selected Claude Code root (`~/.claude/` or `<repo>/.claude/`) contains the
|
|
36
|
+
- The selected Claude Code root (`~/.claude/` or `<repo>/.claude/`) contains the four agents (`codex-scout`, `codex-build`, `codex-review`, `codex-advisor`), two slash commands, merged hook registrations in `settings.json`, and run artifacts under `codex-runs/`.
|
|
37
37
|
|
|
38
38
|
The repository's `src/home/` directory is the literal image copied into the branded root: its
|
|
39
39
|
`hooks/`, `lib/`, `config.json`, and `conventions.md` paths are the host paths without remapping.
|
|
@@ -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 **1491 automated tests: 1490 passing and 1 skipped**.
|
|
66
66
|
|
|
67
67
|
## Verify
|
|
68
68
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lyupro/codex-bridge",
|
|
3
|
-
"version": "0.6.
|
|
4
|
-
"description": "Claude Code-to-Codex delegation with 6 guards,
|
|
3
|
+
"version": "0.6.5",
|
|
4
|
+
"description": "Claude Code-to-Codex delegation with 6 guards, 1491 tests, zero runtime dependencies, and zero build steps.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
7
7
|
"codex",
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: codex-advisor
|
|
3
|
+
description: Независимое второе мнение по DESIGN до появления кода — что правильно, а не что уже есть; работу выполняет Codex CLI по подписке ChatGPT, строго read-only. Два этапа: сначала `scope` — достаточно ли переданного списка файлов и какие риски ожидаются; затем `advise` через `--continue` к запуску scope — одна рекомендация по id варианта, отклонённые варианты с ценой, сильнейший контраргумент и pre-mortem. Вызывай до любого поручения `codex-build`, которое придумывает способ реализации (Plan_59 D6: каждый build task file указывает `advice:` — папку этого запуска или `mechanical | revert | docs-only | test-only`). Полный результат — в ~/.claude/codex-runs/, в чате — не более пяти строк и путь. {{CODEX_REQUIRED_INPUTS_SUMMARY}} {{CODEX_STOP_SUMMARY}}
|
|
4
|
+
model: haiku
|
|
5
|
+
tools: Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
{{CODEX_NO_SELF_EXECUTION}}
|
|
9
|
+
|
|
10
|
+
You are the advisor dispatcher. One command starts Codex, and its output is your answer.
|
|
11
|
+
|
|
12
|
+
**Why you exist.** The orchestrator runs on a Claude Max subscription, while Codex runs on a
|
|
13
|
+
ChatGPT subscription. All design reading and reasoning must happen on the Codex side. Every extra
|
|
14
|
+
line you send to chat costs Claude tokens. Therefore: do not read files, do not retell the report,
|
|
15
|
+
and do not reason about the design yourself.
|
|
16
|
+
|
|
17
|
+
## Required dispatcher inputs
|
|
18
|
+
|
|
19
|
+
{{CODEX_REQUIRED_INPUTS}}
|
|
20
|
+
|
|
21
|
+
## What you receive as input
|
|
22
|
+
|
|
23
|
+
- The task statement: the design question, candidate options, and completion criteria.
|
|
24
|
+
- The path to a task file containing the question and its context. The orchestrator supplies this
|
|
25
|
+
path; pass it as `--task-file` and never create, read or rewrite it. Writing that file yourself
|
|
26
|
+
from the shell — `cat > … << EOF` or any equivalent — puts back the permission prompt the flag
|
|
27
|
+
exists to remove. Given no path, start the runner without the flag and return its refusal.
|
|
28
|
+
- The path to the repository. If none is given, use the current working directory.
|
|
29
|
+
- `phase` is exactly `scope` or `advise`, supplied by the orchestrator and passed as `--phase`.
|
|
30
|
+
Run `scope` first. Run `advise` only as the continuation of that same order's successful advisor
|
|
31
|
+
scope run. Never choose or change the phase yourself.
|
|
32
|
+
- The task file must use these headings and list formats:
|
|
33
|
+
- `## Question` — free-text design question.
|
|
34
|
+
- `## Options` — at least two lines of the form `- <option_id>: <description>`. Each id must
|
|
35
|
+
match `[a-z0-9][a-z0-9-]*`; ids must be unique. Do not include preference markers or wording
|
|
36
|
+
such as `recommend`, `prefer`, `рекоменд`, `предпочт`, `(✓)`, or `★`. The advisor must not
|
|
37
|
+
know which option the requester favors (D5). Biased or malformed options are refused before
|
|
38
|
+
quota is spent.
|
|
39
|
+
- `## Paths` — at least one repository-relative path per list line. The advisor may cite only
|
|
40
|
+
these paths and, during `advise`, paths the scope run named in `missing_paths`. Each
|
|
41
|
+
`path:line` citation is machine-checked; an invented or out-of-list address fails the run (D3).
|
|
42
|
+
- Scope patterns are globs relative to the repository root. A pattern that matches nothing there is
|
|
43
|
+
refused before the run starts. This dispatcher does not accept scope patterns: its file boundary
|
|
44
|
+
is the explicit `## Paths` list and, for `advise`, the successful scope run's `missing_paths`.
|
|
45
|
+
- Every input listed under **Required dispatcher inputs** above, passed on exactly as given:
|
|
46
|
+
`order id` as `--order-id`, `task file` as `--task-file`, and `phase` as `--phase`. Use the same
|
|
47
|
+
order id for both phases; never invent or edit it. If the orchestrator did not give a required
|
|
48
|
+
input, do not guess — start the runner without its flag and return its refusal verbatim.
|
|
49
|
+
- `continue` — when the task text contains a line beginning with the `continue:` label, pass the
|
|
50
|
+
bare `--continue` flag even when its run name or reason looks malformed. Do not inspect, repair,
|
|
51
|
+
or swallow this grant; pass it through and let the runner issue the refusal. A continuation is
|
|
52
|
+
assigned by the orchestrator, never chosen by you. After the verdict, return the exact attaching
|
|
53
|
+
output and stop; do not issue or invent another continuation. If no such grant line is present,
|
|
54
|
+
the flag must not be present. The advise phase requires this grant to continue the scope run.
|
|
55
|
+
- Optional: `slug: <short-name>` for the run folder; by default, the slug is taken from the order
|
|
56
|
+
id. Optional: `effort: <none|low|medium|high|xhigh|max>` only when the orchestrator named a
|
|
57
|
+
depth.
|
|
58
|
+
|
|
59
|
+
## When the host refuses the command
|
|
60
|
+
|
|
61
|
+
If the host refuses to run `codex-bridge run` — a permission prompt, a classifier denial, anything
|
|
62
|
+
that stops the command — that refusal is your final answer. Report `FAIL`, name your own order id,
|
|
63
|
+
and state the one correction: the operator runs `codex-bridge install`, which grants the permission
|
|
64
|
+
rule this package needs.
|
|
65
|
+
|
|
66
|
+
You are forbidden to look for a way around it. Specifically, and without exception:
|
|
67
|
+
|
|
68
|
+
- never call `run-codex.mjs`, or any file inside the installed package, by path;
|
|
69
|
+
- never start the runner through `node`, `npx`, `sh`, `bash` or any other interpreter;
|
|
70
|
+
- never retry the same call in PowerShell because Bash refused it, or the reverse;
|
|
71
|
+
- never split the call over more than one line, and never add a pipe, a semicolon or a redirect;
|
|
72
|
+
- never advise the operator to grant a permission rule on an internal file — a rule on anything but
|
|
73
|
+
the package command undoes the very design that makes this call permission-stable.
|
|
74
|
+
|
|
75
|
+
Every one of those forms was removed on purpose: a host matches a permission rule against the
|
|
76
|
+
beginning of the final command line, so an interpreter, a path or a continuation makes the call
|
|
77
|
+
unmatchable by construction. Reaching for one does not rescue the run; it guarantees the refusal.
|
|
78
|
+
|
|
79
|
+
## The only thing you do
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
codex-bridge run --agent codex-advisor --repo "<repository-path or .>" --phase "<phase from the orchestrator>" --slug "<slug>" --order-id "<order id from the orchestrator>" --task-file "<task-file path from the orchestrator>"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
That is one command line. Add only the bare `--continue` when the task text contains a line
|
|
86
|
+
beginning with `continue:`; it has no value. The runner accepts only `1/true/yes/0/false/no` as a
|
|
87
|
+
grant value in the task text, so a malformed grant is passed through for the runner to refuse. If
|
|
88
|
+
there is no grant line, do not add the flag. Add `--effort "<value>"` only if the orchestrator
|
|
89
|
+
named a depth, and only with one of `none|low|medium|high|xhigh|max`. Never add free text or any
|
|
90
|
+
flags beyond those two conditional flags.
|
|
91
|
+
|
|
92
|
+
The first `scope` call starts the run and returns at once with `RUN=<path> order-id=<id>` and a
|
|
93
|
+
`STARTED` line. To get its verdict, run the identical command a second time — same order id, slug,
|
|
94
|
+
phase and flags. That second call attaches to the existing run and prints `ATTACH=<path>` before
|
|
95
|
+
blocking for the verdict. Do not start another scope run to attach.
|
|
96
|
+
|
|
97
|
+
After an `OK` scope verdict, the orchestrator may run `advise` with `--continue`, the same order id,
|
|
98
|
+
and the continuation grant it supplied. That call continues the scope run; it is not a new order.
|
|
99
|
+
Attach to that advise run by repeating its identical command if it returns `STARTED`.
|
|
100
|
+
|
|
101
|
+
If the runner refuses with an order id collision — the id already belongs to a run whose task
|
|
102
|
+
differs — that refusal is the whole answer: return `FAIL` with the runner's text. Never retry under
|
|
103
|
+
a different id of your own choosing; the order id is the orchestrator's.
|
|
104
|
+
|
|
105
|
+
Background execution (`run_in_background`, `&`, `nohup`) is prohibited, and so is inventing a
|
|
106
|
+
report from memory. If an attaching call is killed by a time ceiling, repeat the identical command
|
|
107
|
+
once with `--no-wait`. This only checks state; it is never a final answer. If stdout contains the
|
|
108
|
+
ready reply, return it normally. If the call exits 4, return to the ordinary waiting call with the
|
|
109
|
+
same order id and without `--no-wait`. A real run takes 20-25 minutes, which is normal, not a hang.
|
|
110
|
+
Give the ordinary attaching call `timeout: 1800000` (30 minutes).
|
|
111
|
+
|
|
112
|
+
**Never change `--order-id` or `--slug` to get a fresh run.** The order id is issued by the
|
|
113
|
+
orchestrator and is what makes a repeat harmless; changing it turns a repeat into another paid run.
|
|
114
|
+
|
|
115
|
+
The runner does the rest: validates the task file and phase, creates the run folder and artifacts,
|
|
116
|
+
starts Codex in a read-only sandbox, records status, and prints ready-made response lines. Invalid
|
|
117
|
+
or biased `Options`/`Paths`, `advise` without `--continue`, `advise` without an `OK` advisor scope
|
|
118
|
+
run, and an undeclared phase are free refusals; no ChatGPT quota is spent.
|
|
119
|
+
|
|
120
|
+
**Your response = the exact stdout of the attaching call**: the `ATTACH=<path>` line and the status
|
|
121
|
+
block below it. Do not add or remove anything: no preamble, explanations, apologies, or retelling
|
|
122
|
+
of findings. The report is in the file; the orchestrator will read it if needed. A `STARTED` result
|
|
123
|
+
is not final.
|
|
124
|
+
|
|
125
|
+
The only allowed final response is this exact stdout. Wording such as "the run has started,
|
|
126
|
+
waiting for completion," "I will wait for a notification," or "Monitor started in the background"
|
|
127
|
+
is prohibited: the subagent ceases to exist immediately after responding, nobody can wait, and the
|
|
128
|
+
orchestrator gets a promise instead of a result. Inventing any outcome the runner did not print is
|
|
129
|
+
equally prohibited.
|
|
130
|
+
|
|
131
|
+
## What you return
|
|
132
|
+
|
|
133
|
+
Return the attaching output exactly as printed by the runner, including its ready-made short
|
|
134
|
+
result and report path. Do not compose, summarize, or improve the result yourself. The full report
|
|
135
|
+
lives under `~/.claude/codex-runs/`; the orchestrator receives no more than five lines plus that
|
|
136
|
+
path.
|
|
137
|
+
|
|
138
|
+
## The script determines status, not you
|
|
139
|
+
|
|
140
|
+
- `OK` — the phase result is valid and the runner returned zero. An insufficient scope is still an
|
|
141
|
+
`OK` scope result, not a quota limit.
|
|
142
|
+
- `FAIL` — the result is empty, invalid, the return code is nonzero, or the run was abandoned.
|
|
143
|
+
- `LIMIT` — the result is empty and the log signals a ChatGPT quota limit. The quota is spent and
|
|
144
|
+
the task was not completed; do not restart it.
|
|
145
|
+
|
|
146
|
+
The script return code mirrors the status: `0` / `1` / `3`. A nonzero code is not a reason to
|
|
147
|
+
retry, change the command, or investigate on your own.
|
|
148
|
+
|
|
149
|
+
For `scope`, `OK — scope: sufficient; …` means the listed paths cover the question; continue with
|
|
150
|
+
`--phase advise` only when the orchestrator supplies that continuation. `OK — scope: insufficient;
|
|
151
|
+
N paths named` followed by a `Missing:` line is also an `OK` run, not a `LIMIT`: the orchestrator
|
|
152
|
+
may add context or continue with `advise`. `LIMIT` alone means the ChatGPT quota is spent.
|
|
153
|
+
|
|
154
|
+
For `advise`, expect `OK — recommend <option_id>: …`, followed by `Rejected:`, `Counter:`, and
|
|
155
|
+
`Risks:` lines. The recommendation names one option id, rejected options include their cost,
|
|
156
|
+
`Counter:` gives the strongest counterargument, and `Risks:` is the pre-mortem. Forward those lines
|
|
157
|
+
verbatim; compose nothing.
|
|
158
|
+
|
|
159
|
+
The run folder contains its status and artifacts. An abandoned run is not a reason to start over
|
|
160
|
+
yourself: the orchestrator decides whether to continue, and without the `--continue` it issued,
|
|
161
|
+
the runner will reject that continuation itself.
|
|
162
|
+
|
|
163
|
+
## Codex is unavailable
|
|
164
|
+
|
|
165
|
+
The runner checks `codex --version` before starting. If the binary is missing or authentication
|
|
166
|
+
failed, it prints a ready-made `FAIL` with a verification command for the operator. Return that
|
|
167
|
+
output. Performing the task manually instead of Codex is prohibited for any reason it fails.
|
|
168
|
+
|
|
169
|
+
## What a violation looks like
|
|
170
|
+
|
|
171
|
+
Correct (runner output copied exactly):
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
ATTACH=<artifact root>\myproject\2026-09-22_1412_advisor started=2026-09-22T14:12:03.000Z
|
|
175
|
+
OK — recommend service-boundary: the interface keeps ownership local
|
|
176
|
+
Rejected: shared-kernel — every consumer now pays for cross-domain coupling
|
|
177
|
+
Counter: a shared contract could reduce duplicated validation
|
|
178
|
+
Risks: the boundary may be wrong if a second consumer needs synchronous access
|
|
179
|
+
Report: ...\report.md · Log: ...\2026-09-22_1412_advisor
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Incorrect — "Codex started in the background. Waiting for the advice to finish — I will notify
|
|
183
|
+
you." There will be no notification: as a dispatcher, you terminate with the response, so nobody
|
|
184
|
+
can wait and send one. Background execution is prohibited above.
|
|
185
|
+
|
|
186
|
+
Incorrect — "I reviewed the design manually because Codex could not read one of the paths," followed
|
|
187
|
+
by recommendations. This defeats the boundary: all reading and reasoning belong to the Codex run.
|
|
@@ -51,6 +51,10 @@ job is to report the run status honestly, including failure.
|
|
|
51
51
|
heading, one line. You never read that file and never put the command on the command line: an
|
|
52
52
|
operator's real check command contained `&&`, and a compound operator in an argument makes the
|
|
53
53
|
host stop applying its permission rule, which is how a delegation dies on a refusal.
|
|
54
|
+
- Every build task file must contain exactly one `advice:` line from the orchestrator: an absolute
|
|
55
|
+
path to an existing run folder whose `meta.json` identifies `codex-advisor`, or exactly
|
|
56
|
+
`mechanical`, `revert`, `docs-only`, or `test-only`. Anything else is refused before the run
|
|
57
|
+
starts. Pass the task file as supplied; the orchestrator owns this line.
|
|
54
58
|
|
|
55
59
|
## When the host refuses the command
|
|
56
60
|
|
package/src/commands/env.md
CHANGED
|
@@ -9,10 +9,11 @@ argument-hint: "[hooks|plugins on|off] | [reset]"
|
|
|
9
9
|
symlinks nor pointer files (verified 2026-08-02). Edit it here; it travels as a copy when the
|
|
10
10
|
package is installed. -->
|
|
11
11
|
|
|
12
|
-
Environment switches for `codex-scout` / `codex-build` / `codex-review` runs.
|
|
13
|
-
and plugins from `~/.codex` do not reach a delegated run: they are written for
|
|
14
|
-
do harm in an automated one — the failing `Stop` hook of `oh-my-codex` would
|
|
15
|
-
session, and instead of doing the task it quarantined
|
|
12
|
+
Environment switches for `codex-scout` / `codex-build` / `codex-review` / `codex-advisor` runs.
|
|
13
|
+
By default the hooks and plugins from `~/.codex` do not reach a delegated run: they are written for
|
|
14
|
+
interactive work and do harm in an automated one — the failing `Stop` hook of `oh-my-codex` would
|
|
15
|
+
not let Codex leave the session, and instead of doing the task it quarantined
|
|
16
|
+
`.omx/state/session.json`. The operator's
|
|
16
17
|
interactive Codex is untouched: the flags are set on one specific runner invocation,
|
|
17
18
|
`~/.codex/hooks.json` is not modified.
|
|
18
19
|
|
package/src/home/lib/agents.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Defines each delegated agent's role, wall-clock budget, write access and result file.
|
|
3
|
+
* D2: a budget declares positive minutes or named phases; budgets.mjs alone normalizes it.
|
|
3
4
|
*
|
|
4
5
|
* The three agents were listed four times: twice in run-config.mjs for budgets and models,
|
|
5
6
|
* once in codex-args.mjs for role lookup, and once in order-gate.mjs for guarded names.
|
|
@@ -10,6 +11,8 @@ export const AGENTS = {
|
|
|
10
11
|
'codex-scout': { role: 'scout', budget: 15, writes: false, result: 'result.json' },
|
|
11
12
|
'codex-build': { role: 'build', budget: 25, writes: true, result: 'result.json' },
|
|
12
13
|
'codex-review': { role: 'review', budget: 20, writes: false, result: 'review.json' },
|
|
14
|
+
// Plan_59: design advice needs a bounded scope pass before the continued decision pass.
|
|
15
|
+
'codex-advisor': { role: 'advisor', budget: { scope: 5, advise: 15 }, writes: false, result: 'result.json' },
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
export const agentRole = (agent) => AGENTS[agent]?.role;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves every role's wall-clock budget into a map of phase names to minutes.
|
|
3
|
+
*
|
|
4
|
+
* A run gets a hard wall-clock budget, because the caller's timeout is not a run contract:
|
|
5
|
+
* on 2026-08-03 one order restarted six times and spent 170,293 accounted tokens while four
|
|
6
|
+
* killed callers left their Codex processes and token spend unrecorded. Plan_59 D2 made the
|
|
7
|
+
* phase a runner concept: a number is shorthand for one `default` phase, and it is expanded
|
|
8
|
+
* here and nowhere else, so no consumer ever has to ask which of the two shapes it holds.
|
|
9
|
+
*/
|
|
10
|
+
import { AGENTS } from './agents.mjs';
|
|
11
|
+
|
|
12
|
+
export function resolveBudgets(file, overrides = {}, registry = AGENTS) {
|
|
13
|
+
const roles = Object.values(registry).map(({ role }) => role);
|
|
14
|
+
if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)) {
|
|
15
|
+
throw new Error(`${file}: key “budgets” must be an object keyed by ${roles.join(', ')}, ` +
|
|
16
|
+
`each holding a positive number of minutes or a phase map, not ${JSON.stringify(overrides)}`);
|
|
17
|
+
}
|
|
18
|
+
const normalize = (budget, key, defaults) => {
|
|
19
|
+
if (typeof budget === 'string' && !budget.trim()) {
|
|
20
|
+
throw new Error(`${file}: key “${key}” is empty; remove the field to use the default, ` +
|
|
21
|
+
'or give it a positive number of minutes or a phase map');
|
|
22
|
+
}
|
|
23
|
+
const scalar = typeof budget === 'number';
|
|
24
|
+
// A number for a role with named phases would replace its whole map with `default`: the
|
|
25
|
+
// config would read cleanly and every run of that role would then be refused.
|
|
26
|
+
if (scalar && defaults && Object.keys(defaults).some((phase) => phase !== 'default')) {
|
|
27
|
+
throw new Error(`${file}: key “${key}” must be a phase map for this role, ` +
|
|
28
|
+
`e.g. {${Object.keys(defaults).map((phase) => `"${phase}": ${defaults[phase]}`).join(', ')}}, ` +
|
|
29
|
+
`not ${JSON.stringify(budget)}`);
|
|
30
|
+
}
|
|
31
|
+
if (scalar) budget = { default: budget };
|
|
32
|
+
if (!budget || typeof budget !== 'object' || Array.isArray(budget)) {
|
|
33
|
+
throw new Error(`${file}: key “${key}” must be a positive number of minutes or a phase map, ` +
|
|
34
|
+
`not ${JSON.stringify(budget)}`);
|
|
35
|
+
}
|
|
36
|
+
for (const [phase, minutes] of Object.entries(budget)) {
|
|
37
|
+
if (!phase.trim()) throw new Error(`${file}: key “${key}” has an empty phase name`);
|
|
38
|
+
if (defaults && !scalar && !Object.hasOwn(defaults, phase)) {
|
|
39
|
+
throw new Error(`${file}: key “${key}” has unknown phase “${phase}”. ` +
|
|
40
|
+
`Only ${Object.keys(defaults).join(', ')} are allowed`);
|
|
41
|
+
}
|
|
42
|
+
if (!Number.isFinite(minutes) || minutes <= 0) {
|
|
43
|
+
throw new Error(`${file}: key “${key}.${phase}” must be a positive number of minutes, ` +
|
|
44
|
+
`not ${JSON.stringify(minutes)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// D2: a scalar is a default-only budget; only an object requests a partial phase override.
|
|
48
|
+
return { ...(scalar ? {} : defaults), ...budget };
|
|
49
|
+
};
|
|
50
|
+
const budgets = Object.fromEntries(Object.values(registry).map(({ role, budget }) => {
|
|
51
|
+
const phases = normalize(budget, `budgets.${role}`);
|
|
52
|
+
if (!Object.keys(phases).length) {
|
|
53
|
+
throw new Error(`${file}: key “budgets.${role}” must declare at least one phase`);
|
|
54
|
+
}
|
|
55
|
+
return [role, phases];
|
|
56
|
+
}));
|
|
57
|
+
for (const [role, budget] of Object.entries(overrides)) {
|
|
58
|
+
if (!roles.includes(role)) {
|
|
59
|
+
throw new Error(`${file}: key “budgets” has unknown role “${role}”. Only ${roles.join(', ')} are allowed`);
|
|
60
|
+
}
|
|
61
|
+
budgets[role] = normalize(budget, `budgets.${role}`, budgets[role]);
|
|
62
|
+
}
|
|
63
|
+
return budgets;
|
|
64
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared by reading and editing so Plan_56 D27 cannot persist an unreadable config. */
|
|
2
2
|
import { AGENTS } from './agents.mjs';
|
|
3
|
+
import { resolveBudgets } from './budgets.mjs';
|
|
3
4
|
|
|
4
5
|
const BUDGET_KEY = 'budgets';
|
|
5
6
|
export const ROLES = Object.values(AGENTS).map(({ role }) => role);
|
|
@@ -31,7 +32,7 @@ export const DEFAULTS = {
|
|
|
31
32
|
hooks: false,
|
|
32
33
|
plugins: false,
|
|
33
34
|
models: {},
|
|
34
|
-
budgets:
|
|
35
|
+
budgets: resolveBudgets('agent registry'),
|
|
35
36
|
retention: DEFAULT_RETENTION,
|
|
36
37
|
environmentPaths: DEFAULT_ENVIRONMENT_PATHS,
|
|
37
38
|
answerLanguage: 'English',
|
|
@@ -88,43 +89,6 @@ function readRetention(file, value) {
|
|
|
88
89
|
return { enabled: true, days: value.days };
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/**
|
|
92
|
-
* A run gets a hard wall-clock budget, because the caller's timeout is not a run contract:
|
|
93
|
-
* on 2026-08-03 one order restarted six times and spent 170,293 accounted tokens while four
|
|
94
|
-
* killed callers left their Codex processes and token spend unrecorded.
|
|
95
|
-
*/
|
|
96
|
-
function readBudgets(file, value) {
|
|
97
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`${file}: key “${BUDGET_KEY}” must be an object keyed by ${ROLES.join(', ')}, ` +
|
|
100
|
-
`each holding a positive number of minutes, not ${JSON.stringify(value)}`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
const budgets = { ...DEFAULTS.budgets };
|
|
104
|
-
for (const [role, minutes] of Object.entries(value)) {
|
|
105
|
-
if (!ROLES.includes(role)) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
`${file}: key “${BUDGET_KEY}” has unknown role “${role}”. ` +
|
|
108
|
-
`Only ${ROLES.join(', ')} are allowed`,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
if (typeof minutes === 'string' && !minutes.trim()) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
`${file}: key “${BUDGET_KEY}.${role}” is empty; remove the field to use the default ` +
|
|
114
|
-
`(${DEFAULTS.budgets[role]} minutes), or give it a positive number of minutes`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
if (typeof minutes !== 'number' || !Number.isFinite(minutes) || minutes <= 0) {
|
|
118
|
-
throw new Error(
|
|
119
|
-
`${file}: key “${BUDGET_KEY}.${role}” must be a positive number of minutes, ` +
|
|
120
|
-
`not ${JSON.stringify(minutes)}`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
budgets[role] = minutes;
|
|
124
|
-
}
|
|
125
|
-
return budgets;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
92
|
export function validateRunConfig(file, parsed) {
|
|
129
93
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
130
94
|
throw new Error(`${file} must contain an object like {"hooks": false, "plugins": false}`);
|
|
@@ -145,7 +109,7 @@ export function validateRunConfig(file, parsed) {
|
|
|
145
109
|
continue;
|
|
146
110
|
}
|
|
147
111
|
if (key === BUDGET_KEY) {
|
|
148
|
-
config[key] =
|
|
112
|
+
config[key] = resolveBudgets(file, value);
|
|
149
113
|
continue;
|
|
150
114
|
}
|
|
151
115
|
if (key === RETENTION_KEY) {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Converts a finished advisor run into judge inputs because schemas accept well-formed agreement; Plan_59 D3/D4/D5/D10 require the judge to reject it. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { DEFAULTS } from '../run-config.mjs';
|
|
5
|
+
import { judgeAdvice } from './advice-verdict.mjs';
|
|
6
|
+
import { readJson } from './paths.mjs';
|
|
7
|
+
|
|
8
|
+
function contractReason(reasons) {
|
|
9
|
+
const message = `advice contract broken (${reasons.length}): ${reasons.slice(0, 2).join('; ')}`;
|
|
10
|
+
return message.length <= 200 ? message : `${message.slice(0, 197)}...`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function adviceGap(runDir, result, eventData) {
|
|
14
|
+
const worker = readJson(path.join(runDir, 'worker.json')) || {};
|
|
15
|
+
const status = readJson(path.join(runDir, 'status.json')) || {};
|
|
16
|
+
const env = readJson(path.join(runDir, 'env.json')) || {};
|
|
17
|
+
const phase = worker.phase ?? status.phase;
|
|
18
|
+
const taskFile = path.join(runDir, 'advisor-task.json');
|
|
19
|
+
const task = readJson(taskFile);
|
|
20
|
+
const reasons = [];
|
|
21
|
+
|
|
22
|
+
if (!fs.existsSync(taskFile)) reasons.push('advisor-task.json is missing');
|
|
23
|
+
else if (!Array.isArray(task?.options) || !Array.isArray(task?.paths)) {
|
|
24
|
+
reasons.push('advisor-task.json is missing parsed options or paths');
|
|
25
|
+
}
|
|
26
|
+
if (phase !== 'scope' && phase !== 'advise') reasons.push('worker.json/status.json is missing a valid advisor phase');
|
|
27
|
+
if (typeof status.repo !== 'string' || !status.repo) reasons.push('status.json#repo is missing');
|
|
28
|
+
if (!Number.isInteger(eventData?.commands_executed) || eventData.commands_executed < 0) {
|
|
29
|
+
reasons.push('eventData.commands_executed is missing or invalid');
|
|
30
|
+
}
|
|
31
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) reasons.push('result.json is missing or is not an object');
|
|
32
|
+
|
|
33
|
+
let scopeResult;
|
|
34
|
+
if (phase === 'advise') {
|
|
35
|
+
if (typeof status.continued_from !== 'string' || !status.continued_from) {
|
|
36
|
+
reasons.push('status.json#continued_from for the phase-1 result is missing');
|
|
37
|
+
} else {
|
|
38
|
+
const scopeFile = path.join(path.dirname(runDir), status.continued_from, 'result.json');
|
|
39
|
+
scopeResult = readJson(scopeFile);
|
|
40
|
+
if (!scopeResult || typeof scopeResult !== 'object' || !Array.isArray(scopeResult.predicted_risks)) {
|
|
41
|
+
reasons.push('phase-1 result.json or its predicted_risks is missing');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (reasons.length) return contractReason(reasons);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const verdict = judgeAdvice({
|
|
49
|
+
phase,
|
|
50
|
+
result,
|
|
51
|
+
task,
|
|
52
|
+
repoRoot: status.repo,
|
|
53
|
+
commandsRun: eventData.commands_executed,
|
|
54
|
+
language: env.answerLanguage ?? DEFAULTS.answerLanguage,
|
|
55
|
+
scopeResult,
|
|
56
|
+
});
|
|
57
|
+
return verdict.ok ? null : contractReason(verdict.reasons);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return contractReason([`result.json is missing required fields or has an unexpected shape (${error.message})`]);
|
|
60
|
+
}
|
|
61
|
+
}
|