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
+14 -3
View File
@@ -17,11 +17,22 @@ const LIVENESS_DOT: Record<Lane["liveness"], string> = {
dead: "bg-status-danger",
};
/** Whether the inferred stage sits ahead of the declared one in node order. */
/**
* Whether the inferred stage sits ahead of the declared one in node order.
*
* The declaration is located by the node the server resolved it onto (`state
* === "current"`), not by `indexOf(lane.stage)`: a stage declared under an
* ALIAS (`tests`, `booting`, `ship` — most of what the ship-feature skill
* declares) is not a node id, so indexOf answers -1 and a detection sitting
* well BEHIND the declaration would still be badged as the live one.
*/
function detectionLeads(lane: Lane): boolean {
if (!lane.detected_stage) return false;
const ids = lane.pipeline_nodes.map((n) => n.id);
return ids.indexOf(lane.detected_stage) > ids.indexOf(lane.stage);
const nodes = lane.pipeline_nodes;
return (
nodes.findIndex((n) => n.id === lane.detected_stage) >
nodes.findIndex((n) => n.state === "current")
);
}
export default function LaneStripCard({