feat(lanes): make the pipeline map track a skill's real progress
A lane's pipeline map only ever moved when a skill remembered to call `ccam stage`, and the ship-feature template shipped with no detection rules at all — so a lane driven by Superpowers skills sat at whatever stage it last declared, and the `gates` node was never declared by anything. Detection (`detect` rules on each node) now covers the Superpowers skill invocations and the `ccam`/`gh` commands the ship-feature-lane skill actually runs. It stays a safety net, not the mechanism: forward-only, never `done`, never overriding a declaration. Two rules were deliberately left out — `git diff` on `review` (this repo's own tests record it pinning a lane at `review` on a real session) and anything on `merged`/`done`. Stage vocabulary grows to 50 names over the same 16 nodes, following Shipyard's PHASES shape: sub-states like `migration-collision`, `e2e-scoped` and `gate-blocked` say WHY a lane sits on a node without the map growing a node per reason. Every alias has a source — the skill declares it, `default.json` uses it, or Shipyard's PHASES lists it. Two silent failures fixed along the way: - `lane.stages` is keyed by the raw declared string, so a stage declared under an alias lost its `--evidence` and rendered amber instead of green. `stageRecords` resolves each key onto its node. - `ccam stage <typo>` stored fine and then rendered nowhere. It now warns on stderr while still exiting 0. `ccam lanes pipeline` closes the gap that made all of this invisible: a lane could only be assigned a template at creation, and no screen in the web UI offers the choice, so every lane added from "+ Add lane" was stuck on `default`'s 8 nodes. An unknown template id is now refused rather than silently falling back to `default` on read. Also merges the repo's own `ship-feature` skill into the Superpowers workflow: it delegates planning/TDD/review/verification instead of restating them, and declares a stage at each phase.
This commit is contained in:
@@ -77,6 +77,44 @@ describe("pipelines", () => {
|
||||
assert.equal(byId.plan, "passed-no-evidence");
|
||||
});
|
||||
|
||||
it("keeps the evidence of a stage declared under an alias", () => {
|
||||
// `lane.stages` is keyed by the raw declared string, so `ccam stage
|
||||
// planning --evidence x` files the record under "planning" while the node
|
||||
// is "plan". Looking it up by node id alone loses the evidence and paints
|
||||
// a `done` node amber — silently, and only for agents that use an alias.
|
||||
const p = getPipeline(DEFAULT_PIPELINE_ID);
|
||||
const lane = {
|
||||
stage: "review",
|
||||
stages: { planning: { enteredAt: "2026-07-27T00:00:00Z", evidence: "docs/plan.md" } },
|
||||
};
|
||||
const byId = Object.fromEntries(nodeStates(p, lane).map((n) => [n.id, n.state]));
|
||||
assert.equal(byId.plan, "done");
|
||||
});
|
||||
|
||||
it("prefers a node-id record over an alias record for the same node", () => {
|
||||
const p = getPipeline(DEFAULT_PIPELINE_ID);
|
||||
const lane = {
|
||||
stage: "review",
|
||||
stages: {
|
||||
planning: { enteredAt: "2026-07-27T00:00:00Z", evidence: null },
|
||||
plan: { enteredAt: "2026-07-27T00:30:00Z", evidence: "docs/plan.md" },
|
||||
},
|
||||
};
|
||||
const byId = Object.fromEntries(nodeStates(p, lane).map((n) => [n.id, n.state]));
|
||||
assert.equal(byId.plan, "done");
|
||||
});
|
||||
|
||||
it("ignores a recorded stage matching no node instead of shifting the others", () => {
|
||||
const p = getPipeline(DEFAULT_PIPELINE_ID);
|
||||
const lane = {
|
||||
stage: "review",
|
||||
stages: { "totally-unknown": { enteredAt: "x", evidence: "y" } },
|
||||
};
|
||||
const byId = Object.fromEntries(nodeStates(p, lane).map((n) => [n.id, n.state]));
|
||||
assert.equal(byId.plan, "passed-no-evidence");
|
||||
assert.equal(byId.review, "current");
|
||||
});
|
||||
|
||||
it("computes progress from node position, 0 for an unknown stage", () => {
|
||||
const p = getPipeline(DEFAULT_PIPELINE_ID);
|
||||
assert.equal(progressPct(p, { stage: p.nodes[0].id, stages: {} }), 0);
|
||||
|
||||
Reference in New Issue
Block a user