feat(lanes): auto-advance a codex lane's stage from its rollout detection

A codex session emits no hooks and rarely calls `ccam stage` on its own, so
its card sat at `idle` forever while the amber "detected" badge was the only
thing that ever moved. `codex-ingest.js` now advances the declared `stage`
from its own written detection — the one place in CCAM where an inference
moves the blue `current` ring, and only for `codex` lanes.

It stays honest: gated on `recordDetection` having written (so forward-only,
declared-wins and the detection TTL are inherited unchanged), passes NO
evidence (the node renders `passed-no-evidence` amber, never `done`), and no
template's last node carries detect rules, so a lane can never be
auto-declared `done`. `setStage` grows a `keepDetection` option — its sole
caller — so the detection columns survive the advance and the card keeps
showing which command moved it instead of looking like an agent's own claim.

Two levers then get the evidence auto-advance cannot supply:

- `codex-nudge.js` now nudges on a stage held 2+ min with no evidence,
  instead of on a detection sitting ahead of the declared stage — after
  auto-advance the old condition can never fire, since the two now match.
- `ccam lanes agents install` on a codex lane writes the marker-delimited
  `<!-- ccam:stage:start -->` block into the lane's own AGENTS.md — the one
  instruction channel Codex reads — instead of copying agent templates into
  `.claude/agents/`, which it never loads. Appended, user prose preserved,
  reinstall replaces exactly that block.

