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:
+123
-3
@@ -420,6 +420,15 @@ The dashboard web UI shows each lane as a card in a grid, with the selected lane
|
||||
|
||||
A lane moves through stages defined in a **pipeline template** (see "Custom pipeline templates" below). The default pipeline has eight stages: `intake`, `plan`, `implement`, `tests`, `review`, `gate`, `ship`, and `done`.
|
||||
|
||||
**A lane is created on `default` unless told otherwise, and nothing in the web UI changes that** — the "+ Add lane" flow has no template picker, so a lane that should render the 16-node `ship-feature` map will show `default`'s 8 nodes until it is switched:
|
||||
|
||||
```bash
|
||||
ccam lanes pipeline # which template this lane uses, and what else exists
|
||||
ccam lanes pipeline ship-feature # switch it
|
||||
```
|
||||
|
||||
Switching re-resolves the lane's existing declared `stage` against the new node list. A stage the old template knew may resolve to nothing in the new one; the command warns when that happens, and the next `ccam stage` fixes it.
|
||||
|
||||
The dashboard renders every node in the pipeline in one of five **states**:
|
||||
|
||||
| State | Color | Meaning |
|
||||
@@ -471,6 +480,20 @@ ccam stage <stage> [--lane <id>] [--cwd <path>] [--status <s>] [--evidence <text
|
||||
- `ship` / `pr` / `pr-open` / `publishing` / `commit` / `push`
|
||||
- `done` / `complete` / `completed` / `merged`
|
||||
|
||||
Aliases are **per template** — the `ship-feature` pipeline has its own set
|
||||
(see "Pipeline template: ship-feature" below). A name matching no node and no
|
||||
alias is still recorded verbatim, but `phaseIdx` then resolves it to nothing:
|
||||
no node renders `current` and progress reads `0`. The CLI prints a warning to
|
||||
stderr and still exits `0` in that case — a typo must not break a
|
||||
declaration the lane can record, but it must not pass silently either:
|
||||
|
||||
```
|
||||
! "revieww" matches no node in pipeline "default" — recorded, but the pipeline map won't show it. Nodes: intake, plan, implement, …
|
||||
```
|
||||
|
||||
The check is skipped for `--result fail`, which paints the node `failed`
|
||||
rather than `current`.
|
||||
|
||||
- `--lane <id>` (optional): the numeric lane ID. If omitted, the command resolves the lane by `cwd`.
|
||||
|
||||
- `--cwd <path>` (optional): working directory to match against a lane's cwd. If omitted, uses the current working directory. Useful when calling from outside the repo.
|
||||
@@ -626,7 +649,11 @@ proof would let a lane's pipeline map lie about what actually happened.
|
||||
The node the agent *declared* itself on is never flagged as detected, so it
|
||||
keeps its blue `current` ring — including when the declaration came in through
|
||||
an alias (`ccam stage coding` resolves to the `implement` node, and `stages` is
|
||||
keyed by the raw declared word, not the node id).
|
||||
keyed by the raw declared word, not the node id). Past nodes get the same
|
||||
protection: `stageRecords` (`server/lib/pipelines.js`) resolves every recorded
|
||||
key back onto its node, so a stage declared by alias keeps its record — and its
|
||||
`--evidence` — instead of reading as an inference or losing its `done`. Where a
|
||||
node has both a canonical record and an alias record, the canonical one wins.
|
||||
|
||||
The same rule governs `ccam lanes`: the inferred stage is printed only when it
|
||||
leads the declared one (see "Viewing lanes" below), and `LaneCard.tsx`'s
|
||||
@@ -772,8 +799,43 @@ accepts `git push`, `git -c core.hooksPath=/dev/null push` and the credential
|
||||
-helper form, while rejecting `git log … "push"`, `git commit -m "don't push"`,
|
||||
`docker push`, and `npm run push-docs`.
|
||||
|
||||
`intake`, `gate`, and `done` deliberately have no rules. `intake` is where a
|
||||
lane starts — there is no tool event that means "just claimed," so there is
|
||||
The `ship-feature` template (`server/data/pipelines/ship-feature.json`) carries
|
||||
its own rules, aimed at the commands and Superpowers skills its driving skill
|
||||
actually runs:
|
||||
|
||||
| Node | Detect rules |
|
||||
|---|---|
|
||||
| `intake` | `Skill` matching `brainstorming`; `Bash` matching `ccam … feature activate` |
|
||||
| `plan` | `Skill` matching `writing-plans`; `Write` matching `docs/superpowers/specs/lane-.*\.md` |
|
||||
| `implementing` | `Skill` matching `test-driven-development\|executing-plans\|subagent-driven-development\|systematic-debugging`; `Edit`/`Write` to any path NOT under `docs/superpowers/specs/` (resp. not under `docs/`) |
|
||||
| `gates` | `Bash` matching `ccam … hook ci-gate` or `ccam … sync-base --check` |
|
||||
| `e2e-feature` | `Bash` matching `ccam … up --qc` or `ccam … hook e2e` |
|
||||
| `review` | `Skill` matching `code-review\|requesting-code-review\|receiving-code-review` |
|
||||
| `qc` | `Agent` matching `qc-local`; `Bash` matching `ccam … lanes proof-link` |
|
||||
| `gate` | `Agent` matching `senior-gate-reviewer`; `Skill` matching `verification-before-completion` |
|
||||
| `publishing` | `Skill` matching `finishing-a-development-branch`; `Bash` matching a `git push` **invocation** (the same pattern `default.json`'s `ship` uses) |
|
||||
| `pr-open` | `Bash` matching `gh … pr create` |
|
||||
| `watching-pr` | `Bash` matching `gh … pr view` |
|
||||
| `e2e-feature-passed`, `qc-plan`, `reported`, `merged`, `done` | none — declaration-only |
|
||||
|
||||
Two rules are deliberately absent from this template. There is **no `git diff`
|
||||
rule on `review`**, for the reason the `default` template learned the hard way
|
||||
below; the `code-review` skill invocation is the honest signal. And `merged` /
|
||||
`done` carry no rule at all, because inference must never reach a terminal
|
||||
state — a test pins that.
|
||||
|
||||
Detection here is a **safety net, not the mechanism**: the `ship-feature-lane`
|
||||
skill declares every one of these stages with `ccam stage` itself. What
|
||||
detection adds is the stage an agent forgot after a context compaction, the
|
||||
heartbeat that keeps a working lane from reading STALLED, and coverage for a
|
||||
lane running Superpowers skills without the driving skill at all. It cannot
|
||||
substitute for the skill's own declarations: because `recordDetection` is
|
||||
forward-only AND never overrides a higher declared stage, a fix-loop re-entry
|
||||
that drops back to `gates` is invisible to detection — only the skill's
|
||||
`ccam stage gates` moves the lane back down.
|
||||
|
||||
`intake`, `gate`, and `done` in `default.json` deliberately have no rules.
|
||||
`intake` is where a lane starts — there is no tool event that means "just claimed," so there is
|
||||
nothing to detect. `gate` and `done` are explicitly out of scope for
|
||||
inference (see the task's "Out of scope" list): a gate's pass/fail is a human
|
||||
or skill decision, and `done` is the one state detection must never reach on
|
||||
@@ -1151,6 +1213,64 @@ The skill uses the `ship-feature` pipeline template, which defines the following
|
||||
|
||||
The Workspace page (`/run`) displays this template with nodes rendered in five states: `failed` (rejected), `current` (now), `done` (with evidence), `passed-no-evidence` (claimed or skipped), and `pending` (not reached).
|
||||
|
||||
### More stage names than nodes
|
||||
|
||||
Each node carries aliases, and they do two different jobs. Some absorb a
|
||||
near-miss (`implement` for `implementing`). Others are **sub-states**: a name
|
||||
the skill declares to say *why* the lane is sitting on a node, without adding a
|
||||
node to the map.
|
||||
|
||||
This is the shape Shipyard converged on — its dashboard renders 13 nodes while
|
||||
its `PHASES` table folds roughly 35 stage names onto them. A pipeline map is
|
||||
read at a glance across many lanes at once, so it stays coarse; the stage name
|
||||
is read one lane at a time, so it can be specific.
|
||||
|
||||
| Node | Near-miss aliases | Sub-states the skill declares |
|
||||
|---|---|---|
|
||||
| `intake` | `assigned`, `claimed`, `start` | `bootstrapping` |
|
||||
| `plan` | `planning`, `brainstorm`, `design` | — |
|
||||
| `implementing` | `implement`, `coding`, `build` | — |
|
||||
| `gates` | `pre-push-gate`, `tests` | `migration-collision`, `sync-conflict` |
|
||||
| `e2e-feature` | `e2e`, `live` | `booting`, `e2e-scoped` |
|
||||
| `e2e-feature-passed` | `e2e-passed` | — |
|
||||
| `review` | `reviewing`, `code-review`, `self-review` | — |
|
||||
| `qc-plan` | — | — |
|
||||
| `qc` | — | — |
|
||||
| `gate` | `sr-gate`, `verify`, `verification` | `gate-blocked` |
|
||||
| `publishing` | `push` | — |
|
||||
| `pr-open` | `ship`, `pr`, `push-conflict` | `push-revalidate` |
|
||||
| `reported` | — | — |
|
||||
| `watching-pr` | — | `pr-comment-fix` |
|
||||
| `merged` | — | — |
|
||||
| `done` | `complete`, `completed` | — |
|
||||
|
||||
50 names over 16 nodes. Every one of them has a **source**: the skill declares
|
||||
it, or `default.json` uses it (an agent moving between pipelines will type
|
||||
what the other one taught it), or Shipyard's `PHASES` lists it. An alias with
|
||||
no source is not "flexibility" — it is a synonym someone imagined, and the
|
||||
`ccam stage` warning already catches a name that resolves to nothing, which is
|
||||
better feedback than silently absorbing every plausible spelling. Twelve
|
||||
sourceless aliases were written and then cut for exactly this reason.
|
||||
|
||||
Two constraints on this:
|
||||
|
||||
- **No alias may collide** with another node's id or alias. `phaseIdx` takes the
|
||||
FIRST match, so a duplicate would silently resolve a declaration onto the
|
||||
wrong node. A test asserts the whole template is collision-free.
|
||||
- **Aliases of one node share one record slot.** `lane.stages` is keyed by the
|
||||
declared string, and `stageRecords` resolves each key onto its node — so
|
||||
declaring `migration-collision` and then `gates` leaves ONE record for that
|
||||
node (the canonical `gates` one wins). A sub-state that needs to keep its own
|
||||
`--evidence` separately has to be a real node, not an alias.
|
||||
|
||||
`integrate`, `dev-gates`, `e2e-on-dev` and `push-dev` are deliberately **not**
|
||||
nodes here. Shipyard had them and retired them on 2026-07-16 (see the comment
|
||||
above `PHASES` in its `dashboard/src/lib/constants.js`); it now folds those
|
||||
names into the surviving phases so old feature cards still render. CCAM never
|
||||
shipped them, so there is nothing to fold.
|
||||
|
||||
These nodes also carry `detect` rules; see "Stage detection → Where the rules live".
|
||||
|
||||
## Orchestration: what CCAM does NOT do
|
||||
|
||||
**CCAM does not chain, queue, retry, or evaluate gates.**
|
||||
|
||||
Reference in New Issue
Block a user