fix(lanes): revive dead detect rules and stop alias-blind detection badges

Two defects that made the 16-node ship-feature pipeline look like stage
detection was broken:

- stage-detect matched each rule against a JOIN of the tool input's
  identifying fields. An Agent event really carries description + prompt +
  subagent_type, so `^Explore$` (intake) and `^Plan$` (plan) could never
  fire and both nodes shipped inert — the map jumped straight to
  `implementing`. Rules are now tested one field at a time, which also
  stops a rule matching across the seam between two unrelated fields.

- `detectionLeadsDeclaration` (bin/ccam.js) and `detectionLeads`
  (LaneStripCard.tsx) located the declaration with `indexOf(lane.stage)`.
  Most of what the skill declares is an ALIAS (`tests`, `booting`, `ship`,
  `gate-blocked`), which is not a node id, so indexOf answered -1 and every
  detection — however far behind — was badged as leading. Both now use the
  node the server already resolved it onto (`state === "current"`).

server 1346/1346, client 447/447.
This commit is contained in:
2026-08-28 10:05:24 +07:00
parent fd7f57929c
commit 82263a0ab5
6 changed files with 218 additions and 27 deletions
+16 -4
View File
@@ -838,10 +838,14 @@ key back onto its node, so a stage declared by alias keeps its record — and it
node has both a canonical record and an alias record, the canonical one wins.
The same rule governs `ccam lanes`: the inferred stage is printed only when it
leads the declared one (see "Viewing lanes" below), and `LaneCard.tsx`'s
`detectionLeadsDeclaration` shows its `auto: <stage>` chip under the identical
condition — the terminal and the browser never disagree about which one is
the headline.
leads the declared one (see "Viewing lanes" below), and `LaneStripCard.tsx`'s
`detectionLeads` shows its badge under the identical condition — the terminal
and the browser never disagree about which one is the headline. Both locate the
declaration by the node the server already resolved it onto (the one whose
`state` is `current`), never by looking `lane.stage` up as a node id: most of
what the ship-feature skill declares is an ALIAS (`tests`, `booting`, `ship`,
`gate-blocked`), which is not an id, and an id lookup scores it -1 — every
detection, however far behind, would then read as leading.
### Which lane a signal is credited to
@@ -881,6 +885,14 @@ contain whole code blocks:
- `description`
- `subagent_type`
Each rule is tested against these fields **one at a time**, never against a join
of them. An `Agent` event really carries `description` + `prompt` +
`subagent_type` together, so a rule anchored to the agent's identity —
`{"tool": "Agent", "match": "^Explore$"}` in `ship-feature.json` — would never
fire against a concatenation of all three, and a rule could otherwise match
across the seam between two unrelated fields. Write anchored rules against the
one field that identifies the tool call.
The signal shown to the user is the span the rule's regex actually matched,
plus 24 characters of context on each side, with an ellipsis marking either end
that was trimmed. A shell line like