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:
+62
@@ -192,6 +192,7 @@ async function api(method, pathname, body, options = {}) {
|
||||
}
|
||||
const get = (p, b, options) => api("GET", p, undefined, options);
|
||||
const post = (p, b, options) => api("POST", p, b, options);
|
||||
const patch = (p, b, options) => api("PATCH", p, b, options);
|
||||
|
||||
/**
|
||||
* Print the standard "server is not running" indicator and exit 1. Every
|
||||
@@ -2262,6 +2263,43 @@ async function cmdLanesIntegration(args) {
|
||||
process.exitCode = enabled ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ccam lanes pipeline [<template-id>] [<id>]` — show or switch which pipeline
|
||||
* template a lane renders against. `ccam lanes add --pipeline` could only set
|
||||
* this at creation time, so every lane added from the dashboard's "+ Add lane"
|
||||
* was stuck on `default` with no way back to a 16-node template.
|
||||
*/
|
||||
async function cmdLanesPipeline(args) {
|
||||
const target = args.find((arg) => !arg.startsWith("--") && !/^\d+$/.test(arg));
|
||||
const resolved = await resolveLaneArg(args.filter((a) => a !== target));
|
||||
if (!resolved) return;
|
||||
|
||||
if (!target) {
|
||||
const { lane } = await get(`/api/lanes/${resolved.laneId}`);
|
||||
const { pipelines } = await get("/api/lanes/pipelines");
|
||||
console.log(`lane #${lane.id} → ${lane.pipeline} (${lane.pipeline_nodes.length} nodes)`);
|
||||
console.log(`available: ${pipelines.map((p) => `${p.id} (${p.nodes.length})`).join(", ")}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const { lane } = await patch(`/api/lanes/${resolved.laneId}`, { pipeline: target });
|
||||
console.log(
|
||||
`${c.green("✔")} lane #${lane.id} → pipeline ${lane.pipeline} ` +
|
||||
`(${lane.pipeline_nodes.length} nodes, stage ${lane.stage}, ${lane.progress}%)`
|
||||
);
|
||||
// Switching templates re-resolves the SAME declared stage string against a
|
||||
// different node list, so a stage that meant something in the old pipeline
|
||||
// can land nowhere in the new one. Same warning as `ccam stage`, same reason.
|
||||
if (!lane.pipeline_nodes.some((n) => n.state === "current")) {
|
||||
console.error(
|
||||
c.yellow(
|
||||
`! the lane's current stage "${lane.stage}" matches no node in "${lane.pipeline}" — ` +
|
||||
`declare one of: ${lane.pipeline_nodes.map((n) => n.id).join(", ")}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function cmdFeatureShow(args) {
|
||||
const slug = args.find((arg) => !arg.startsWith("--"));
|
||||
if (!slug) {
|
||||
@@ -2315,6 +2353,22 @@ async function cmdStage(args) {
|
||||
result: flag("result"),
|
||||
});
|
||||
console.log(`lane #${lane.id} → ${lane.stage} (${lane.progress}%)`);
|
||||
|
||||
// A stage name matching no node (nor alias) still stores — setStage takes the
|
||||
// string verbatim — but phaseIdx() then returns -1, so nothing renders as
|
||||
// `current` and progress reads 0. Warn, never fail: a typo must not break a
|
||||
// declaration the pipeline can still record, but it must not pass silently.
|
||||
// Skipped for `--result fail`, which paints the node `failed` rather than
|
||||
// `current` and would otherwise look identical to an unknown stage.
|
||||
const nodes = lane.pipeline_nodes || [];
|
||||
if (flag("result") !== "fail" && nodes.length && !nodes.some((n) => n.state === "current")) {
|
||||
console.error(
|
||||
c.yellow(
|
||||
`! "${stage}" matches no node in pipeline "${lane.pipeline}" — recorded, but the ` +
|
||||
`pipeline map won't show it. Nodes: ${nodes.map((n) => n.id).join(", ")}`
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command catalog ─────────────────────────────────────────────────────────
|
||||
@@ -2425,6 +2479,11 @@ const COMMAND_GROUPS = [
|
||||
"[<path>]",
|
||||
"Validate a profile (path defaults to cwd, not a lane id)",
|
||||
],
|
||||
[
|
||||
"lanes pipeline",
|
||||
"[<template-id>] [<id>]",
|
||||
"Show, or switch, which pipeline template a lane renders against",
|
||||
],
|
||||
[
|
||||
"lanes reset|remove|purge",
|
||||
"<id> [--force] [--keep-db] --yes",
|
||||
@@ -3300,6 +3359,9 @@ async function runCommand(argv) {
|
||||
if (rest[0] === "integration") {
|
||||
return cmdLanesIntegration(rest.slice(1));
|
||||
}
|
||||
if (rest[0] === "pipeline") {
|
||||
return cmdLanesPipeline(rest.slice(1));
|
||||
}
|
||||
if (rest[0] === "gc") {
|
||||
return cmdLanesGc(rest.slice(1));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user