@lyupro/codex-bridge 0.6.2 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +146 -0
  2. package/README.md +3 -3
  3. package/cli/runs-inventory.mjs +5 -3
  4. package/cli/stop.mjs +2 -2
  5. package/cli/unlock.mjs +2 -2
  6. package/package.json +2 -2
  7. package/src/agents/codex-advisor.md +187 -0
  8. package/src/agents/codex-build.md +4 -0
  9. package/src/commands/env.md +5 -4
  10. package/src/home/hooks/live-runs.mjs +19 -21
  11. package/src/home/hooks/reply-guard.mjs +5 -2
  12. package/src/home/hooks/shell-write-intent.mjs +320 -154
  13. package/src/home/hooks/worktree-lock.mjs +9 -2
  14. package/src/home/hooks/worktree-witness.mjs +21 -40
  15. package/src/home/lib/agents.mjs +3 -0
  16. package/src/home/lib/budgets.mjs +64 -0
  17. package/src/home/lib/config-validate.mjs +3 -39
  18. package/src/home/lib/meta/advice-status.mjs +61 -0
  19. package/src/home/lib/meta/advice-verdict.mjs +226 -0
  20. package/src/home/lib/meta/advisor-task.mjs +69 -0
  21. package/src/home/lib/meta/events.mjs +4 -0
  22. package/src/home/lib/meta/launch-rows.mjs +45 -0
  23. package/src/home/lib/meta/reply.mjs +42 -34
  24. package/src/home/lib/meta/run-liveness.mjs +64 -0
  25. package/src/home/lib/meta/run-state.mjs +28 -34
  26. package/src/home/lib/meta/verdict.mjs +18 -0
  27. package/src/home/lib/required-inputs.mjs +25 -4
  28. package/src/home/lib/run-codex.mjs +1 -0
  29. package/src/home/lib/run-config.mjs +5 -1
  30. package/src/home/lib/runner/args.mjs +8 -4
  31. package/src/home/lib/runner/attach.mjs +5 -13
  32. package/src/home/lib/runner/codex-args.mjs +14 -5
  33. package/src/home/lib/runner/codex-cmd.mjs +105 -19
  34. package/src/home/lib/runner/git-state.mjs +6 -1
  35. package/src/home/lib/runner/launcher.mjs +36 -30
  36. package/src/home/lib/runner/preflight.mjs +109 -0
  37. package/src/home/lib/runner/prompts.mjs +39 -0
  38. package/src/home/lib/runner/sandbox-probe.mjs +109 -0
  39. package/src/home/lib/runner/schemas.mjs +137 -1
  40. package/src/home/lib/runner/scope-check.mjs +31 -3
  41. package/src/home/lib/runner/worker-order.mjs +4 -2
  42. package/src/home/lib/runner/worker.mjs +2 -2
  43. package/src/home/lib/write-meta.mjs +18 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,152 @@ 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
