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:
@@ -186,6 +186,31 @@ describe("ccam stage", () => {
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(`${r.stdout}${r.stderr}`, /no lane/i);
|
||||
});
|
||||
|
||||
it("warns, but still records, a stage name matching no pipeline node", async () => {
|
||||
// setStage stores the string verbatim, so a typo'd stage is accepted and
|
||||
// then renders nowhere (phaseIdx -1, progress 0). Silent acceptance is how
|
||||
// a lane ends up looking unstarted for an entire pipeline run.
|
||||
const r = await cli(["stage", "revieww"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /revieww/);
|
||||
assert.match(r.stderr, /matches no node/i);
|
||||
assert.match(r.stderr, /revieww/);
|
||||
});
|
||||
|
||||
it("does not warn for a stage declared by alias", async () => {
|
||||
const r = await cli(["stage", "planning"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.doesNotMatch(r.stderr, /matches no node/i);
|
||||
});
|
||||
|
||||
it("does not warn for a valid stage reported as failed", async () => {
|
||||
// --result fail paints the node `failed`, not `current`; the warning must
|
||||
// not read that as an unknown stage.
|
||||
const r = await cli(["stage", "review", "--result", "fail"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.doesNotMatch(r.stderr, /matches no node/i);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes add", () => {
|
||||
@@ -248,6 +273,35 @@ describe("ccam lanes add", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("switches an existing lane's pipeline, and shows it when given no target", async () => {
|
||||
// `--pipeline` was creation-only, so every lane added from the dashboard
|
||||
// was pinned to `default`'s 8 nodes with no way to reach a longer template.
|
||||
const show = await cli(["lanes", "pipeline"], LANE_DIR);
|
||||
assert.equal(show.status, 0, show.stderr);
|
||||
assert.match(show.stdout, /default/);
|
||||
assert.match(show.stdout, /available:.*ship-feature/);
|
||||
|
||||
const set = await cli(["lanes", "pipeline", "ship-feature"], LANE_DIR);
|
||||
assert.equal(set.status, 0, set.stderr);
|
||||
assert.match(set.stdout, /ship-feature/);
|
||||
assert.match(set.stdout, /16 nodes/);
|
||||
|
||||
const back = await cli(["lanes", "pipeline", "default"], LANE_DIR);
|
||||
assert.equal(back.status, 0, back.stderr);
|
||||
assert.match(back.stdout, /8 nodes/);
|
||||
});
|
||||
|
||||
it("refuses an unknown pipeline id instead of silently falling back to default", async () => {
|
||||
// getPipeline() returns the default template for an unknown id, so without
|
||||
// a write-side check a typo would store, render `default`, and look fine.
|
||||
const r = await cli(["lanes", "pipeline", "no-such-pipeline"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(`${r.stdout}${r.stderr}`, /unknown pipeline/i);
|
||||
|
||||
const after = await cli(["lanes", "pipeline"], LANE_DIR);
|
||||
assert.match(after.stdout, /default/);
|
||||
});
|
||||
|
||||
it("provisions a managed worktree lane and reports it ready", async () => {
|
||||
const r = await cli(
|
||||
["lanes", "add", "--repo", SOURCE_REPO, "--title", "CLI worktree", "--slug", "cli-worktree"],
|
||||
|
||||
Reference in New Issue
Block a user