Verified with a real `codex exec` run: stage moved to `watching-pr` with
`evidence: null` and the detection signal intact. 1364 server tests pass.
This commit is contained in:
2026-09-03 16:51:02 +07:00
parent 8a812a174b
commit e109d2804c
13 changed files with 318 additions and 106 deletions
+57 -20
View File
@@ -73,11 +73,14 @@ Codex does not emit Claude Code hooks (sessions, agent events, tool-execution tr
**These are not bugs; they reflect Codex's architecture.** The stage you declare via `ccam stage` (or that the driving agent calls itself) still works; liveness is tracked by the lane's tmux pane (if the stack is up) and `dashboard_runs` table (recording every invocation). Cost tracking for Codex runs would require parsing Codex's own logs, a separate design not yet implemented.
**Stage detection is not in that list.** It does work for a codex lane, from
Codex's own on-disk rollout journals rather than from hooks — see "Stage
detection" below. What a codex lane cannot get for free is a *declared* stage:
the run console never pre-fills a Claude slash command into a codex pane (Codex
Codex's own on-disk rollout journals rather than from hooks, and unlike a claude
lane a written detection there also **advances the lane's declared `stage`** —
see "Stage detection" below. What a codex lane cannot get for free is
*evidence*: an auto-advanced node renders `passed-no-evidence` (amber), never
`done`, because only the session itself can say what it actually finished. The
run console never pre-fills a Claude slash command into a codex pane (Codex
would read `/ccam:ship-feature-lane` as prose), so a codex session declares
stages only when something tells it to. Two things do:
evidence only when something tells it to. Three things do:
- **An opening prompt checklist.** The Run Setup panel pre-fills a codex pane's
first prompt with a plain-text list of `ccam stage <stage> --tool codex`
@@ -86,14 +89,20 @@ stages only when something tells it to. Two things do:
the same up-front priming a Claude lane gets from its pre-filled
`/ccam:ship-feature-lane` skill command, just as plain text since Codex has
no slash commands. See `LaneConsolePane.tsx`'s `promptPrefill`.
- **A stale-detection nudge.** `server/lib/codex-nudge.js` sweeps every 60s
- **An `AGENTS.md` block.** `ccam lanes agents install` on a codex lane writes
a marker-delimited `<!-- ccam:stage:start -->` section into
`<lane cwd>/AGENTS.md` — the one instruction channel Codex actually reads,
since it never loads `.claude/agents/`. Appended, never overwriting your own
prose, and a reinstall replaces exactly that block. (On a `claude` lane the
same command still copies the two agent templates instead.)
- **A missing-evidence nudge.** `server/lib/codex-nudge.js` sweeps every 60s
(`DASHBOARD_CODEX_NUDGE_MS` to change, `0` to disable) for a codex lane whose
fresh, non-stale `detected_stage` has sat ahead of its declared `stage` for
at least two minutes, and types a one-line `[ccam] ... run: ccam stage ...`
reminder straight into that lane's live tmux pane (`tmux.sendText`, skipped
if the pane is at a bare shell prompt). It never writes `stage` or
`detected_stage` itself — declared-wins and inferred-never-`done` are
untouched — it only prods the session into making the real declaration.
current stage has been held for at least two minutes with no evidence
recorded, and types a one-line `[ccam] ... run: ccam stage ... --evidence
"..."` reminder straight into that lane's live tmux pane (`tmux.sendText`,
skipped if the pane is at a bare shell prompt). It never writes `stage`,
`stages` or `detected_stage` itself — the evidence that results is a real
declaration the session made.
## Destructive lane actions
@@ -852,9 +861,25 @@ shape a Claude hook delivers:
| anything else (messages, reasoning, web search, tool OUTPUT) | ignored |
Everything past that point is shared: the same templates, the same
forward-only/declared-wins rules, the same amber-never-green boundary. Two
forward-only/declared-wins rules, the same amber-never-green boundary. Three
codex-only details:
- **A written detection also advances the lane's declared `stage`.** This is
the one place in CCAM where an inference moves the blue `current` ring, and
it exists because nothing else can: a codex session has no hook and rarely
calls `ccam stage` on its own, so without it the card sits at `idle` forever
while the amber badge is the only thing that ever moves. It is gated on
`recordDetection` having *written*, so it inherits that function's
forward-only, declared-wins and TTL guards unchanged — a detection behind the
declared stage still advances nothing. The `setStage` call passes **no
evidence**, so every node it moves through renders `passed-no-evidence`
(amber), never `done`: a detection sees a tool call, never its outcome. And
it passes `keepDetection: true`, the sole exception to `setStage`'s clearing
of the detection columns — here the detection is the *reason* for the
declaration, so the card keeps showing which command caused it rather than
looking like a claim the agent made. The last node of every template carries
no `detect` rules, so this can never auto-declare a lane `done`.
- Only bytes appended since the last pass are read. On the first sight of a
journal only its recent tail is read, and nothing at all when that file is
already idle — replaying a thread that finished days ago would paint stages
@@ -869,14 +894,15 @@ codex-only details:
`provisioning` or `failed`). It writes nothing else: not `stage`, not
`notes`, not `slot`/`ports`.
**A codex lane's *declared* stage is a separate matter.** CCAM cannot inject an
instruction into a CLI it merely spawns, so a codex session declares a stage
only if something tells it to — an `AGENTS.md` in the repository (this repo has
one) is the only lever. A codex lane in a repo without one shows detected stages
and no declarations. Note that from inside a codex session `ccam stage` needs to
know which lane it is: a pane the dashboard started carries `CCAM_LANE_ID`, and
anywhere else `--tool codex` disambiguates (see "Which lane a CLI command
addresses" below).
**A codex lane's *evidence* is a separate matter.** Auto-advance moves the ring
but can never supply evidence, so every node a codex lane passes through stays
amber until the session itself runs `ccam stage <node> --evidence "..."`. CCAM
cannot inject an instruction into a CLI it merely spawns, so an `AGENTS.md` in
the working copy is the only lever — `ccam lanes agents install` writes the
marker-delimited block for you (see "Installing the QC/gate agents"). Note that
from inside a codex session `ccam stage` needs to know which lane it is: a pane
the dashboard started carries `CCAM_LANE_ID`, and anywhere else `--tool codex`
disambiguates (see "Which lane a CLI command addresses" below).
### The evidence boundary
@@ -1413,6 +1439,17 @@ ccam lanes agents install
Writes both agent definitions (from the repo's own `agents/`, the same files the plugin ships) into `<lane>/.claude/agents/` and adds that directory to the lane's local `.git/info/exclude` (never the tracked `.gitignore` — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as `ccam lanes proof-link` — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a CCAM upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
**On a `codex` lane the same command installs something else.** Codex never
reads `.claude/agents/`, so copying agent definitions there would be dead
files. Instead it writes the stage-declaration block into
`<lane>/AGENTS.md` — the project doc Codex *does* load from the working
directory — delimited by `<!-- ccam:stage:start -->` / `<!-- ccam:stage:end -->`
so a reinstall replaces exactly that block and nothing else. The file is
appended to when it already exists (your own prose is preserved) and created
when it does not; `installed` comes back as `["AGENTS.md"]`. This is the lever
that gets a codex session to supply the *evidence* auto-advance cannot — see
"Stage detection".
**Credentials are not embedded.** Unlike Shipyard's original per-lane agent generation, these templates carry no QA account credentials — this repo has no seeded-QA-account system yet. `qc-local` degrades gracefully: if it hits a login page with no credentials block present, it reports `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile` rather than guessing.
**`ticketer`, `dev-qc`, and `pr-reviewer`** are not ported yet — `ticketer` is invoked by Stage 9 (skipped), and `dev-qc` by Stage 13's dev-QC half (unavailable); `pr-reviewer` isn't referenced anywhere in this skill's text.
+3 -1
View File
@@ -106,7 +106,9 @@ root holds all five (`qc-local`, `senior-gate-reviewer`, `backend-reviewer`,
`.claude/agents/` — the highest-priority agent source — which is what makes the
bare `subagent_type: qc-local` in the pipeline skill resolve there. One source
file, two ways to reach it; the skill names the bare form first and falls back
to the `ccam:` form.
to the `ccam:` form. On a `codex` lane the same command installs the
`<!-- ccam:stage:start -->` block in the lane's `AGENTS.md` instead — Codex
never reads `.claude/agents/`.
Because the plugin ships the hooks itself, `npm run install-hooks` is not needed
for plugin users — and must not be run alongside it. Events carry no id, so two