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
+39
View File
@@ -95,6 +95,29 @@ test("flattenInput handles awkward tool-input shapes without traversing deeply",
assert.equal(flattenInput({ nested: { command: "never recurse" } }), "");
});
test("matches a rule per field, so an anchored rule is not killed by its neighbours", () => {
// A real Agent input carries {description, prompt, subagent_type}. Matched
// against a join of the three, `^Explore$` can never fire and the intake and
// plan rules ship inert; matched per field it hits subagent_type verbatim.
const anchored = {
nodes: [{ id: "intake", detect: [{ tool: "Agent", match: "^Explore$" }] }],
};
const event = {
tool_name: "Agent",
tool_input: {
description: "Find the stage rules",
prompt: "Search the repo for stage detection rules and report back.",
subagent_type: "Explore",
},
};
assert.equal(detect(anchored, event).nodeId, "intake");
// The seam between two fields is not matchable either — "rules Search" only
// exists once description and prompt are glued together.
const seam = { nodes: [{ id: "intake", detect: [{ tool: "Agent", match: "rules Search" }] }] };
assert.equal(detect(seam, event), null);
});
test("caps a long signal so it never inflates a DB column or a title attribute", () => {
const longCommand = `git push ${"x".repeat(4990)}`;
const result = detect(pipeline, { tool_name: "Bash", tool_input: { command: longCommand } });
@@ -512,6 +535,22 @@ test.describe("ship-feature pipeline template", () => {
const pipeline = getPipeline("ship-feature");
const cases = [
["intake", "Skill", { skill: "superpowers:brainstorming" }],
// Agent inputs always arrive with a description and a prompt alongside
// the type, which is exactly what these two anchored rules must survive.
[
"intake",
"Agent",
{
description: "Scout the lane code",
prompt: "Read-only sweep.",
subagent_type: "Explore",
},
],
[
"plan",
"Agent",
{ description: "Design the fix", prompt: "Propose an approach.", subagent_type: "Plan" },
],
["intake", "Bash", { command: "ccam feature activate lane-stage-detect" }],
["plan", "Skill", { skill: "superpowers:writing-plans" }],
["plan", "Write", { file_path: "docs/superpowers/specs/lane-foo.md" }],