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:
+24
-2
@@ -77,6 +77,27 @@ function phaseIdx(pipeline, stage) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index every recorded stage by the node it resolves to, so a stage declared
|
||||
* under an ALIAS keeps its record — and therefore its evidence. Keying the
|
||||
* records by node id alone (what `lane.stages` is keyed by, verbatim from the
|
||||
* declaration) silently drops `--evidence` the moment an agent says `e2e`
|
||||
* instead of `e2e-feature`, which is exactly what aliases exist to allow.
|
||||
* A record stored under the node's own id always wins over an alias record for
|
||||
* the same node; a key matching no node is skipped.
|
||||
*/
|
||||
function stageRecords(pipeline, stages) {
|
||||
const byIdx = new Map();
|
||||
for (const [key, rec] of Object.entries(stages || {})) {
|
||||
const i = phaseIdx(pipeline, key);
|
||||
if (i === -1) continue;
|
||||
const isCanonical = key.toLowerCase() === pipeline.nodes[i].id.toLowerCase();
|
||||
if (byIdx.has(i) && !isCanonical) continue;
|
||||
byIdx.set(i, rec);
|
||||
}
|
||||
return byIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render state per node:
|
||||
* failed — the stage recorded result "fail"
|
||||
@@ -86,10 +107,10 @@ function phaseIdx(pipeline, stage) {
|
||||
* pending — not reached
|
||||
*/
|
||||
function nodeStates(pipeline, lane) {
|
||||
const stages = lane.stages || {};
|
||||
const records = stageRecords(pipeline, lane.stages);
|
||||
const cur = phaseIdx(pipeline, lane.stage);
|
||||
return pipeline.nodes.map((n, i) => {
|
||||
const rec = stages[n.id];
|
||||
const rec = records.get(i);
|
||||
let state;
|
||||
if (rec && rec.result === "fail") state = "failed";
|
||||
else if (i === cur) state = "current";
|
||||
@@ -111,6 +132,7 @@ module.exports = {
|
||||
listPipelines,
|
||||
getPipeline,
|
||||
phaseIdx,
|
||||
stageRecords,
|
||||
nodeStates,
|
||||
progressPct,
|
||||
reload,
|
||||
|
||||
Reference in New Issue
Block a user