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
+28 -12
View File
@@ -1569,11 +1569,21 @@ at a live rollout file, so reading the JSONL avoids coupling to a schema the
next Codex release renames. `rolloutFiles()` is the single function to re-point
if that changes.
### Codex declared-stage reliability (`server/lib/codex-nudge.js`)
A written detection then **also advances the lane's declared `stage`** —
`setStage(lane.id, { stage: nodeId, keepDetection: true })`, the only place in
CCAM where an inference moves the blue `current` ring. It is gated on
`recordDetection` having written, so it inherits forward-only/declared-wins/TTL
unchanged; it passes no `evidence`, so the node renders `passed-no-evidence`
(amber) and never `done`; and `keepDetection` is the sole exception to
`setStage`'s clearing of the detection columns, so the card still shows which
command moved it rather than looking like an agent's own claim. No template's
last node carries `detect` rules, so a lane can never be auto-declared `done`.
Detection alone only paints the amber badge; a codex lane's *declared* stage
depends on the session calling `ccam stage` itself, so two mechanisms make
that more likely without weakening declared-wins/inferred-never-`done`:
### Codex evidence reliability (`server/lib/codex-nudge.js`)
Auto-advance moves the ring but can never supply evidence; only the session's
own `ccam stage --evidence` call can, so three mechanisms make that more likely
without weakening declared-wins/inferred-never-`done`:
- **Opening-prompt priming** (`LaneConsolePane.tsx`'s `promptPrefill`): a codex
pane's first prompt is pre-filled with a plain-text checklist of
@@ -1581,17 +1591,23 @@ that more likely without weakening declared-wins/inferred-never-`done`:
— the same up-front nudge a Claude lane gets from its pre-filled
`/ccam:ship-feature-lane` skill command, in plain text since Codex has no
slash commands.
- **Stale-detection nudge** (`sweepCodexNudges`, an unref'd 60 s interval from
- **`AGENTS.md` stage block** (`server/lib/lane-agents.js`): on a `codex` lane,
`ccam lanes agents install` writes a marker-delimited
`<!-- ccam:stage:start -->` section into `<lane cwd>/AGENTS.md` instead of
copying the two agent templates into `.claude/agents/`, which Codex never
reads. Appended to an existing file (user prose preserved), and a reinstall
replaces exactly that block.
- **Missing-evidence nudge** (`sweepCodexNudges`, an unref'd 60 s interval from
`server/index.js`, tunable via `DASHBOARD_CODEX_NUDGE_MS`, `0` disables):
when a codex lane's fresh (non-stale) `detected_stage` has sat ahead of its
declared `stage` for at least two minutes and hasn't already been nudged for
that exact detected stage, it types a one-line `[ccam] ... run: ccam stage
...` reminder into the lane's live tmux pane via `tmux.sendText` (skipped if
when a codex lane's current stage has been held for at least two minutes with
`stages[stage].evidence` still null and hasn't already been nudged for that
exact stage, it types a one-line `[ccam] ... run: ccam stage ... --evidence
"..."` reminder into the lane's live tmux pane via `tmux.sendText` (skipped if
the pane isn't running an interactive program — `pty-run.js`'s
`SHELL_COMMANDS` — since Enter there would execute the reminder as a shell
command instead of submitting it as a chat turn). It never writes `stage` or
`detected_stage` — any resulting declaration is still one the codex session
made itself, through the real `ccam stage` call.
command instead of submitting it as a chat turn). It never writes `stage`,
`stages` or `detected_stage` — any resulting evidence is still something the
codex session declared itself, through the real `ccam stage` call.
---