Files
Claude-Code-Monitor/docs/superpowers/plans/2026-08-04-ship-feature-skill.md
nntrivi2001 c37933adfe docs(lanes): plan E1 — ship-feature pipeline template + skill port
4 tasks: --qc boot flag + QC_BOOT_ENV, pipeline template JSON, the ported
skill text (Stages 0-14, integrations hardcoded off pending F), and a dry
run + docs. Corrected the pipeline-template task against the real node
schema (id/label/icon/gate/aliases, not the detect.stage sketch) and the
real getPipeline never-throws behavior during self-review.
2026-08-04 17:44:50 +07:00

659 lines
55 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# E1 — Ship-feature Pipeline Template + Skill Port Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** a lane can opt into the `ship-feature` pipeline template and a session can drive it with `.claude/skills/ship-feature-lane/SKILL.md`, whose every command is a real, working `ccam` command today (stages 02 fully runnable; later stages reference the not-yet-built agents/`sync-base`/F integrations by name, same as the roadmap's own text does).
**Architecture:** One backend addition (`--qc` boot flag + `QC_BOOT_ENV`, threaded through `server/lib/lane-profile.js`'s `hookEnv`/`runHook` and `server/lib/lane-runtime.js`'s `upLane`), one new pipeline template JSON, one new skill file. No new routes, no new CLI subcommands beyond one flag on the existing `ccam lanes up`.
**Tech Stack:** Existing profile/hook system (`server/lib/lane-profile.js`), existing runtime lifecycle (`server/lib/lane-runtime.js`), existing pipeline node-state renderer (`server/lib/pipelines.js`, reused verbatim).
## Global Constraints
- Every applicable source file MUST start with the project's authorship header — verify with `bash .claude/skills/file-headers/scripts/check-headers.sh`. This does NOT apply to `.claude/skills/ship-feature-lane/SKILL.md` (a prose skill file, not source code — no header used anywhere in `.claude/skills/*/SKILL.md` today, confirm by checking `.claude/skills/ship-feature/SKILL.md`, which has none) or to `server/data/pipelines/*.json` (data files, not source — confirm `server/data/pipelines/default.json` has none).
- **`QC_BOOT_ENV` is off by default.** A profile that never declares it sees zero behavior change from this plan — same pattern every other optional `DEFAULTS` entry in `lane-profile.js` already follows.
- **This skill's tracker/dev_qc/ci_wait integration checks are hardcoded off**, not a runtime check — F doesn't exist yet. Do not add a `lane-env`-style check function as part of this plan.
- **`.claude/skills/ship-feature-lane/SKILL.md` is a DIFFERENT skill from the existing `.claude/skills/ship-feature/SKILL.md`** (this repo's own generic "implement a feature in this codebase" guide, unrelated to lanes). Never edit the existing one; never let the two names collide in prose.
- Run `npm run test:server` (full suite) plus `bash .claude/skills/file-headers/scripts/check-headers.sh` before every commit.
- Never use `git add -A`. Stage exactly the files each task names.
---
### Task 1: `--qc` boot flag + `QC_BOOT_ENV`
**Files:**
- Modify: `server/lib/lane-profile.js` (`DEFAULTS`, `hookEnv`, `runHook`)
- Modify: `server/lib/lane-runtime.js` (`upLane`)
- Modify: `server/routes/lanes.js` (`POST /:id/up`)
- Modify: `bin/ccam.js` (`cmdLanesRuntime`'s `up` branch)
- Test: `server/__tests__/lane-profile.test.js` (or wherever `hookEnv`/`runHook` are already tested — find with `grep -rln "hookEnv\|runHook" server/__tests__/*.test.js` and add there), `server/__tests__/lane-runtime.test.js`
**Interfaces:**
- Modifies: `hookEnv(lane, profile, extraEnv = {})` → object (previously `hookEnv(lane, profile)`; the new third parameter is merged in AFTER everything else, so it can override anything including `profile.env`'s own declarations — the point of `--qc` is to force deterministic values).
- Modifies: `runHook(lane, profile, name, args = [], options = {})` → now also reads `options.extraEnv` and passes it to `hookEnv`.
- Modifies: `upLane(lane, options = {})` → now also reads `options.qc` (boolean); when true and `profile.env.QC_BOOT_ENV` is non-empty, computes `extraEnv` from it and passes it only to the `boot` hook call (not `migrate`/`seed`/`health`).
- Produces: `parseQcBootEnv(value)``Record<string,string>`, exported from `lane-profile.js` alongside the existing `splitList`/`parseEnvFile`.
- [ ] **Step 1: Find the existing hookEnv/runHook test coverage**
Run: `grep -rln "hookEnv\|runHook" server/__tests__/*.test.js`
Read whichever file(s) that finds — this task adds to existing coverage, it does not create a new test file for `lane-profile.js` if one already exists.
- [ ] **Step 2: Write the failing tests**
Add to the file found in Step 1 (adapt lane/profile fixture setup to match that file's existing helpers — every test file in this suite builds a temp profile directory differently; read the file first):
```js
describe("parseQcBootEnv", () => {
it("parses space-separated KEY=value pairs", () => {
const { parseQcBootEnv } = require("../lib/lane-profile");
assert.deepEqual(parseQcBootEnv("MOCK_PAYMENTS=1 STUB_EMAIL=1"), {
MOCK_PAYMENTS: "1",
STUB_EMAIL: "1",
});
});
it("returns an empty object for an empty or missing declaration", () => {
const { parseQcBootEnv } = require("../lib/lane-profile");
assert.deepEqual(parseQcBootEnv(""), {});
assert.deepEqual(parseQcBootEnv(undefined), {});
});
it("ignores a malformed token with no =", () => {
const { parseQcBootEnv } = require("../lib/lane-profile");
assert.deepEqual(parseQcBootEnv("GOOD=1 malformed"), { GOOD: "1" });
});
});
describe("hookEnv extraEnv", () => {
it("merges extraEnv on top of everything else, including profile.env", () => {
const { hookEnv } = require("../lib/lane-profile");
// Use this file's existing lane+profile fixture builder here — read the
// file to find its name (e.g. makeProfile/makeLane) and reuse it exactly.
const { lane, profile } = /* this file's existing fixture builder */ makeLaneAndProfile({
PORTS: "api",
});
const env = hookEnv(lane, profile, { PORTS: "overridden" });
assert.equal(env.PORTS, "overridden");
});
it("defaults extraEnv to nothing when omitted (existing callers unaffected)", () => {
const { hookEnv } = require("../lib/lane-profile");
const { lane, profile } = /* this file's existing fixture builder */ makeLaneAndProfile({});
const env = hookEnv(lane, profile);
assert.equal(env.LANE_ID, String(lane.id));
});
});
```
- [ ] **Step 3: Run tests to verify they fail**
Run: `node --test <the file found in Step 1>`
Expected: FAIL — `parseQcBootEnv is not a function`, `hookEnv` ignores the third argument (or the test file itself fails to require it).
- [ ] **Step 4: Implement `parseQcBootEnv` and thread `extraEnv` through `hookEnv`/`runHook`**
In `server/lib/lane-profile.js`, add `QC_BOOT_ENV: ""` to the `DEFAULTS` object (alongside the other A2 "empty = off" declarations, same comment style):
```js
UPLOAD_SUBDIR: "",
// E1: space-separated KEY=value pairs injected into the BOOT hook's
// environment only, only when `up` is called with qc:true — the deterministic
// stack `ship-feature-lane`'s Stage 3 boots for QC. Empty = off, same as
// every declaration above.
QC_BOOT_ENV: "",
});
```
Add `parseQcBootEnv` near `splitList` (same file):
```js
/**
* Parse a `QC_BOOT_ENV` declaration — space-separated `KEY=value` pairs — into
* a plain object. A token with no `=` is dropped rather than throwing: a
* malformed declaration should degrade to "that one pair is missing", not
* crash a boot.
*
* @param {string} [value]
* @returns {Record<string,string>}
*/
function parseQcBootEnv(value) {
const out = {};
for (const token of splitList(value)) {
const eq = token.indexOf("=");
if (eq <= 0) continue;
out[token.slice(0, eq)] = token.slice(eq + 1);
}
return out;
}
```
Change `hookEnv`'s signature and the final step of its body (find the existing `return env;` at the end of the function — read the file first to confirm nothing else sits after `redisUrl` handling before the return):
```js
function hookEnv(lane, profile, extraEnv = {}) {
```
... (body unchanged up through the existing `if (facts.redisUrl) { ... }` block) ...
```js
Object.assign(env, extraEnv);
return env;
}
```
Change `runHook` to forward `options.extraEnv`:
```js
{
cwd: lane.cwd,
env: hookEnv(lane, profile, options.extraEnv),
stdio: ["ignore", "pipe", "pipe"],
}
```
Update the exports list at the bottom of the file to include `parseQcBootEnv`:
```js
module.exports = {
HOOKS,
DEFAULTS,
PROFILE_SUBDIR,
parseEnvFile,
splitList,
parseQcBootEnv,
resolveProfile,
profileSearchPaths,
hookEnv,
runHook,
};
```
- [ ] **Step 5: Run tests to verify they pass**
Run: `node --test <the file found in Step 1>`
Expected: PASS
- [ ] **Step 6: Wire `qc` through `upLane`**
Read `server/lib/lane-runtime.js`'s `upLane` (already shown above in this plan's research — confirm line numbers with `grep -n "async function upLane" server/lib/lane-runtime.js` since Task 1-5 of earlier plans may have shifted them). Change the destructure and the boot-hook call:
```js
async function upLane(lane, options = {}) {
const profile = requireProfile(lane);
const { build = true, qc = false, onLine } = options;
```
... (body unchanged through `ensureDatabase`/`migrate`/`seed`) ...
```js
const bootExtraEnv = qc ? require("./lane-profile").parseQcBootEnv(profile.env.QC_BOOT_ENV) : undefined;
const boot = await runHook(current, profile, "boot", build ? [] : ["--no-build"], {
onLine,
timeoutMs: BOOT_TIMEOUT_MS,
extraEnv: bootExtraEnv,
});
```
(`require("./lane-profile")` inline rather than a top-of-file import: check first whether `lane-runtime.js` already imports from `lane-profile.js` at the top — if `runHook`/`requireProfile` are already destructured from a top-level `require("./lane-profile")`, add `parseQcBootEnv` to that same destructure instead of a second inline require.)
- [ ] **Step 7: Add a test for `upLane`'s qc option**
Find `server/__tests__/lane-runtime.test.js`'s existing profile-fixture helper (the one used by the `upLane` tests already in that file — read it first) and add:
```js
describe("upLane qc option", () => {
it("injects QC_BOOT_ENV into the boot hook's environment when qc:true", async () => {
// Build a profile fixture (this file's existing helper) whose boot.sh hook
// writes $SOME_QC_VAR to a file, so the test can assert on it. Declare
// QC_BOOT_ENV="SOME_QC_VAR=from-qc" in that fixture's profile.env.
// ... follow this file's existing pattern for asserting on a hook's
// observable side effect (most tests here check pid files / log output /
// written markers rather than mocking child_process — read an existing
// "boot hook did X" test and mirror its exact mechanism) ...
});
it("does not touch the environment when qc is omitted (default false)", async () => {
// Same fixture, called without {qc: true} — assert the marker file from
// the above test does NOT get the QC value.
});
});
```
- [ ] **Step 8: Run the full suite**
Run: `npm run test:server`
Expected: PASS, including the new tests.
- [ ] **Step 9: Wire the route and CLI flag**
In `server/routes/lanes.js`, find `router.post("/:id/up", ...)` (already read above in this plan's research — confirm with `grep -n "router.post(\"/:id/up\"" server/routes/lanes.js`) and change:
```js
const build = req.body?.build !== false;
const qc = req.body?.qc === true;
res.status(202).json({ ok: true, laneId: lane.id });
void withLaneLock(lane.id, async () => {
const onLine = (line, stream) =>
broadcast("lane_hook_output", { laneId: lane.id, hook: "up", stream, line });
try {
const facts = await upLane(lanesLib.getLane(lane.id), { build, qc, onLine });
```
In `bin/ccam.js`, find the `if (sub === "up") {` block inside `cmdLanesRuntime` (confirmed at the line already read: `const body = laneArgs.includes("--no-build") ? { build: false } : {};`) and change:
```js
if (sub === "up") {
const body = {};
if (laneArgs.includes("--no-build")) body.build = false;
if (laneArgs.includes("--qc")) body.qc = true;
const result = await post(`/api/lanes/${laneId}/up`, body, { allowError: true });
```
Update the help-table row for `lanes up` (search `COMMAND_GROUPS` for the existing `"lanes up|down|..."` row) to mention `--qc`:
```js
[
"lanes up|down|runtime|logs|hook",
"[<id>] [--no-build] [--qc]",
"Boot or stop the lane's own app stack (--no-build: up only, skip the build step; --qc: up only, inject QC_BOOT_ENV for a deterministic stack; id defaults to the lane owning this directory)",
],
```
(Read the exact current row text first with `grep -n "no-build" bin/ccam.js` — merge into the real current wording rather than overwriting unrelated parts of the description.)
- [ ] **Step 10: Run the full suite and header audit**
Run: `npm run test:server && bash .claude/skills/file-headers/scripts/check-headers.sh`
- [ ] **Step 11: Commit**
```bash
git add server/lib/lane-profile.js server/lib/lane-runtime.js server/routes/lanes.js bin/ccam.js server/__tests__/lane-profile.test.js server/__tests__/lane-runtime.test.js
git commit -m "feat(lanes): add --qc boot flag + QC_BOOT_ENV for deterministic QC stacks (E1)"
```
(Adjust the test file list in `git add` to whichever files Step 1 actually found and Step 7 actually edited.)
---
### Task 2: `server/data/pipelines/ship-feature.json`
**Files:**
- Create: `server/data/pipelines/ship-feature.json`
- Test: `server/__tests__/pipelines.test.js` (or wherever `default.json`/custom templates are already tested — find with `grep -rln "pipelines/default\|loadPipeline\|getPipeline" server/__tests__/*.test.js`)
**Interfaces:**
- Consumes: `server/lib/pipelines.js`'s existing `getPipeline`, `nodeStates`, `progressPct` — reused verbatim, no changes to that module.
- Produces: a lane can set `pipeline: "ship-feature"` (already a plain string column on `lanes`, no schema change) and get this template's nodes back from `GET /api/lanes/:id` and `GET /api/lanes/:id/features` (Task B's `featurePayload` already calls `getPipeline(feature.pipeline)`).
- [ ] **Step 1: Read the existing template format**
`server/data/pipelines/default.json`'s real shape (read it yourself with `cat server/data/pipelines/default.json` to confirm this plan's transcription is still current before writing the new file): top-level `{id, name, nodes}`; each node is `{id, label, icon, gate, aliases, detect?}``id` is the canonical stage name, `aliases` are other declared-stage strings (from `ccam stage <name>`) that also map to this node, `gate: true` marks a node that needs `--evidence` to render `done` rather than the amber `passed-no-evidence` state (per this repo's five node states, documented in `docs/LANES.md`), and `detect` (an array of `{tool, match}` rules) is for INFERRING a stage from tool-call patterns when no session ever explicitly declares it — `ship-feature-lane` (Task 3) always explicitly declares every stage via `ccam stage`, so no `ship-feature.json` node needs a `detect` block.
- [ ] **Step 2: Write the failing test**
Add to the test file found above (mirror its existing test structure for `default.json` — e.g. "loads without throwing", "every node has a unique id"):
```js
describe("ship-feature pipeline template", () => {
it("loads without throwing and has one node per skill stage", () => {
const { getPipeline } = require("../lib/pipelines");
const pipeline = getPipeline("ship-feature");
const ids = pipeline.nodes.map((n) => n.id);
assert.deepEqual(ids, [
"intake",
"plan",
"implementing",
"gates",
"e2e-feature",
"e2e-feature-passed",
"review",
"qc-plan",
"qc",
"gate",
"publishing",
"pr-open",
"reported",
"watching-pr",
"merged",
"done",
]);
});
it("every node id is unique", () => {
const { getPipeline } = require("../lib/pipelines");
const pipeline = getPipeline("ship-feature");
const ids = pipeline.nodes.map((n) => n.id);
assert.deepEqual(ids, [...new Set(ids)]);
});
});
```
- [ ] **Step 3: Run test to verify it fails**
Run: `node --test <the test file>`
Expected: FAIL — `getPipeline` never throws on an unknown id (it falls back to `default.json`, per its own doc comment: "Never throws: an unknown id yields the default template"), so the first test fails on a node-id mismatch (`default.json`'s 8 ids, not the 16 expected here), not on an exception.
- [ ] **Step 4: Write `server/data/pipelines/ship-feature.json`**
```json
{
"id": "ship-feature",
"name": "Ship feature (lane pipeline)",
"nodes": [
{ "id": "intake", "label": "intake", "icon": "📝", "gate": false, "aliases": [] },
{ "id": "plan", "label": "plan", "icon": "🧭", "gate": false, "aliases": [] },
{ "id": "implementing", "label": "implement (TDD)", "icon": "🛠", "gate": false, "aliases": [] },
{ "id": "gates", "label": "CI gates + preflight", "icon": "🧪", "gate": true, "aliases": [] },
{ "id": "e2e-feature", "label": "e2e on feature branch", "icon": "🧪", "gate": false, "aliases": [] },
{ "id": "e2e-feature-passed", "label": "e2e passed", "icon": "🧪", "gate": true, "aliases": [] },
{ "id": "review", "label": "code review", "icon": "👀", "gate": true, "aliases": [] },
{ "id": "qc-plan", "label": "QC plan", "icon": "📋", "gate": false, "aliases": [] },
{ "id": "qc", "label": "browser QC", "icon": "🔍", "gate": true, "aliases": [] },
{ "id": "gate", "label": "senior GO/NO-GO gate", "icon": "🚦", "gate": true, "aliases": [] },
{ "id": "publishing", "label": "publish PR", "icon": "🔀", "gate": false, "aliases": [] },
{ "id": "pr-open", "label": "PR open", "icon": "🔀", "gate": false, "aliases": ["ship"] },
{ "id": "reported", "label": "reported", "icon": "📣", "gate": false, "aliases": [] },
{ "id": "watching-pr", "label": "watching PR", "icon": "👁", "gate": false, "aliases": [] },
{ "id": "merged", "label": "merged — post-verify", "icon": "🔗", "gate": false, "aliases": [] },
{ "id": "done", "label": "done", "icon": "✅", "gate": false, "aliases": ["complete", "completed"] }
]
}
```
(`gate: true` is a rendering hint only — `nodeStates` passes it straight through to the UI unchanged; it does NOT change whether a node needs `--evidence` to show `done` (every node does, regardless of `gate`). It's set here on the five real checkpoints a fix-loop can bounce off of — CI gates, e2e-passed, code review, QC, and the senior gate — purely so the dashboard can visually distinguish a checkpoint from a transit stage, matching `default.json`'s own use of `gate` on `tests`/`review`/`gate`. `pr-open`'s `"ship"` alias and `done`'s `"complete"/"completed"` aliases mirror `default.json`'s own aliasing for the same concepts, so a stray `ccam stage ship` or `ccam stage complete` from habit still resolves to the right node.)
- [ ] **Step 5: Run test to verify it passes**
Run: `node --test <the test file>`
Expected: PASS
- [ ] **Step 6: Run the full suite and header audit**
Run: `npm run test:server && bash .claude/skills/file-headers/scripts/check-headers.sh`
- [ ] **Step 7: Commit**
```bash
git add server/data/pipelines/ship-feature.json <the test file>
git commit -m "feat(lanes): add ship-feature pipeline template (E1)"
```
---
### Task 3: `.claude/skills/ship-feature-lane/SKILL.md`
**Files:**
- Create: `.claude/skills/ship-feature-lane/SKILL.md`
**Interfaces:** none — this is a prose skill file, not code. No test file; verified by Task 4's dry run.
- [ ] **Step 1: Write the skill file**
Create `.claude/skills/ship-feature-lane/SKILL.md` with this exact content:
````markdown
---
name: ship-feature-lane
description: "Autonomous end-to-end feature pipeline for ONE CCAM lane. Invoke inside a lane's working directory with a requirement: `/ship-feature-lane <requirement>`. Frontloads ALL clarifying questions once, then runs unattended: implement (TDD) → pre-push CI gates + dev preflight → e2e on the feature branch → code review → local QC → senior GO/NO-GO gate → push branch + open PR (base `development`) → CI watch → report, then watches the PR (new comments → gated fix-loop; base conflicts → sync development into the branch) until a HUMAN merges it. The PR is only published after all local gates pass and the senior gate says GO — it's finalized when reviewers see it. Declares lane stage at every step via `ccam stage` for the dashboard. Use when the user wants to build/ship/implement a feature in a CCAM lane. NOTE: ticket-filing and post-merge dev-site QC are currently OFF (F's integrations aren't built yet) — this pipeline stops at Stage 14 once dev CI/dev-QC support lands."
---
# Ship Feature Lane (CCAM lane pipeline)
You are running the autonomous feature pipeline for **one CCAM lane**. The human's only interactive touchpoints are **Stage 0 (frontloaded Q&A)** and **merging the PR on GitHub**; everything else runs to completion or to a `blocked` escalation, reporting progress through `ccam stage` (which the dashboard renders).
## Setup — do this first, every run
```bash
LANE_DIR="$(pwd)" # the lane clone IS your cwd — CCAM resolves the lane from this, never a hardcoded path
```
- CCAM resolves your lane from `cwd` automatically (longest path-boundary prefix match) — there is no marker file to check and no separate assign step. If `ccam stage` or `ccam feature activate` ever fails with "no lane found", you are not inside a lane's working directory; stop and tell the human.
- All stage updates go through `ccam stage <stage> [--status <s>] [--evidence "..."]` — **call it at the start of every stage** (this is also the heartbeat, visible on the dashboard).
- **Integration toggles are currently OFF.** Tracker (ticket-filing), dev-site QC, and CI deploy-wait integrations are not built in CCAM yet — treat all three as permanently off for this run: Stage 9 (ticket) is skipped entirely, Stage 13's dev-QC half is skipped, Stage 13's dev-CI-wait half is skipped, and Stage 10's CI watch always uses the plain `gh pr checks` path (never `ccam ci`, which doesn't exist). When these land, this skill gets a follow-up edit to make the checks real — do not invent a check now.
- **Heartbeat during long stages.** Implementing (Stage 1), CI waits (Stage 10), and the watch/post-merge polls (Stages 1213) can run many minutes between stage transitions — bump the heartbeat with `ccam stage <same-stage>` after each commit and on each poll iteration, so the dashboard doesn't false-flag a working lane as stalled.
- Profile hooks (`bootstrap`/`boot`/`migrate`/`seed`/`ci-gate`/`e2e`/`health`/`regen`) run through `ccam lanes hook <name> [args…]` and `ccam lanes up`/`down`. Use them; don't reinvent their logic.
- **NEVER merge or rebase branches manually.** The ONLY merge that ever happens in this flow is `origin/development` INTO the feature branch, and only through `ccam lanes sync-base` (fetches fresh, pre-checks migration collisions, auto-regenerates generated files — see the note on this command in Stage 2; it is a LATER task, referenced here by its intended contract). There is no other direction: never merge a feature branch into anything locally, never commit on `development`, and never touch `main`.
- **NEVER push `origin/development` or `origin/main` (HARD).** `development` moves ONLY when a human merges a PR on GitHub. The only branch you ever push is your own `feat/<slug>` — and only after the senior gate's GO (Stage 8). If you ever find yourself typing `git push` with `development` or `main` on the line: STOP, `ccam stage <current> --status blocked`.
## Context recovery — after conversation compaction
Long pipelines outlive the context window. When context is compacted (summarized), re-derive these before continuing:
```bash
LANE_DIR="$(pwd)" # lane = cwd, resolved fresh
```
Then check your current position:
- **Lane state**: `ccam feature show <slug>` (or `ccam lanes` for the lane's own row) — shows current stage, status, feature title, branch, gate decision, PR URL, notes.
- **Git branch**: `git rev-parse --abbrev-ref HEAD` — which branch you're on.
- **Feature slug**: from the branch name (`feat/X` → `X`), or from `ccam feature list` (the lane's currently-active feature is marked ``).
Resume from the stage shown. If state says `stage=X status=running`, you were mid-stage X when context compacted — re-run that stage from the top (all `ccam` commands are idempotent).
- **MCP preflight (fail fast):** confirm this session actually loaded the lane's required Playwright MCPs (their `browser_*` tools must be available) — `playwright` and a local-QC MCP are always required for Stage 3/6. `ccam lanes mcp sync` (F, not built yet) would normally do this for you; until then, tell the human to configure `.mcp.json` manually and restart the session if a required MCP is missing. Catching this at Stage 0 costs a minute; catching it at Stage 13 strands a merged feature unverified.
## Hard rules
- **Publish ONLY after the senior-gate-reviewer returns `VERDICT: GO`.** "Publish" = push the feature branch + open/update the PR (Stage 8). Nothing reviewer-visible exists before GO, and nothing else authorizes it.
- **The fix-loop:** any failure in stages 27 (gates/preflight, e2e, review, QC, senior gate), any red PR CI that's genuinely yours (Stage 10), and any worth-fixing review comment (Stage 12) → fix on the **feature branch**, and re-run **from Stage 2 through Stage 8** (gates+preflight → e2e → review → QC plan → QC → senior gate → publish/update PR), then the Stage 10 CI watch. Never skip a gate — the full process applies; no shortcuts because "it's just review feedback".
- **EXCEPTION — test-only re-entry (browser-QC fast-path).** If the re-entry's change is ENTIRELY test files — `git diff --name-only` since the last browser-QC'd commit matches only test paths — the app's runtime behavior/UI is unchanged from the last QC'd pass. Run this EXACT stage set, nothing else:
- **Always run:** Stage 2 (gates + preflight), Stage 7 (senior gate), Stage 8 (publish/update PR), and the Stage 10 tail (CI watch).
- **Run only if e2e spec files are among the changed tests:** Stage 3 (boot + e2e on the feature branch). No e2e specs changed → skip it.
- **Always SKIP** (runtime UI unchanged): Stage 5 (QC plan), Stage 6 (qc-local). Record `--evidence "QC skipped: test-only change"`.
- If the diff contains ANY non-test file → this fast-path does NOT apply; take the full path above. The first pass (not a re-entry) always runs full QC.
- **EXCEPTION — localized re-entry (scoped-e2e fast-path).** On a re-entry whose diff since the last fully-validated commit is SMALL and LOCALIZED — only files inside the feature's own surface, NO migrations, NO contract/generated files, NO shared fixtures/utilities, NO dependency changes — you may shrink Stage 3's e2e to a SCOPED run of the specs covering the touched surface: `ccam lanes hook e2e -- <spec files>` (scoped runs still heartbeat + lock + time-bound like the full suite), and browser QC runs SCOPED to the affected QC-Plan scenarios (tell the qc agent exactly which scenario numbers). Know the trade-off: there is no dev-merged full suite anymore — post-merge dev CI + dev-QC (Stage 13, currently off) are normally the integration net; without them, a localized re-entry after this lands is a real gap until F ships. If in doubt whether the change is localized, it isn't — run the full path.
- **Run long helpers so they can't be killed mid-flight or hang your turn.** `ccam lanes hook ci-gate`, `ccam lanes up`, `ccam lanes hook e2e` legitimately run 320+ minutes (builds, tests, Playwright, lock waits). NEVER invoke them with the default Bash timeout (2 min kills them mid-flight and strands the lane half-done): use `run_in_background: true` and poll the output file until done, or set `timeout: 600000` for the shorter gates. If a helper does die mid-run, don't panic: every hook is idempotent — re-run the step (e.g. re-run `ccam lanes up --no-build` to revive a stack).
- **Waiting + polling NEVER use a foreground `sleep`** (the Bash tool blocks it) or `ScheduleWakeup` (that's a `/loop`-only primitive — this pipeline is not a `/loop` session, so it won't sustain your watch). To pace a poll loop or wait out a timer, **background the wait**: run `sleep <secs>` with `run_in_background: true` — you're re-invoked when it exits, and re-invoked the moment a backgrounded Agent or helper finishes, so you never busy-poll for background work. **If you ever can't sustain a wait/loop in this session, set `ccam stage <current> --note "<honest note>"` and STOP — never narrate a watch or loop you are not actually running.**
- **e2e: actively poll — never wait on the completion re-invoke alone.** A hung suite never fires it, stranding the lane at `stage=e2e-feature`. When running `ccam lanes hook e2e` (Stage 3): start it `run_in_background: true` AND background a `sleep 90` beside it. Each wake — finished → parse PASS/FAIL; still running → read the e2e log tail (`ccam lanes logs <id> e2e`), bump the heartbeat, and re-background `sleep 90`, UNLESS it's erroring or has run past ~22 min, in which case kill the e2e task and treat it as FAIL → re-enter Stage 2.
- **Turn-liveness: never let the pipeline die silently.** A "stalled" lane usually died one of two ways: (a) a turn ended with NOTHING pending — no backgrounded wait, no running helper, no background agent — so nothing ever re-invoked the session; or (b) a transient API error (rate-limit, 529/overload, connection refused) killed the turn mid-stage. Rules: while the pipeline is anywhere between Stage 1 and Stage 14 (done), every turn you end MUST leave at least one re-invoker pending (a `run_in_background` helper/sleep or a background agent) — check before ending the turn. And on ANY resume after an error or a human nudge ("continue"), do not ask questions: re-derive position from lane state (Context recovery above) and continue the stage. If you truly cannot leave a re-invoker, set `--note "watch needs re-trigger: <what to do>"` so the dashboard shows it honestly.
- **No retry cap — the phase clock is the signal.** A failing gate/QC/CI/e2e just re-enters the loop (fix on the feature branch, re-run from Stage 2); there is NO automatic block after N attempts. The dashboard shows how long the lane has sat in its current stage, so the human can spot a stuck or endlessly-looping lane and step in. Reserve `--status blocked` for GENUINE blockers you cannot resolve (an ambiguous merge conflict, a hard/unrecoverable error).
- **Commit only on the feature branch.** Never commit on `development`/`main`. Stage only intended files (never `git add .` blindly — this repo collects stray build/QA artifacts).
- Keep the lane's state truthful: on any stop, set an accurate `ccam stage <stage> --status <status> --note "<why>"`.
- **Quality bar (applies to every code change, including fix-loop re-entries and follow-up PRs).** Tests are sharp and meaningful — each pins a real behavior/edge case (happy + negative + boundary), none trivial, redundant, or coverage-padding. Comments are minimal — only the non-obvious *why*, matching the surrounding density; never narrate the *what*. Investigate before fixing (root cause, not symptom — use **systematic-debugging**). Prefer reusing/extending existing code over duplicating it.
- **One driver per MCP browser server.** Each MCP server owns ONE browser; two agents driving the SAME server interleave clicks in one tab. The local-QC MCP → the qc-local agent (Stage 6) only; the general `playwright` MCP → the main session for ad-hoc checks only (never while qc-local runs). Parallel agents on DIFFERENT servers are safe by design; a second concurrent driver on the SAME server is never OK.
- **Cross-lane etiquette (locks + siblings).** Lanes share one machine and one dev site. Waiting on a cross-lane serializer (`ccam lock acquire <name>`, e.g. around a shared build/e2e step) is NORMAL — it heartbeats while it waits, so you won't look stalled. NEVER free a lock by killing another lane's session or processes, deleting the lock's directory by hand, or shrinking `LOCK_MAX_HOLD`; a dead holder's lock auto-expires on its own. If a lock wait times out: re-try with a longer `--timeout`, or set `--status blocked` with a note and report. Touch ONLY your own lane's clone, state, and locks you hold.
## Stages
### 0 — Intake & frontloaded Q&A *(the only interactive part)*
Do NOT jump to code. Understand the requirement first.
- **Restate + quick scan.** Restate the requirement. Do a fast targeted scan of the relevant code (use the `Explore` agent for breadth; the **brainstorming** skill if the requirement is fuzzy) so your questions are grounded in what actually exists.
- **Frontloaded Q&A.** Ask the human **every** clarifying question in ONE batch: acceptance criteria, scope / non-goals, UI/UX specifics, data shapes, edge cases, which existing flows it touches. **Sibling-surface check (mandatory):** if your scan shows the app has N parallel surfaces of the pattern the requirement touches (e.g. several foldered areas, several list pages sharing a component) and the requirement names fewer than N, explicitly ask "this exists in [all N places] — apply to all, or only [the named ones]?" A missed sibling here costs a full second pipeline pass when a reviewer catches it on the PR. Then activate a clean feature slot for this run: `ccam feature activate <slug> --title "<short title>"` — Task B's `activate` archives whatever feature was previously active on this lane automatically, so this run's dashboard state starts clean without a separate "init" step. Then mark intake: `ccam stage intake --status running`. Announce "Questions answered — going autonomous now." After this, don't ask the human anything unless you hit a `blocked` escalation.
### 0b — Investigate & plan *(autonomous)*
- `ccam stage plan` — now design a real plan and have it independently challenged before you implement.
- **Investigate (autonomous, thorough).** Read the actual code paths, models, existing tests, and conventions the feature touches — `Explore`/`general-purpose` subagents for breadth, then read the key files yourself for depth. Pin down: integration points, data/migration needs, API/contract impact, reuse opportunities, and risks. Use **systematic-debugging** if the feature is a fix (root-cause first, no symptom patches).
- **Plan.** Produce a concrete implementation plan (the **writing-plans** skill): approach, files to change, the test strategy (which behaviors/edge cases each test will pin), migration/contract impact, and how each acceptance criterion is met.
- **Debate the plan (adversarial review).** Spawn a SEPARATE sub-agent (Agent tool — `Plan` or `general-purpose`) to critique the plan + investigation: missed requirements, wrong assumptions, a simpler approach, unhandled edge cases, acceptance-criteria gaps. Apply the worthwhile critiques (use **receiving-code-review** judgment — verify each point, don't blindly accept or reject). Iterate once or twice until the plan holds up.
- Write the Q&A answers **and the agreed plan** to a lane spec file `docs/superpowers/specs/lane-<slug>.md` (gitignored, or add it to `.gitignore` if this is the first one) — the acceptance contract the senior gate checks against.
### 1 — Implement (TDD, to the plan)
- Choose a **single-segment slug** for the feature — lowercase, hyphens, NO slashes. Cut the feature branch from **development** (the PR base): `git fetch origin && git checkout -b feat/<slug> origin/development`.
- If Stage 0 activated a placeholder slug different from the final chosen one, reconcile: `ccam feature activate <slug>` — it echoes back the canonicalized slug it actually stored; use THAT for the branch and every later reference.
- Implement the agreed plan with the **test-driven-development** skill: failing test → minimal code → green → commit. Frequent small commits.
- **Tests must be sharp and meaningful.** Each test pins a real behavior or edge case from the plan / acceptance criteria — cover the happy path, the negative/error path, and boundaries. NO trivial or redundant tests: don't assert constants or framework internals, don't re-test the same path twice, don't pad for coverage. A few precise tests that would actually catch a regression beat many shallow ones.
- **Comment only when it earns its place.** Match the surrounding code's comment density. Comment the non-obvious *why* (intent, invariants, gotchas, links to context) — never narrate the *what* the code already says. Delete redundant/boilerplate/restating comments rather than adding them.
- If your stack generates an API contract/client and the API changed, regenerate it (`ccam lanes hook regen`) so the contract-check gate passes (stacks without a contract gate skip this).
- `ccam stage implementing`
### 2 — Pre-push CI gates + dev preflight (on the feature branch)
- `ccam lanes hook ci-gate` — runs the profile's CI gate (lint / test / contract checks) against an isolated per-lane test DB. On failure: read the output, fix on the feature branch, commit, re-run. Loop until green.
- `ccam lanes sync-base --check feat/<slug>` — the dev preflight: fetches and checks the branch against the CURRENT `origin/development` without merging anything. **This command is a LATER task, not yet built as of this skill's authoring** — until it exists, skip this preflight check and rely on Stage 12's conflict handling to catch a divergence at merge time; note this gap explicitly in your Stage 2 evidence (`ccam stage gates --evidence "sync-base preflight unavailable, skipped"`).
- Once built, its contract is: exit 5 on a migration-number collision (print the exact rename, do it on the feature branch, re-run Stage 2); informational `DEV_DELTA:`/`DEV_OVERLAP:` output otherwise (you do NOT sync the branch for it — GitHub merges non-conflicting histories fine).
### 3 — E2E on the feature branch
The e2e hook doesn't run migrations itself — it tests the already-running stack. To exercise the feature's code and any new schema, boot the lane stack with the feature branch first:
- `ccam stage e2e-feature --status running`
- `ccam lanes up --qc` — boots with the profile's QC env (mock/stub flags so QC is deterministic, from `QC_BOOT_ENV`), applies the feature branch's own migrations, and reboots the stack. Idempotent; safe to re-run. (The branch was cut from `origin/development`, so this stack IS development + your feature as of the branch point.)
- `ccam lanes hook e2e` — Playwright e2e under the e2e lock against the now-booted stack. **This is the only e2e gate in the flow** — there is no dev-merged suite behind it.
- On failure: fix on the feature branch, commit, re-run from Stage 2.
- **Iterating on a failing spec:** use scoped runs through the hook — `ccam lanes hook e2e -- <spec file/filter>` — never a bare test-runner invocation in the lane (bare runs skip the cross-lane lock, the hard timeout, and the heartbeat, so the dashboard false-flags STALLED). A scoped green is never the gate; finish with the full suite (unless the localized fast-path applies — see Hard rules).
- On success: `ccam stage e2e-feature-passed --status running`
### 4 — Code review *(no open PR yet — use local diff)*
- Run the **`code-review` skill at effort `high`** on the feature diff vs `origin/development` — this is the deterministic code-review gate, not an ad-hoc read. The PR isn't open yet, so point it at the local diff: `git diff origin/development...feat/<slug>` (and `git log origin/development..feat/<slug>` for commits). ONLY if the `code-review` skill is unavailable, fall back to a manual review of that diff (correctness, security, tests, migration/contract safety). The Stage-6 `qc-local` report covers the user-flow review for the senior gate.
- Apply the fixes worth making on the feature branch; if you change code, re-run **from Stage 2**.
- `ccam stage review`
### 5 — QC plan *(bound the test scope before any browser QC)*
- Author a **QC Plan** the browser-QC agents (Stage 6 now, and a future Stage 13 once dev-QC exists) will execute against — so QC covers everything that matters and nothing that doesn't (no missed scenarios, no wandering into unrelated areas). Derive it from the acceptance points (lane spec) + the real change surface (`git diff origin/development...feat/<slug>` and `--stat`). Three parts:
- **In-scope scenarios** (numbered): each acceptance point with positive AND negative cases; adjacent flows sharing routes/components/data with the change; required **state coverage** (reload on each stateful screen touched, one logout→re-login, back/forth nav); and the required **UI/UX layout checks** for every form/screen the feature touches (narrow AND short viewport, expandables open so content exceeds the viewport, fixed chrome not clipped, every control labelled, section headers more prominent than field labels).
- **Out-of-scope** (explicit): areas NOT to test because the change cannot affect them — this is what stops QC from over-testing.
- **Smoke set**: login + main nav + ≥3 unaffected major areas.
- Append it to the lane spec under a `## QC Plan` heading (`docs/superpowers/specs/lane-<slug>.md`) — the same file the senior gate reads. You are the **single writer** of this section; the QC agent only *proposes* additions in its report and you fold them in (Stage 6). This keeps the plan race-free yet living.
- `ccam stage qc-plan --status running`
### 6 — Browser QC via the qc-local agent
- **Test-only fast-path:** on a fix-loop re-entry whose change is ENTIRELY test files (see the fix-loop rule), SKIP this stage — the app's runtime UI is unchanged — and record `--evidence "QC skipped: test-only change"`. Otherwise run it:
- `ccam stage qc --status running`, then launch the **qc-local** agent (Agent tool, `subagent_type: qc-local` — FOREGROUND; it gates the pipeline. **This agent does not exist yet as of this skill's authoring — a separate, later task ports it.** Until then, this stage cannot run; treat a lane that reaches here as `--status blocked --note "qc-local agent not yet available"` and report to the human). When it exists, give it: the lane's working directory, the feature slug, the feature title, the acceptance points (lane spec), and the **QC Plan** (lane spec, Stage 5) as the authoritative scope to execute against. It owns the whole local browser QC and proof capture (`ccam lanes proof-link` first, then screenshots land under the proof gallery automatically). It runs against the lane's feature-branch stack from Stage 3. Do NOT drive the browser yourself at this stage.
- Parse its last line: `LOCAL-QC: PASS` → continue. `LOCAL-QC: FAIL — <reasons>` → fix on the feature branch → re-run from Stage 2. Keep its report — it is the feature user-flow review for the senior gate.
- **Fold back discoveries:** if its report lists scenarios it found that weren't in the plan (its "Scenarios discovered during QC" section), add them to the `## QC Plan` in-scope list in the lane spec.
### 7 — Senior GO/NO-GO gate *(authorizes the publish)*
- Launch the **senior-gate-reviewer** agent (Agent tool, `subagent_type: senior-gate-reviewer`. **This agent does not exist yet as of this skill's authoring — same later task as Stage 6.** Until then, treat a lane reaching here as `--status blocked --note "senior-gate-reviewer agent not yet available"`). When it exists, give it: the lane's working directory, the requirement + Stage-0 answers (the lane spec file), the feature branch, the Stage-4 code-review findings + resolutions, the Stage-6 `qc-local` report (the user-flow review), and confirmation that gates/e2e/review/QC passed. The agent inspects the local diff with `git diff origin/development...feat/<slug>` — no open PR is required (and none exists yet).
- Parse its final line:
- `VERDICT: GO` → proceed to Stage 8.
- `VERDICT: NO-GO — <fixes>` → fix on the feature branch, re-run **from Stage 2**. No attempt cap — the loop re-enters; the dashboard's time-on-stage surfaces a lane stuck cycling so the human can step in. Set `--status blocked` only for a genuine blocker you can't resolve.
- `ccam stage gate --evidence "GO"` (or `NO-GO — <reason>`)
### 8 — Publish: push branch + open/update PR *(GATED — only on GO)*
- `ccam stage publishing --status running`
- Re-run the preflight if `ccam lanes sync-base` exists by the time you read this — development may have moved while you were in QC. If it doesn't exist yet, skip straight to the push (same gap noted in Stage 2).
- `git push -u origin feat/<slug>` — first push of the feature branch to remote. All gates have passed before this point; the PR is finalized before reviewers see it.
- Open or update the PR **based on and targeting `development`**: `gh pr create --base development --fill` (or `gh pr edit` / the push itself if a prior run already created it). Capture the URL.
- `ccam stage pr-open --evidence "<pr-url>"` — the dashboard shows the PR link from here (via `--evidence` in `ccam feature show`/`ccam lanes`).
### 9 — Ticket *(currently SKIPPED — tracker integration is off)*
- Tracker integration is hardcoded off (see Setup). Do nothing here; do not attempt to file a ticket. When F ships `ccam lanes integration tracker`, this stage gets a real implementation.
### 10 — CI watch on the PR *(non-blocking)*
- Check the PR's CI with `gh pr checks` / `gh`. Green → continue to the report + watch — never idle waiting for green.
- **Red CI → triage before the fix-loop** (shared CI flakes under multi-lane load; treating every red as your defect wastes cycles):
1. Read WHICH job/tests failed with `gh` (there is no `ccam ci` yet — F builds that; use `gh run view`/`gh pr checks --watch` directly).
2. **Your tests / your code implicated** → real failure: fix on the feature branch, re-enter from Stage 2.
3. **Infra/flake signature** (a job with no test failures, OOM/contention on the shared runner, a hung job with no output, or a test that is green locally on the identical tree) → re-run the workflow via `gh run rerun <run-id> --failed` — ONCE. Still red after the rerun → treat it as real (or escalate with the evidence). Never rerun more than twice, and never push an empty commit to re-trigger CI.
4. A **hung** workflow (running way past its normal duration with no output) → `gh run cancel <run-id>` then rerun once.
### 11 — Report
- Post a concise report: PR URL, CI status, what shipped, and that the PR now **awaits a human merge** (this pipeline never merges).
- `ccam stage reported --status running`
- Do **NOT** clear or reset the lane's feature state. Cleanup is the human's call, from the dashboard — they may still be manually testing.
### 12 — Watch the PR *(until a human merges or closes it)*
- `ccam stage watching-pr --note "watching PR for comments + base conflicts + the merge"`
- Loop every ~5 minutes, paced by a **backgrounded** wait so the turn isn't pinned (see the waiting-primitive rule above): run `sleep 300` with `run_in_background: true` — you're re-invoked when it elapses. Each iteration:
- Check PR state: `gh pr view <pr_url> --json state,mergeable -q '.state + " " + (.mergeable|tostring)'`, and bump the heartbeat (`ccam stage watching-pr`).
- `MERGED` → a human merged it: go to **Stage 13** (post-merge verification).
- `CLOSED` (unmerged) → the human rejected/abandoned it: `ccam stage done --status passed --note "PR closed unmerged by human"` → STOP.
- `CONFLICTING` → the feature branch conflicts with `development`. Resolve it as real work:
- If `ccam lanes sync-base` exists by now: `ccam lanes sync-base feat/<slug>` (merges the latest `origin/development` INTO the feature branch — the only sanctioned merge). Resolve every conflict thoughtfully — keep `development`'s behavior for code unrelated to this feature, preserve the feature's intent where they overlap; when genuinely ambiguous, STOP and escalate (`--status blocked`, note the files) rather than guess. `git add` ONLY the conflicted files, `git commit --no-edit`.
- If it doesn't exist yet: `git fetch origin && git merge origin/development` directly on the feature branch, resolve conflicts the same way, commit.
- Re-enter the pipeline **from Stage 2 through Stage 8** (the push updates the PR), then return here and keep watching.
- For each new PR comment (list with `gh pr view <pr_url> --json comments`, tracking which you've already handled by comment id in your own notes), triage AND **always reply on its thread** (every comment gets a response — no silent handling, so reviewers see it was considered):
- **Worth fixing** (reviewer-requested change, real bug, test/doc gap): this is a NEW CHANGE — apply it on the feature branch and re-enter the pipeline **from Stage 2 through Stage 8** (+ Stage 10 CI watch). The full process applies; no shortcuts because "it's just review feedback". **After the fix is pushed, reply to the comment** confirming resolution — what changed + the commit/PR ref — by writing the reply to a file and posting `gh pr comment <pr_url> --body-file <path>` referencing the comment. **Never inline `--body "..."`** — bodies carry backticks/`file:line`/`$(...)` that bash reads as command substitution inside double quotes, which corrupts the comment and trips an approval prompt. Then come back here and keep watching.
- **Question / discussion**: answer it via `gh pr comment <pr_url> --body-file <path>` (same file-not-inline rule) — no code change.
- **Not worth fixing** (out of scope, working as intended, deferred): **reply with the reasoning** so the reviewer knows why it wasn't actioned (don't just skip it).
- **Sign every reply** with a distinct attribution — end each posted body, on its own line, with: `— 🤖 ship-feature-lane pipeline`.
- Nothing new → background another `sleep 300` (`run_in_background: true`) and end the turn; you'll be re-invoked for the next poll. A PR can sit for days — that's fine.
### 13 — Post-merge verification *(currently SKIPPED — CI-wait and dev-QC integrations are off)*
- `ccam stage merged --status running --note "PR merged — post-merge verification unavailable (F not built)"`
- Both dev-CI-wait and dev-QC are hardcoded off (see Setup). Go straight to Stage 14. When F ships these integrations, this stage gets its real implementation (mirroring Shipyard's: dev CI watch, then a `dev-qc` agent QCing the deployed site, looping to a Stage 15 follow-up-fix pattern on any issue found).
### 14 — Done
- Post the final report: PR merged, that post-merge verification is unavailable pending F.
- `ccam stage done --status passed --note "merged; post-merge verification unavailable (F not built)"` → STOP (leave the lane for the human to clear from the dashboard whenever).
## Escalation
Whenever you STOP early (an ambiguous merge conflict, an unexpected/unrecoverable failure, a missing agent this skill depends on), set `ccam stage <current> --status blocked --note "<what the human must decide>"` — the dashboard surfaces it. Then summarize for the human and wait.
````
- [ ] **Step 2: Verify frontmatter parses**
Run: `node -e "const fm = require('fs').readFileSync('.claude/skills/ship-feature-lane/SKILL.md','utf8').split('---')[1]; console.log(fm.includes('name: ship-feature-lane'))"`
Expected: prints `true`
- [ ] **Step 3: Confirm no name collision with the existing generic skill**
Run: `grep -l "^name: ship-feature" .claude/skills/*/SKILL.md`
Expected: two results — `.claude/skills/ship-feature/SKILL.md` (`name: ship-feature`, unrelated, pre-existing) and `.claude/skills/ship-feature-lane/SKILL.md` (`name: ship-feature-lane`, this task's file). Confirm the names themselves differ character-for-character (not just the directory).
- [ ] **Step 4: Commit**
```bash
git add .claude/skills/ship-feature-lane/SKILL.md
git commit -m "feat(lanes): port ship-feature-lane skill (Stages 0-14, E1)"
```
---
### Task 4: Dry-run verification + documentation
**Files:**
- Modify: `docs/LANES.md`
- Modify: `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md` (mark E1's slice progress — E as a whole stays "planned" since 3 of its 4 pieces remain)
**Interfaces:** none — verification + documentation.
- [ ] **Step 1: Dry run on a scratch repo**
Create a throwaway repo with a minimal profile (reuse this repo's existing profile fixtures from `server/__tests__/` as a template — search `grep -rl "profile.env" server/__tests__/*.test.js` for one that builds a full working `bootstrap`/`boot`/`ci-gate` hook set) and a lane pointed at it via `ccam lanes add`. Walk Stage 0 through Stage 2 by hand, running the exact commands the skill text specifies:
```bash
ccam feature activate smoke-test --title "Dry run"
ccam stage intake --status running
ccam stage plan
ccam stage implementing
ccam lanes hook ci-gate
ccam stage gates --evidence "sync-base preflight unavailable, skipped"
```
Expected: every command exits 0 and the lane's stage visibly advances (`ccam feature show smoke-test` after each step). Also exercise Stage 3's new pieces in isolation:
```bash
ccam lanes up --qc
ccam lanes hook e2e
```
Expected: `up --qc` succeeds (or fails with a clear profile-level error if the scratch profile has no e2e hook — that's fine, the point is `--qc` itself doesn't error).
- [ ] **Step 2: Record the dry run's outcome**
If any command in Step 1 does not behave as the skill text says, that is a plan/skill defect — fix `.claude/skills/ship-feature-lane/SKILL.md` (Task 3's file) directly, re-run the affected commands, and note the fix in this task's commit message. Do not proceed to documentation with a known-wrong skill file.
- [ ] **Step 3: `docs/LANES.md`**
Add a new top-level section (after "Cross-lane named locks", before "Pipeline stages and the five node states" if that heading already exists there, otherwise at the end — search for the nearest existing heading with `grep -n "^## " docs/LANES.md` and place it sensibly among the other pipeline-related sections) titled `## The ship-feature-lane skill (E1)`, covering:
- What it is: the ported Shipyard driving skill, `/ship-feature-lane <requirement>` inside a lane's working directory.
- What's real today vs. deferred: Stages 08, 1012, 14 work now; Stage 9 (ticket) and Stage 13 (post-merge dev verification) are hardcoded skipped pending F; Stages 67 (qc-local, senior-gate-reviewer agents) block until the agents-port task lands.
- The `--qc` boot flag and `QC_BOOT_ENV` profile declaration, with the exact format (`KEY=value` space-separated, same as `PORTS`).
- The `ship-feature` pipeline template and its 16 node ids.
- [ ] **Step 4: Mark E1's slice in the roadmap**
In `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, find the `## E` section (search `grep -n "^## E "`) and add a line directly under its `**Goal:**` line noting the slice split, e.g.:
```markdown
**Progress:** pipeline template + skill text (E1) done 2026-08-04 — see
`docs/superpowers/specs/2026-08-04-ship-feature-skill-design.md`. Agents,
`sync-base`, and F's integrations remain.
```
Do NOT change the status table's `**E**` row to done — E as a whole is not done until the remaining three pieces land.
- [ ] **Step 5: Verify and commit**
```bash
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
```
```bash
git add docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): document ship-feature-lane skill + --qc flag (E1)"
```