+
52
+ ## [0.6.4] - 2026-09-21
53
+
54
+ ### Fixed
55
+
56
+ - A refusal that spends no quota no longer leaves a run folder behind. A second writing run against a
57
+ busy tree was refused correctly, but its folder was created first — and in a repository that hosts
58
+ the run folders, the live writer's witness then spent every tool call ordering the orchestrator to
59
+ revert a directory the tool itself had made, or to stop an honest run. Neither is a safe action. The
60
+ misplaced check was the symptom: refusals sat on two sides of the registration boundary with no rule
61
+ about which side was which, and two gates of the same class already disagreed — a dead sandbox
62
+ refused without a folder, a busy tree with one. The quota-free host checks now live in a module that
63
+ is handed no run directory, so a check added there cannot register a run, and a table test holds
64
+ every refusal with its side and the reason for that side. A busy tree and an unavailable Codex CLI
65
+ therefore write nothing at all: they no longer produce an `aborted_pre_start` folder, and their
66
+ exit code stays 1, because the order was correct and the host or the tree was not.
67
+ - The live witness and the final verdict judge one worktree by one definition. The witness read
68
+ `git status --porcelain` on its own while the verdict reads the recorded snapshot, so three kinds of
69
+ change were visible to one and invisible to the other: the run's own folder, the orchestrator's
70
+ environment writes, and gitignored working notes. The witness now composes the same chain as the
71
+ verdict, and its message says first that those three are already excluded — so what it lists is the
72
+ orchestrator's own edit — and only then offers the two actions. A structural test fails any hook that
73
+ spells a worktree query in an argument list; this drift went unnoticed for months and surfaced only
74
+ as a false accusation against a run that was doing its job.
75
+ - A file moved inside its own scope is no longer a stray. The shared snapshot was read with git's
76
+ rename detection on, which prints one row spelled `old => new` — not a path, and matching no scope
77
+ pattern. The witness named that token at the orchestrator, and the verdict would have failed an
78
+ honest build for a `git mv` it was told to make. The snapshot is now read with `--no-renames`: a
79
+ rename is a deletion plus an addition, two real paths, each judged on its own merits.
80
+ - A scope pattern that can only name a directory is refused before the run starts. A scope is compared
81
+ against file paths, never directories, so `muse/scripts` authorises one file that will never exist
82
+ while everything created inside the folder counts as a stray. Declared patterns escaped this by luck
83
+ — `git ls-files` prints no directories — but `--scope-new` is exempt from the existence check by
84
+ design, and a run ordered with `--scope-new muse/scripts` worked ten minutes, returned a filled
85
+ result, and was failed for the three files it created there. A trailing slash, a name that is a
86
+ directory on disk, and a glob-free name whose last segment has no extension are now refused before
87
+ the run folder exists, with the spelling that works named in the refusal. Patterns containing a glob
88
+ are never judged by these rules.
89
+
90
+ ## [0.6.3] - 2026-09-17
91
+
92
+ ### Added
93
+
94
+ - A dead Codex sandbox refuses the run before it spends quota. On 2026-09-16 an unclean Windows
95
+ shutdown left the sandbox helper's state file as 22 zero bytes, and every delegated run after it
96
+ paid for a session in which not one command ran — one scout even came back `OK`. The launcher now
97
+ asks the sandbox to echo a marker before the run folder exists, and calls it dead only on double
98
+ evidence: the role's own flags and a flagless control both produce no marker, neither reports an
99
+ argument error, and the CLI still answers `--version`. Codex had already changed the shape of its
100
+ own sandbox command once, so anything the probe cannot judge lets the run start and is named in
101
+ the reply — a release that stops accepting the package's sandbox flags cannot turn the check off
102
+ silently. `stderr` is quoted for the operator and never parsed. The probe is bounded on every
103
+ path: a hung attempt stops the whole Codex tree rather than the `cmd.exe` in front of it, and a
104
+ capture that overflows is inconclusive, never dead. Windows and Linux are judged, each after both
105
+ outcomes were seen live on it; on Linux, where a dead sandbox is the ordinary state of a fresh
106
+ Ubuntu 23.10+ server, the refusal names the AppArmor profile that repairs it and warns against
107
+ lifting the restriction through `sysctl`. macOS is skipped — no outcome has been observed there.
108
+
109
+ ### Fixed
110
+
111
+ - A scout that executed no command can no longer come back `OK`. On the dead sandbox of 2026-09-16
112
+ a scout was graded `OK` without reading a single file: its evidence explained that the files were
113
+ unreadable, and the coverage gate only checked that the evidence was a non-empty string. The
114
+ verdict now requires a fact before it judges coverage — at least one completed
115
+ `command_execution` in `events.jsonl`, with any status or exit code. Refused calls never enter the
116
+ stream, which is what makes the fact reliable; across 99 earlier successful scout runs it was
117
+ missing exactly twice, both on that sandbox.
118
+ - Quoted text no longer reads as shell commands to the worktree lock. The write-intent scanner read
119
+ quotes three different ways, and its raw split on `;` and `|` cut inside them: an `awk` script
120
+ containing "Do not touch any file outside" was refused on 2026-09-16 as a write to `any`, `file`
121
+ and `outside` — the fourth refusal on one root after `node -e`, `sed` and a heredoc body. One
122
+ lexer now owns quotes, escapes and command boundaries for all three consumers, drops comments, and
123
+ opens subshells and `$(…)` only where the shell does.
124
+ - An argument that happens to be a command name is not a write. `git log --grep rm -- README.md` and
125
+ `grep -rn touch src/` were refused while a build held the repository, because the scanner looked
126
+ for `rm` and `touch` in any word. A command name now counts only in command position — after
127
+ assignments, reserved words, leading redirections, a short list of launchers, and a `find -exec`
128
+ action up to its terminator.
129
+ - A stop at the deadline cannot block the launcher forever. `stopCodex()` gave `taskkill` no timeout,
130
+ so a stalled `taskkill` held the event loop and every settlement timer with it. It now has ten
131
+ seconds, after which the direct child is killed.
132
+ - The response guard judges a run by its own process, not by a bare pid. Without the run folder or
133
+ the recorded start time, a pid reused by another process after a reboot read as "the run is still
134
+ working", and the guard held the dispatcher on a dead run until the session's retry budget stopped
135
+ the session.
136
+ - The project list no longer prints the raw `running` of a run whose worker died before writing
137
+ `meta.json`. A row without a verdict shows what closing the run would write: `abandoned` for a
138
+ dead or foreign process, `unverified` when the end cannot be proven. The "live now" column keeps
139
+ its fresh-heartbeat policy.
140
+
141
+ ### Changed
142
+
143
+ - One module decides whether a recorded run is still live. Seven places answered that question on
144
+ their own through pid-first wrappers whose other inputs were optional — which is how the response
145
+ guard came to ask with a bare pid and the project list never to ask at all, although
146
+ `markAbandoned()` already held the exact rule. `src/home/lib/meta/run-liveness.mjs` now owns it,
147
+ and `tests/one-liveness-judge.test.mjs` fails the suite when another module asks a process
148
+ identity, re-exports one under a new name, or declares a pid-first wrapper again.
149
+ - The rows a reply carries are rendered by one function. The Retention row had been copied into five
150
+ reply functions and was still missing from the runner's failure path, so a runner failure lost it;
151
+ a sixth copy for the sandbox row would have repeated that defect.
152
+
7
153
  ## [0.6.2] - 2026-09-07
