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
+17 -1
View File
@@ -14,7 +14,11 @@
* tool-call payload onto the same `{tool_name, tool_input}` shape a Claude hook
* delivers, and feeds the EXISTING detect() + recordDetection() path — so a
* codex lane paints the same amber "detected" badge under the same rules
* (forward-only, declared-wins) and never renders `done`. Like
* (forward-only, declared-wins) and never renders `done`. A written detection
* then ALSO advances the lane's declared `stage` to that node, without
* evidence: codex lanes are the one place where nothing else can move the
* pipeline map, so the node renders `passed-no-evidence` (amber) rather than
* `done`, and codex-nudge.js asks the session for the real evidence. Like
* workflow-ingest.js, everything here is fail-safe: a malformed or partial
* journal throws only locally and is skipped, and a poll pass never throws.
*
@@ -309,6 +313,18 @@ function ingestCodexOnce({ broadcast } = {}) {
if (detection) {
try {
changed = lanesLib.recordDetection(lane.id, detection).written;
// Auto-advance: a codex session has no hook and rarely calls `ccam
// stage` on its own, so without this its card sits at `idle` forever
// while the amber badge is the only thing that ever moves. Gated on
// recordDetection having WRITTEN, so it inherits that function's
// forward-only + declared-wins + TTL guards for free. Deliberately
// passes NO evidence: the node renders `passed-no-evidence` (amber),
// never `done` — detection sees a tool call, never its outcome. The
// last node has no detect rules in any template, so this can never
// auto-declare a lane `done`.
if (changed) {
lanesLib.setStage(lane.id, { stage: detection.nodeId, keepDetection: true });
}
} catch {
/* lane deleted mid-pass, or sqlite busy — an inference is never worth throwing for */
}