fix(lanes): auto-detect and report stage for codex lanes (issue 11)
A lane on the 16-node `ship-feature` template showed an untouched pipeline
map and never declared a stage. Four independent causes, fixed at their
shared choke points:
1. Codex emits no hooks, so `touchLaneFromHook` — the only caller of
`stage-detect.detect()`, and correctly pinned to `claude` — could never
see a `codex` lane. New `server/lib/codex-ingest.js` tails Codex's own
rollout journals (`$CODEX_HOME/sessions/**/rollout-*.jsonl`), maps its
tool calls onto the hook-shaped `{tool_name, tool_input}` the matcher
already understands, and feeds the existing detect path plus an
idle/running status mirror under the same guard hooks use. Polled every
`DASHBOARD_CODEX_POLL_MS` ms (default 15000, `0` disables).
2. `resolveLaneArg` matched on cwd alone, so with a claude and a codex lane
at one directory (the composite `idx_lanes_cwd_tool`) a `ccam stage`
call could drive the sibling lane's card. It now resolves by
`--lane` → `CCAM_LANE_ID` → `--tool`/harness marker → owning lane, and
errors naming both candidates rather than picking one. Every pane the
dashboard starts carries `CCAM_LANE_ID` (`tmux new-session -e`, or an
assignment prefix when typing into a live shell pane).
3. `recordDetection`'s declared-wins veto had no exit: a lane left at the
pipeline's last node rejected every detection for the rest of its life.
A declaration on the LAST node no longer vetoes — the pipeline is over
there, so a later detection is new work. Nothing else is relaxed, and
detection still never writes `stage` nor renders `done`.
4. The run console pre-filled the pipeline's Claude slash command into
codex panes, which Codex reads as prose — so the skill that would have
called `ccam stage` never ran. Claude lanes only now.
A codex lane's *declared* stage still depends on the session calling
`ccam stage`; `AGENTS.md` is the only lever CCAM has, so it now says so.
Verified: server 1345/1345, client 441/441, and end-to-end against a real
codex lane row (detected_stage/detected_signal set, declared stage
untouched, no node rendered done).
This commit is contained in:
+12
-2
@@ -327,7 +327,8 @@ function setStage(id, { stage, status, evidence, note, result } = {}) {
|
||||
* not drag a lane back to `plan`);
|
||||
* - declared wins: the lane's DECLARED stage index is strictly less than
|
||||
* the detection's (a lane already declared at `review` ignores an
|
||||
* `implement` detection).
|
||||
* `implement` detection) — UNLESS the declaration is the pipeline's last
|
||||
* node, which means the pipeline is over and a new detection is new work.
|
||||
* Otherwise touches nothing and reports why: `behind-detected`,
|
||||
* `behind-declared`, or `unknown-node`.
|
||||
*
|
||||
@@ -351,8 +352,17 @@ function recordDetection(id, { nodeId, signal } = {}) {
|
||||
: phaseIdx(pipeline, lane.detected_stage);
|
||||
if (nodeIdx <= detectedIdx) return { written: false, reason: "behind-detected" };
|
||||
|
||||
// Declared wins, with ONE exception: a declaration sitting on the pipeline's
|
||||
// LAST node means the pipeline is finished, so a later detection is new work
|
||||
// rather than a stale rerun of what the claim already covers. Without it a
|
||||
// lane left at `done` rejects every detection for the rest of its life, which
|
||||
// reads to a user as "auto-detect is broken". Still paints only the amber
|
||||
// detected badge — `stage` is untouched here, as everywhere in this function.
|
||||
const declaredIdx = phaseIdx(pipeline, lane.stage);
|
||||
if (declaredIdx >= nodeIdx) return { written: false, reason: "behind-declared" };
|
||||
const declarationIsTerminal = declaredIdx === pipeline.nodes.length - 1;
|
||||
if (!declarationIsTerminal && declaredIdx >= nodeIdx) {
|
||||
return { written: false, reason: "behind-declared" };
|
||||
}
|
||||
|
||||
db.prepare(
|
||||
"UPDATE lanes SET detected_stage = ?, detected_signal = ?, detected_at = ? WHERE id = ?"
|
||||
|
||||
Reference in New Issue
Block a user