8
154
 
9
155
  ### Fixed
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # codex-bridge
2
2
 
3
- **Delegate implementation, reconnaissance and review from Claude Code to Codex — six host guards, zero runtime dependencies, zero build steps.**
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
  [![npm version](https://img.shields.io/npm/v/@lyupro/codex-bridge)](https://www.npmjs.com/package/@lyupro/codex-bridge)
6
6
  [![Node.js](https://img.shields.io/node/v/@lyupro/codex-bridge)](https://www.npmjs.com/package/@lyupro/codex-bridge)
@@ -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 three agents, two slash commands, merged hook registrations in `settings.json`, and run artifacts under `codex-runs/`.
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 **975 automated tests: 974 passing and 1 skipped**.
65
+ The current suite contains **1491 automated tests: 1490 passing and 1 skipped**.
66
66
 
67
67
  ## Verify
68
68
 
@@ -3,6 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { liveRuns } from '../src/home/hooks/live-runs.mjs';
5
5
  import { readJsonFileSync } from '../src/home/lib/json-file.mjs';
6
+ import { runLiveness } from '../src/home/lib/meta/run-liveness.mjs';
6
7
  import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
7
8
 
8
9
  const record = (value) => value && typeof value === 'object' && !Array.isArray(value);
@@ -108,7 +109,7 @@ function readRun(runDir, livePaths) {
108
109
  return {
109
110
  run: path.basename(runDir),
110
111
  agent: text(facts?.agent),
111
- verdict: live ? 'running' : text(meta ? meta.status : status?.state),
112
+ verdict: live ? 'running' : text(meta ? meta.status : runLiveness({ runDir, status }).state),
112
113
  tokens: meta ? number(meta.tokens) : null,
113
114
  size: recursiveSize(runDir),
114
115
  timestamp: runTimestamp(path.basename(runDir), meta, status),
@@ -131,8 +132,9 @@ function publicRun(run) {
131
132
 
132
133
  function buildProject(projectDir) {
133
134
  const runDirs = directoryEntries(projectDir).map((entry) => path.join(projectDir, entry.name));
134
- // live-runs owns PID and status interpretation after the 2026-08-05 live probe incident;
135
- // this module only maps its result back to the corresponding inventory row.
135
+ // live-runs owns the `live` column (a fresh heartbeat) after the 2026-08-05 live probe incident.
136
+ // A row without a verdict shows what closing it would write — the judge's `abandoned` rather than
137
+ // the raw `running` a dead run keeps in status.json forever (Plan_57 D27).
136
138
  const livePaths = new Set(liveRuns(projectDir).map(({ dir }) => pathKey(dir)));
137
139
  const details = runDirs.map((runDir) => readRun(runDir, livePaths));
138
140
  // Sum what is known instead of blanking the column. One archived run without accounting used to
package/cli/stop.mjs CHANGED
@@ -2,12 +2,12 @@
2
2
  import path from 'node:path';
3
3
  import { stopCodex } from '../src/home/lib/runner/codex-cmd.mjs';
4
4
  import { git, worktreeSnapshot } from '../src/home/lib/runner/git-state.mjs';
5
+ import { runLiveness } from '../src/home/lib/meta/run-liveness.mjs';
5
6
  import {
6
7
  IDENTITY_ALIVE,
7
8
  IDENTITY_FOREIGN,
8
9
  IDENTITY_UNVERIFIED,
9
10
  processAlive,
10
- processIdentity,
11
11
  } from '../src/home/lib/process-identity.mjs';
12
12
  import { markAbandoned, readJson } from '../src/home/lib/write-meta.mjs';
13
13
  import { runsRoot } from '../src/home/lib/runner/runs-root.mjs';
@@ -72,7 +72,7 @@ export async function stop({
72
72
  return result(1, `Run ${runDir} has no valid recorded pid; it was not changed.`);
73
73
  }
74
74
 
75
- const identity = processIdentity({ runDir, status, commandRunner });
75
+ const { identity } = runLiveness({ runDir, status, commandRunner });
76
76
  if (identity === IDENTITY_UNVERIFIED || identity === IDENTITY_FOREIGN) {
77
77
  return result(
78
78
  1,
package/cli/unlock.mjs CHANGED
@@ -3,11 +3,11 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { formatSilence, heartbeatAge, HEARTBEAT_STALE_MS } from '../src/home/lib/heartbeat.mjs';
6
+ import { runLiveness } from '../src/home/lib/meta/run-liveness.mjs';
6
7
  import { markAbandoned } from '../src/home/lib/meta/run-state.mjs';
7
8
  import {
8
9
  IDENTITY_ALIVE,
9
10
  IDENTITY_UNVERIFIED,
10
- processIdentity,
11
11
  } from '../src/home/lib/process-identity.mjs';
12
12
  import { git } from '../src/home/lib/runner/git-state.mjs';
13
13
  import { resolveProjectRunsDir } from '../src/home/lib/runner/project-dir.mjs';
@@ -144,7 +144,7 @@ function inspectRuns(project, options) {
144
144
  age: runAge(status.started_at, now),
145
145
  silence: formatSilence(silenceAge),
146
146
  silenceAge,
147
- identity: processIdentity(identityOptions(dir, status, options, now)),
147
+ identity: runLiveness(identityOptions(dir, status, options, now)).identity,
148
148
  state: status.state,
149
149
  });
150
150
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@lyupro/codex-bridge",
3
- "version": "0.6.2",
4
- "description": "Claude Code-to-Codex delegation with 6 guards, 975 tests, zero runtime dependencies, and zero build steps.",
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
 
@@ -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. By default the hooks
13
- and plugins from `~/.codex` do not reach a delegated run: they are written for interactive work and
14
- do harm in an automated one — the failing `Stop` hook of `oh-my-codex` would not let Codex leave the
15
- session, and instead of doing the task it quarantined `.omx/state/session.json`. The operator's
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
 
@@ -9,34 +9,17 @@
9
9
  *
10
10
  * The heartbeat is required here and deliberately NOT in meta/run-state.mjs: releasing a lock
11
11
  * early costs an operator one knowing edit, while closing a record early makes a second writer
12
- * of its meta.json. See the comment above pidAlive() there.
12
+ * of its meta.json. See the comment above markAbandoned() in meta/run-state.mjs.
13
13
  */
14
14
  import fs from 'node:fs';
15
15
  import path from 'node:path';
16
16
  import { heartbeatAge, isHeartbeatFresh } from '../lib/heartbeat.mjs';
17
17
  import { readJsonFileSync } from '../lib/json-file.mjs';
18
- import {
19
- IDENTITY_ALIVE,
20
- IDENTITY_DEAD,
21
- IDENTITY_FOREIGN,
22
- processIdentity,
23
- } from '../lib/process-identity.mjs';
18
+ import { runLiveness } from '../lib/meta/run-liveness.mjs';
19
+ import { IDENTITY_ALIVE } from '../lib/process-identity.mjs';
24
20
 
25
21
  export const RECENT_RUN_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
26
22
 
27
- /**
28
- * The hook requires pid judgment plus a fresh heartbeat. Identity uncertainty stays live here
29
- * (fail-open), while the identity module deliberately treats a missing heartbeat as unverified;
30
- * isHeartbeatFresh() keeps its pre-Plan_20 missing-file compatibility for this separate question.
31
- */
32
- // Plan_31 needs confirmed process identity before warning about paid work that TaskStop leaves
33
- // behind; existing lock/reply guards retain the fail-open default for uncertain identities.
34
- export const isPidAlive = (pid, runDir, status = {}, options = {}) => {
35
- const identity = processIdentity({ runDir, status: { ...status, pid } });
36
- if (options.requireConfirmedIdentity === true) return identity === IDENTITY_ALIVE;
37
- return identity !== IDENTITY_DEAD && identity !== IDENTITY_FOREIGN;
38
- };
39
-
40
23
  /**
41
24
  * Match the package's repository comparison without resolving symlinks: Windows realpath adds
42
25
  * `\\?\\` and UNC spellings, turning equal paths into unequal ones. A null result is uncertainty,
@@ -58,9 +41,24 @@ function isNonEmptyString(value) {
58
41
  return typeof value === 'string' && Boolean(value.trim());
59
42
  }
60
43
 
44
+ /**
45
+ * The hook requires pid judgment plus a fresh heartbeat. Identity uncertainty stays live here
46
+ * (fail-open), while the identity module deliberately treats a missing heartbeat as unverified;
47
+ * isHeartbeatFresh() keeps its pre-Plan_20 missing-file compatibility for this separate question.
48
+ */
61
49
  function recognizedStatus(runDir, status, options) {
62
50
  const heartbeatFresh = isHeartbeatFresh(runDir)
63
51
  && (options.requireConfirmedIdentity !== true || heartbeatAge(runDir) !== null);
52
+ // Judged last: an identity probe can spawn PowerShell, and every hook call scans every project,
53
+ // so a record already ruled out by its fields or a stale heartbeat must never pay for one.
54
+ const judged = () => {
55
+ const liveness = runLiveness({ runDir, status });
56
+ // Plan_31 needs confirmed process identity before warning about paid work that TaskStop leaves
57
+ // behind; existing lock/reply guards retain the fail-open default for uncertain identities.
58
+ return options.requireConfirmedIdentity === true
59
+ ? liveness.identity === IDENTITY_ALIVE
60
+ : liveness.processMayBeAlive === true;
61
+ };
64
62
  return Boolean(status)
65
63
  && typeof status === 'object'
66
64
  && !Array.isArray(status)
@@ -70,7 +68,7 @@ function recognizedStatus(runDir, status, options) {
70
68
  && isNonEmptyString(status.slug)
71
69
  && isNonEmptyString(status.repo)
72
70
  && heartbeatFresh
73
- && isPidAlive(status.pid, runDir, status, options);
71
+ && judged();
74
72
  }
75
73
 
76
74
  function readStatus(runDir) {
@@ -33,9 +33,10 @@ import { AGENTS } from '../lib/agents.mjs';
33
33
  import { runOrderMismatch, transcriptOrderId } from '../lib/dispatcher-order.mjs';
34
34
  import { recognizeHostRefusal } from '../lib/host-refusal.mjs';
35
35
  import { readJsonFileSync } from '../lib/json-file.mjs';
36
+ import { runLiveness } from '../lib/meta/run-liveness.mjs';
36
37
  import { resolveProjectRunsDir } from '../lib/runner/project-dir.mjs';
37
38
  import { runsRoot } from '../lib/runner/runs-root.mjs';
38
- import { isPidAlive, liveRuns, normalizePath, recentRuns } from './live-runs.mjs';
39
+ import { liveRuns, normalizePath, recentRuns } from './live-runs.mjs';
39
40
  import { FORM, MAX_STATE_BLOCKS, STATE, takeTry } from './guard-tries.mjs';
40
41
  import { parseReply } from './reply-parser.mjs';
41
42
  import {
@@ -233,7 +234,9 @@ if (fs.existsSync(statusPath)) {
233
234
  }
234
235
 
235
236
  if (runStatus?.state === 'running') {
236
- if (isPidAlive(runStatus.pid)) {
237
+ // Plan_57 D5/D27: a bare pid mistook a reused foreign process for this run's worker.
238
+ // The shared judge uses run identity and keeps unverified processes live (fail open).
239
+ if (runLiveness({ runDir, status: runStatus }).processMayBeAlive) {
237
240
  blockState(
238
241
  liveRunReason,
239
242
  liveRunStop(MAX_STATE_BLOCKS, runStatus, runDir),