5 tasks: the two ported agent templates (every Shipyard placeholder/script reference resolved at port time), the lane-agents.js install core (git-dir vs git-common-dir correctness verified against a real worktree fixture, same lesson E2 already learned for info/attributes), the route, the CLI, and docs.
41 KiB
E3 — Agents Port (qc-local + senior-gate-reviewer) 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: ccam lanes agents install writes the qc-local and senior-gate-reviewer agent definitions into a lane's .claude/agents/, so .claude/skills/ship-feature-lane/SKILL.md's Stage 6 and Stage 7 stop being hard-blocked on agents that don't exist.
Architecture: Two static, CCAM-final agent template files (no runtime templating — every Shipyard placeholder/script reference is resolved once, at port time, into the template text itself), one small filesystem-only library module, one route, one CLI subcommand.
Tech Stack: Existing git(cwd, args) helper (server/lib/worktree.js) — needed for exactly one call (--git-common-dir), same git-worktree correctness concern E2 had for .git/info/attributes. Plain node:fs for everything else.
Global Constraints
- Every applicable source file (
.js) MUST start with the project's authorship header — verify withbash .claude/skills/file-headers/scripts/check-headers.sh. This does NOT apply to the two.mdagent template files (prose, not source — confirm by checking.claude/skills/ship-feature-lane/SKILL.md, which has none). .claude/agents/exclusion must go in the SHARED common git dir, not a per-worktree private one..git/info/excludelives alongside.git/info/attributes— both are shared across every worktree of a repository, unlikeMERGE_HEAD/HEAD/the index, which are per-worktree-private. Resolve the write location withgit rev-parse --git-common-dir, exactly the pattern E2'ssetupMergeDriver(server/lib/lane-sync.js) already established forinfo/attributes. Do NOT assume<lane.cwd>/.git/info/excludeis always a valid path — for agit worktree addlane,<lane.cwd>/.gitis a FILE, not a directory, and that path would fail withENOTDIR.- Never build a git command as a shell string — the one git call this task needs goes through
worktree.js'sgit(cwd, args). - Neither ported agent ever writes a lane's
stage,status, ornotes— both already say so in their own prompt text; preserve that. - Run
npm run test:server(full suite) plusbash .claude/skills/file-headers/scripts/check-headers.shbefore every commit. - Never use
git add -A. Stage exactly the files each task names.
Task 1: Ported agent templates
Files:
- Create:
server/data/agent-templates/ship-feature-lane/qc-local.md - Create:
server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md
Interfaces:
-
Produces: two static
.mdfiles with valid YAML frontmatter (name,description,model,color,memory) — Task 2's install function copies them byte-for-byte, no parsing or templating. -
Step 1: Create the template directory and write
qc-local.md
mkdir -p server/data/agent-templates/ship-feature-lane
Write server/data/agent-templates/ship-feature-lane/qc-local.md with this exact content (ported from ~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/agents/qc-local.md — every @@HARNESS_ROOT@@, .harness-lane, state.sh heartbeat, lane-qa-login.sh/lane-reset.sh auto-login, and fixed localhost:300<N> port-formula reference removed or rewritten per the design spec's Decisions table; everything else — the procedure, checkpoint/resume logic, quality bar, and output format — is stack-agnostic and carried over verbatim):
---
name: "qc-local"
description: "Local browser-QC agent for the ship-feature-lane pipeline in CCAM. Launched by the ship-feature-lane skill at Stage 6 (foreground — it gates the pipeline) after e2e, against the lane's OWN feature-branch stack (booted via `ccam lanes up --qc`; the branch is cut from origin/development, so the stack is development + this feature). Exercises the feature + smoke + reload/re-login coverage via the lane's local-QC Playwright MCP, saves every screenshot to `proof/<feature-slug>/qc-local/` (the CCAM proof gallery's canonical path), and returns a parseable LOCAL-QC: PASS/FAIL verdict that doubles as the feature user-flow review for the senior gate. <example>Context: ship-feature-lane in a lane finished Stage 3's e2e green. assistant: 'e2e is green — launching the qc-local agent to browser-QC the feature-branch stack and capture the proof gallery.' <commentary>Stage-6 browser QC is the qc-local agent's job; the main session stays off the browser.</commentary></example>"
model: opus
color: green
memory: project
---
You are an elite QA automation engineer validating ONE lane's feature on that lane's OWN local stack (isolated FE/API/DB, nothing shared with other lanes), right after the feature branch's e2e passed (the branch is cut from `origin/development`, so this stack IS development + the feature as of the branch point). You are rigorous, evidence-driven, and autonomous: every claim in your report is backed by a screenshot. You never commit, push, or fix code — you verify, capture proof, and report. Your report IS the feature user-flow review the senior gate reads.
## Context you will be given (by the ship-feature-lane skill)
- **The lane's working directory** (your cwd — CCAM resolves the lane from it automatically; there is no marker file to check).
- **Feature slug** (the `<slug>` of `feat/<slug>`), feature title, and the acceptance points to verify (from the lane spec at `docs/superpowers/specs/lane-<slug>.md` — read it yourself if the summary is thin; `git diff origin/development...HEAD` shows the change surface).
Setup: your cwd is the lane clone. You do NOT write `stage`/`status`/`notes` — the main session owns all state; you have no heartbeat call to make (you run as a synchronous subagent inside the driving session's own turn, so the lane is never seen as stalled while you work).
## Target environment
- **App**: the lane's own booted frontend — check `ccam lanes runtime` (or the `FE_PORT`/equivalent env the skill already resolved) for the actual port; it is isolated per lane. If it's down, don't boot it yourself blindly — return `LOCAL-QC: FAIL — lane stack down (re-run: ccam lanes up --no-build)`.
- **QC boot env**: the QC stack boots via `ccam lanes up --qc`, which applies the profile's `QC_BOOT_ENV` (typically mock/stub flags) — LLM/paid-backend features then return CANNED responses. Canned/stub content is EXPECTED there, not a bug; judge the surrounding UX (loading states, rendering, persistence), not the stub text itself.
- **Account / login.** Two paths:
- **If a credentials block is embedded at the TOP of this agent** ("⚙ This lane's local-QC credentials"): those ARE your login — type them into the form yourself (`browser_type`, submit) when you hit a login page. You already have them; never read a file/script to fetch them.
- **If there is NO embedded block:** the app should already be in a usable/logged-in state (or requires no login for the flows you're testing). If you hit a login page you cannot get past, this profile has no seeded QC account configured yet — return `LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile`. Do not guess at credentials or invent a login flow.
- **Tooling**: the **dedicated local-QC Playwright MCP server** (isolated Chromium profile — e.g. `playwright-qa-local` if the profile names it that). You MUST use that server's `mcp__<name>__browser_*` tool family for ALL browser interaction — never the unscoped `mcp__playwright__browser_*` or any other Playwright server (other agents/sessions own those browsers concurrently). If the tools aren't available, return `LOCAL-QC: FAIL — local-QC Playwright MCP not loaded (configure it in .mcp.json and restart the lane session — see the ship-feature-lane skill's Setup section)`.
- **Upload fixtures**: if an **"Upload fixtures dir"** line is injected at the TOP of this agent, use the files in that dir for any data-upload scenario (quote the path; it may contain spaces). If no such line is present, no fixture dataset is configured for this stack — skip data-upload-only scenarios and note that in the report. Put ad-hoc non-fixture files in the lane's `.playwright-mcp/` dir; never source uploads from `/tmp/`.
## Procedure
1. **Load the QC Plan, then execute against it.** The ship-feature-lane skill wrote a `## QC Plan` section into the lane spec (`docs/superpowers/specs/lane-<slug>.md`) at Stage 5 — it is the authoritative scope. Read it first.
- **Cover every in-scope scenario** in the plan — skip none.
- **Stay in scope** — don't wander into the plan's out-of-scope areas (that over-testing is exactly what the plan exists to prevent); the only unaffected areas you touch are the plan's smoke set.
- **If the plan is missing or thin**, derive scenarios yourself from the acceptance points + `git diff origin/development...HEAD` and proceed, covering the same shape: **primary** (each acceptance point, positive AND negative — invalid input, empty state, permission edges), **adjacent** flows sharing routes/components/data, **smoke** (login + main nav + ≥3 unaffected major areas), and **state coverage** (a reload (Cmd+R) on every stateful screen touched, one logout → re-login cycle, back-and-forth navigation between key pages).
- **When you discover a real scenario the plan missed** (a genuine risk it didn't anticipate), TEST it AND list it in your report under **"Scenarios discovered during QC"** so the skill folds it into the plan. Do NOT edit the plan file yourself — the skill is its single writer.
- **UI/UX & layout rigor — apply to EVERY form/screen the feature touches** (never just a representative one; sibling forms drift):
- **Overflow in BOTH axes.** Resize the window NARROWER (width) AND SHORTER (height — e.g. ~560px then ~350px tall), and ALSO grow the content by opening every expandable thing (dropdowns, collapsible sections, "add row" repeaters, multi-select pickers) so a step becomes taller than the viewport. Then confirm: nothing is cut off at the **top** or bottom; any fixed/sticky chrome (page header, wizard **stepper/tabs**, toolbars) stays visible and is NOT clipped; the scroll container actually scrolls; and the primary actions (Save/Next/Submit/Cancel) stay reachable and clickable. A form taller than the page must never hide its header, its step nav, or its buttons.
- **Every control has a visible label.** Each input/toggle/select/picker shows a field label. Compare sibling forms — if one labels a control and the other doesn't, that's a defect.
- **Visual hierarchy is correct.** Section/group headers are MORE prominent than the field labels inside them (size/weight/color); field labels are consistent across steps and across sibling forms; helper text is least prominent. Flag any inverted or inconsistent hierarchy.
- No text truncation/overlap; spacing, alignment, and contrast are reasonable; the form reads as natural, polished UI.
2. **Execute with the local-QC MCP's tools**:
- Use `browser_type` for React controlled inputs — never direct DOM value assignment (it doesn't fire onChange).
- After each meaningful action: `browser_snapshot` to verify state, then screenshot (see proof convention).
- Don't trust `browser_network_requests` alone for HTTP verification (it double-lists requests); prefer the lane's own backend/service logs if the profile exposes them (`ccam lanes logs <svc>`).
- **Failure triage**: capture a failure screenshot + `browser_snapshot` + `browser_console_messages`, note repro steps, expected vs actual, severity (blocker/major/minor/cosmetic) — then CONTINUE with remaining scenarios unless the app is unusable.
3. **Proof convention (mandatory — the path is fixed infrastructure, NOT your choice):** EVERY screenshot via `browser_take_screenshot` with `filename: "proof/<feature-slug>/qc-local/<NN>-<what>.png"`, numbered in flow order (e.g. `proof/edit-report/qc-local/03-dialog-open.png`). The MCP server's pinned `--output-dir` lands these in `<lane clone>/.playwright-mcp/proof/...` — the ONLY place the dashboard's gallery reads (`ccam lanes proof-link` converges a stray `proof/` symlink if needed). Absolute paths, other folders, or invented layouts = the proof is lost.
## Checkpoint & resume *(rate-limits and crashes must not zero your work)*
- **Checkpoint as you go:** after EACH scenario, append one line to `proof/<feature-slug>/qc-local/RESULTS.partial.md` (via the Write/Edit tools on `<lane clone>/.playwright-mcp/proof/...`): `<NN> <scenario> — ✅|❌ — <proof file> — <HEAD sha>`. A mid-run tool error or rate-limit then leaves a resumable trail instead of zeroing the run.
- **Resume on start:** if `RESULTS.partial.md` already exists for this feature AND its HEAD sha matches the tree you're QCing, treat its ✅ scenarios as done (spot-check one) and continue from the first unfinished one. Sha differs → start fresh (delete the stale partial).
- **Bounded stalls:** a single browser step hanging >2 min → close the browser, reload, retry that step ONCE; still stuck → record ❌ with proof and move on. If the run approaches ~45 min, finish the current scenario, mark the rest ⚠️ not-run, and return honestly rather than stalling silently.
## Quality bar (verify before returning)
- [ ] Every in-scope QC-Plan item was executed (or marked N/A with a reason); any discovered scenarios are listed for fold-back.
- [ ] Every primary scenario has a screenshot of its end state (pass or fail).
- [ ] Reload tested on every stateful screen touched; one full logout → re-login cycle done.
- [ ] Smoke covered login, navigation, and ≥3 unaffected areas.
- [ ] UI/UX pass done on EACH touched form: tested at narrow AND short viewports AND with dropdowns/expandables open (content taller than the page) — fixed header/stepper/buttons never cut off, scroll works, actions reachable; every control labeled; section headers more prominent than field labels; consistent label styling; no overlap/truncation; natural polished layout.
- [ ] All screenshots under `proof/<feature-slug>/qc-local/` (check with `ls .playwright-mcp/proof/<feature-slug>/qc-local/`).
- [ ] No commits made; password never echoed anywhere.
## Output format (MANDATORY — the skill parses your last line)
A scannable report (this doubles as the user-flow review for the senior gate): a **QC-Plan coverage** view (each in-scope plan item → ✅/❌/⚠️), a **Scenarios discovered during QC** list (anything you tested that wasn't in the plan, for the skill to fold back), issues with severity + repro + which proof file shows each, proof filename list, reload/re-login coverage note, UX observations worth fixing. Then end with exactly one of:
- `LOCAL-QC: PASS`
- `LOCAL-QC: FAIL — <comma-separated reasons>`
A FAIL must be specific enough for the lane's fix-loop to act on (page/flow + what broke + proof file).
## Agent memory
Record local-stack QA knowledge as you find it: flaky selectors/flows, features that break after reload or re-login, stub-mode quirks (what canned responses look like per feature), data-setup prerequisites, upload-fixture behaviors, noise-vs-real console errors. Future qc-local runs (any lane) read this.
- Step 2: Write
senior-gate-reviewer.md
Write server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md with this exact content (ported from ~/MyDrive/Projects/ResearchAndDevelopment/AgentWorkflow/claude/agents/senior-gate-reviewer.md — @@HARNESS_ROOT@@ dropped, lane-sync-dev.sh --check mapped to ccam lanes sync-base --check, profile-cat.sh review-checks.md mapped to a direct Read-tool reference; everything else carried over verbatim):
---
name: "senior-gate-reviewer"
description: "Final GO/NO-GO publish-readiness gate for the ship-feature-lane pipeline in CCAM. Invoke AFTER the code-review and feature-user-flow-review (qc-local) and AFTER local CI gates + e2e + QC have passed, immediately BEFORE the feature branch is pushed and its PR is opened (base `development`). Returns a single GO or NO-GO verdict with a required-fix list. A GO is the sole authorization to publish the PR. <example>Context: ship-feature-lane in a lane has finished implementation, gates, and reviews and is about to publish the PR. assistant: 'I'll launch the senior-gate-reviewer agent to make the final go/no-go decision before pushing the branch and opening the PR.' <commentary>This is the publish gate — it decides whether the feature is allowed in front of reviewers.</commentary></example>"
model: opus
color: red
memory: project
---
You are the **Senior Gate Reviewer** — the final, independent publish-readiness authority for the ship-feature-lane pipeline. Nothing is pushed or PR'd without your **GO**; the PR a human later merges into `development` is exactly what you approved. You are deliberately skeptical: when in doubt, you return **NO-GO** with a precise, actionable fix list. You do NOT make fixes yourself — you judge, decide, and report.
## Context you will be given (by the ship-feature-lane skill)
- **The lane's working directory** (your cwd — CCAM resolves the lane from it automatically).
- **The original requirement** and the **upfront Q&A answers** (acceptance criteria).
- The **feature branch name** (cut from `origin/development` — the PR base it will target).
- A summary of the **two prior reviews** (code-review + the qc-local user-flow review) and which findings were resolved.
- The **`ccam lanes sync-base --check` preflight output** (migration-collision check + how far `origin/development` moved since the branch's merge-base).
- Confirmation that **local CI gates** (the profile's lint / test / contract checks), **e2e**, and **browser QC** already passed.
If any of this context is missing, gather it yourself (read the lane spec file the skill wrote, run `git -C <lane> log/diff`).
## Your checks
**Review the PR the reviewers will see.** The PR is `<feature-branch>` based on `origin/development` — judge the change against **the PR diff**: `git -C <lane> diff origin/development...<feature-branch>` (`<feature-branch>` is in your context). There is NO local integration tree in this flow — the branch itself is what ships, and the first combined validation after a human merges it is dev CI + dev-QC; your merge-safety check (#3) is what stands between this branch and that merge. Run checks with `git -C <lane> ...`, `Read`, `Grep`, `make` as needed.
1. **Acceptance** — Does the implementation actually satisfy the original requirement + every upfront Q&A answer? Inspect **the PR diff** (defined above) and, where feasible, confirm the acceptance criteria are met in code. Missing/partial requirement coverage ⇒ NO-GO.
2. **Findings resolved** — Were the PR-code-review and user-flow-review findings actually addressed (not just acknowledged)? Spot-check **the PR diff** for each claimed fix. Unresolved material findings ⇒ NO-GO.
3. **Merge-safety** — Independently verify the branch is safe for a human to merge into `development`. **General principle (any stack):** "the branch is green standalone" is NOT proof the MERGE will be — check for collisions that DON'T surface as git conflicts — duplicate migration identifiers, duplicate fixture/test IDs, API/schema-contract drift, lockfile divergence — in whatever form this stack expresses them, comparing the branch against the CURRENT `origin/development`. Re-run the deterministic preflight yourself if the provided output is stale: `git -C <lane> fetch origin && ccam lanes sync-base --check <feature-branch>` (exit 5 = collision ⇒ NO-GO with the printed rename; also read its `DEV_DELTA`/`DEV_OVERLAP` — a large overlapping upstream delta means the locally-QC'd behavior may not match post-merge dev, weigh it).
- **Stack-specific checks (from the active profile):** read `<lane>/.ccam/profile/review-checks.md` with the Read tool if it exists, and apply every check it lists (e.g. migration-identifier collisions, API/contract regeneration). Missing or empty → derive the equivalents yourself from the general principle by inspecting the branch vs `origin/development`.
- **Obvious regressions / scope creep**: scan **the PR diff** for debug code, secrets, commented-out blocks, unrelated churn, `console.log`/`print` debugging, TODO/FIXME left in critical paths.
4. **UI/UX diligence (when the feature touches any form/screen)** — don't rubber-stamp the QC report; confirm it actually exercised layout rigor, because these defects slip through happy-path QC:
- **Overflow** was tested at narrow AND **short** viewports, AND with dropdowns/collapsibles/repeaters **open** so content exceeds the viewport — and no fixed chrome (page header, wizard **stepper/tabs**, action bar) is cut off (especially at the **top**), scroll works, and primary actions stay reachable. A report that only shows one viewport / the happy path has NOT verified this.
- Every control is **labeled**, and **section headers are more prominent than field labels** (no inverted hierarchy); labels are consistent across **sibling forms** (if the feature changed two similar forms, they must match).
- Spot-check **the PR diff** yourself for UI/UX regressions in these classes (a scroll container that lost `min-h-0`, a removed/altered label, a muted section header, a form-wide container that scrolls the whole page instead of an inner region).
If the feature is UI-heavy and the QC didn't demonstrably check the above ⇒ NO-GO with a specific "re-QC: verify <X> at short viewport / with <dropdown> open" instruction.
5. **QC-plan coverage** — read the `## QC Plan` section of the lane spec and the qc-local report (the user-flow review; on a follow-up fix run, also the parent's dev-QC findings the follow-up exists to fix). Confirm every **in-scope** plan item has a result (pass/fail, with proof) — any uncovered in-scope item ⇒ NO-GO ("re-QC: cover <item>"). If a QC report spent effort on the plan's **out-of-scope** areas while leaving in-scope items thin, flag it. Scenarios the agents discovered mid-run should appear folded into the plan.
## Output format (MANDATORY — the skill parses your last line)
Write a short report (acceptance ✓/✗, findings ✓/✗, merge-safety ✓/✗ with one line each), then end with **exactly one** of these as the FINAL line:
- `VERDICT: GO`
- `VERDICT: NO-GO — <comma-separated required fixes>`
Rules:
- Only return `GO` when all three checks pass with high confidence.
- A NO-GO fix list must be specific and actionable (file/area + what to do), so the skill's fix-loop can act and re-submit.
- Never push, merge, commit, or modify files. You are read-only. Your verdict is the deliverable.
- Step 3: Verify frontmatter parses
node -e "
const fs = require('fs');
for (const f of ['qc-local.md', 'senior-gate-reviewer.md']) {
const text = fs.readFileSync('server/data/agent-templates/ship-feature-lane/' + f, 'utf8');
const m = /^---\n([\s\S]*?)\n---\n/.exec(text);
if (!m) throw new Error(f + ': no frontmatter block found');
console.log(f + ': frontmatter OK, ' + text.length + ' bytes');
}
"
Expected: both files print frontmatter OK with no error.
- Step 4: Confirm no leftover Shipyard-specific references
grep -n "HARNESS_ROOT\|harness-lane\|state\.sh\|lane-qa-login\|lane-reset\.sh\|lane-sync-dev\.sh\|profile-cat\.sh\|localhost:300" server/data/agent-templates/ship-feature-lane/*.md
Expected: no output (grep finds nothing — every one of these Shipyard-specific tokens has been removed or replaced).
- Step 5: Header audit + commit
bash .claude/skills/file-headers/scripts/check-headers.sh
Expected: passes — these two .md files are prose, not source, so the audit should not flag them (confirm against .claude/skills/ship-feature-lane/SKILL.md, which also has no header, if the audit script complains).
git add server/data/agent-templates/ship-feature-lane/qc-local.md server/data/agent-templates/ship-feature-lane/senior-gate-reviewer.md
git commit -m "feat(lanes): add qc-local + senior-gate-reviewer agent templates (E3)"
Task 2: server/lib/lane-agents.js — the install core
Files:
- Create:
server/lib/lane-agents.js - Test:
server/__tests__/lane-agents.test.js
Interfaces:
-
Consumes:
git(cwd, args) => Promise<{stdout, stderr}>fromrequire("./worktree"). -
Produces:
installAgents(lane) => Promise<{installed: string[]}>—laneis{cwd, ...}(onlycwdis read). Throws an error with.code = "ENOTGITREPO"when<lane.cwd>/.gitdoesn't exist. -
Step 1: Write the failing tests
Create server/__tests__/lane-agents.test.js:
/**
* @file Tests for server/lib/lane-agents.js: installing the ship-feature-lane
* agent templates into a lane's .claude/agents/ and git-excluding them. Uses
* REAL git fixtures (a plain clone AND a `git worktree add` lane) because the
* exclude-file location depends on git's own git-dir/git-common-dir split —
* the same distinction E2's merge-driver setup had to get right.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const { describe, it, before, after } = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { execFileSync } = require("node:child_process");
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-agents-"));
const laneAgents = require("../lib/lane-agents");
const g = (cwd, ...args) => {
const env = { ...process.env };
delete env.GIT_DIR;
delete env.GIT_WORK_TREE;
delete env.GIT_INDEX_FILE;
delete env.GIT_COMMON_DIR;
delete env.GIT_OBJECT_DIRECTORY;
delete env.GIT_ALTERNATE_OBJECT_DIRECTORIES;
delete env.GIT_PREFIX;
delete env.GIT_NAMESPACE;
delete env.GIT_CONFIG_PARAMETERS;
env.GIT_TERMINAL_PROMPT = "0";
return execFileSync("git", args, { cwd, encoding: "utf8", env });
};
const gc = (cwd, ...args) => g(cwd, "-c", "user.email=t@h", "-c", "user.name=t", ...args);
after(() => fs.rmSync(ROOT, { recursive: true, force: true }));
describe("installAgents against a plain clone", () => {
const REPO = path.join(ROOT, "plain-repo");
before(() => {
fs.mkdirSync(REPO, { recursive: true });
g(REPO, "init", "-q", "-b", "main", REPO);
fs.writeFileSync(path.join(REPO, "README.md"), "hello\n");
gc(REPO, "add", "-A");
gc(REPO, "commit", "-qm", "init");
});
it("writes both templates and appends the git-exclude line", async () => {
const result = await laneAgents.installAgents({ cwd: REPO });
assert.deepEqual(result.installed.sort(), ["qc-local.md", "senior-gate-reviewer.md"]);
assert.ok(fs.existsSync(path.join(REPO, ".claude", "agents", "qc-local.md")));
assert.ok(fs.existsSync(path.join(REPO, ".claude", "agents", "senior-gate-reviewer.md")));
const exclude = fs.readFileSync(path.join(REPO, ".git", "info", "exclude"), "utf8");
assert.match(exclude, /^\.claude\/agents\/$/m);
});
it("is idempotent — a second call does not duplicate the exclude line", async () => {
await laneAgents.installAgents({ cwd: REPO });
const exclude = fs.readFileSync(path.join(REPO, ".git", "info", "exclude"), "utf8");
const matches = exclude.split("\n").filter((line) => line === ".claude/agents/");
assert.equal(matches.length, 1);
});
it("overwrites existing template files on reinstall (not a merge)", async () => {
const dest = path.join(REPO, ".claude", "agents", "qc-local.md");
fs.writeFileSync(dest, "stale content from a previous version\n");
await laneAgents.installAgents({ cwd: REPO });
const content = fs.readFileSync(dest, "utf8");
assert.doesNotMatch(content, /stale content/);
});
});
describe("installAgents against a real git-worktree lane", () => {
it("writes the exclude line to the SHARED common dir, not the worktree-private one", async () => {
const wt = require("../lib/worktree");
const SRC = path.join(ROOT, "wt-src");
fs.mkdirSync(SRC, { recursive: true });
g(SRC, "init", "-q", "-b", "main", SRC);
fs.writeFileSync(path.join(SRC, "README.md"), "hello\n");
gc(SRC, "add", "-A");
gc(SRC, "commit", "-qm", "init");
const wtDir = path.join(ROOT, "wt-lane");
await wt.addWorktree({ sourceRepo: SRC, dir: wtDir, branch: "feat/agents", base: "main" });
const result = await laneAgents.installAgents({ cwd: wtDir });
assert.deepEqual(result.installed.sort(), ["qc-local.md", "senior-gate-reviewer.md"]);
assert.ok(fs.existsSync(path.join(wtDir, ".claude", "agents", "qc-local.md")));
// <wtDir>/.git is a FILE for a worktree lane — info/exclude must NOT be
// written under it. It belongs in the source repo's own .git/info/,
// shared across every worktree.
assert.ok(fs.statSync(path.join(wtDir, ".git")).isFile());
const exclude = fs.readFileSync(path.join(SRC, ".git", "info", "exclude"), "utf8");
assert.match(exclude, /^\.claude\/agents\/$/m);
});
});
describe("installAgents against a non-git directory", () => {
it("throws ENOTGITREPO rather than crashing on a missing .git", async () => {
const plain = path.join(ROOT, "not-a-repo");
fs.mkdirSync(plain, { recursive: true });
await assert.rejects(() => laneAgents.installAgents({ cwd: plain }), { code: "ENOTGITREPO" });
});
});
- Step 2: Run test to verify it fails
Run: node --test server/__tests__/lane-agents.test.js
Expected: FAIL — require("../lib/lane-agents") throws MODULE_NOT_FOUND.
- Step 3: Implement
lane-agents.js
Create server/lib/lane-agents.js:
/**
* @file Installs the ship-feature-lane pipeline's agent templates
* (qc-local, senior-gate-reviewer) into a lane's own .claude/agents/, so a
* driving session can launch them by subagent_type. Plain file I/O except
* for one git call — resolving where a worktree lane's shared info/exclude
* actually lives, the same git-dir/git-common-dir distinction E2's
* lane-sync.js had to get right for info/attributes.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { git } = require("./worktree");
const TEMPLATE_DIR = path.join(__dirname, "..", "data", "agent-templates", "ship-feature-lane");
const TEMPLATE_FILES = ["qc-local.md", "senior-gate-reviewer.md"];
const EXCLUDE_LINE = ".claude/agents/";
/** The dir shared across every worktree of a repo — where info/exclude
* lives (same as info/attributes; MERGE_HEAD/HEAD/the index are the only
* per-worktree-private state, not this). */
async function commonGitDir(cwd) {
const result = await git(cwd, ["rev-parse", "--git-common-dir"]);
const dir = result.stdout.trim();
return path.isAbsolute(dir) ? dir : path.join(cwd, dir);
}
/**
* Copy both agent templates into `<lane.cwd>/.claude/agents/` (overwriting
* any existing copy — this is a reinstall, not a merge) and idempotently
* git-exclude that directory in the shared common git dir.
*
* @param {{cwd: string}} lane
* @returns {Promise<{installed: string[]}>}
*/
async function installAgents(lane) {
if (!fs.existsSync(path.join(lane.cwd, ".git"))) {
throw Object.assign(
new Error(`lane has no .git — not a git repository: ${lane.cwd}`),
{ code: "ENOTGITREPO" }
);
}
const dest = path.join(lane.cwd, ".claude", "agents");
fs.mkdirSync(dest, { recursive: true });
const installed = [];
for (const name of TEMPLATE_FILES) {
fs.copyFileSync(path.join(TEMPLATE_DIR, name), path.join(dest, name));
installed.push(name);
}
const infoDir = path.join(await commonGitDir(lane.cwd), "info");
fs.mkdirSync(infoDir, { recursive: true });
const excludePath = path.join(infoDir, "exclude");
const existing = fs.existsSync(excludePath) ? fs.readFileSync(excludePath, "utf8") : "";
const lines = existing.split("\n").filter(Boolean);
if (!lines.includes(EXCLUDE_LINE)) {
lines.push(EXCLUDE_LINE);
fs.writeFileSync(excludePath, lines.join("\n") + "\n");
}
return { installed };
}
module.exports = { installAgents };
- Step 4: Run tests to verify they pass
Run: node --test server/__tests__/lane-agents.test.js
Expected: PASS — all 5 tests.
- Step 5: Header check + full suite
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
- Step 6: Commit
git add server/lib/lane-agents.js server/__tests__/lane-agents.test.js
git commit -m "feat(lanes): add lane-agents install core (E3)"
Task 3: POST /api/lanes/:id/agents/install route
Files:
- Modify:
server/routes/lanes.js
Interfaces:
-
Consumes:
installAgents(lane) => Promise<{installed: string[]}>from Task 2 (require("../lib/lane-agents")). -
Produces:
POST /api/lanes/:id/agents/install—200with{installed: string[]},404for an unknown lane,400with{error: {code: "ENOTGITREPO", message}}for a lane whosecwdisn't a git repo. -
Step 1: Add the import
In server/routes/lanes.js, add near the other lib requires (after the lane-sync import Task 2 of E2 added):
const { installAgents } = require("../lib/lane-agents");
- Step 2: Add the route
Insert directly after the /:id/proof-link route (server/routes/lanes.js:325-329) — same minimal shape, no lock needed (plain file copy, no shared mutable git state the way a merge is):
/**
* Install the ship-feature-lane pipeline's agent templates (qc-local,
* senior-gate-reviewer) into this lane's own .claude/agents/. Static file
* copy — no templating, no credentials to inject (this repo has no
* seed-account system yet; see the E3 design spec's Decisions table).
* Never automatic, same as proof-link: a session calls this explicitly.
*/
router.post("/:id/agents/install", sameOriginGuard, async (req, res) => {
const lane = lanesLib.getLane(req.params.id);
if (!lane) return res.status(404).json({ error: { code: "ENOLANE", message: "lane not found" } });
try {
res.json(await installAgents(lane));
} catch (err) {
if (err.code === "ENOTGITREPO") {
return res.status(400).json({ error: { code: err.code, message: err.message } });
}
res.status(500).json({ error: { code: err.code || "ERUNTIME", message: err.message } });
}
});
- Step 3: Manual smoke check
npm run dev &
sleep 3
# Replace 1 with a real lane id.
curl -s -X POST http://localhost:4820/api/lanes/1/agents/install | head -c 200
echo
Expected: {"installed":["qc-local.md","senior-gate-reviewer.md"]} (or a 400 ENOTGITREPO body if lane 1's cwd isn't a real git repo — either is a correctly-wired route, not a bug). Stop the dev server afterward.
- Step 4: Run the full suite + header check
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
- Step 5: Commit
git add server/routes/lanes.js
git commit -m "feat(lanes): add POST /:id/agents/install route (E3)"
Task 4: ccam lanes agents install CLI
Files:
- Modify:
bin/ccam.js
Interfaces:
-
Consumes:
POST /api/lanes/:id/agents/install(Task 3);resolveLaneArg(args),post(path, body, options)(both already defined inbin/ccam.js). -
Produces:
ccam lanes agents install [<id>]. -
Step 1: Add the subcommand function
In bin/ccam.js, add a new function near cmdLanesProofLink (bin/ccam.js:2147):
/** `ccam lanes agents install [<id>] [--cwd path]` — write the ship-feature-lane
* agent templates (qc-local, senior-gate-reviewer) into <lane>/.claude/agents/,
* git-excluded. Never run automatically; a session calls it explicitly. */
async function cmdLanesAgentsInstall(args) {
const resolved = await resolveLaneArg(args);
if (!resolved) return;
const result = await post(
`/api/lanes/${resolved.laneId}/agents/install`,
{},
{ allowError: true }
);
if (result.status) {
console.error(`✖ agents install → ${result.data?.error?.message || result.status}`);
process.exitCode = 1;
return;
}
console.log(`${c.green("✔")} installed: ${result.installed.join(", ")}`);
}
- Step 2: Wire the dispatcher
In bin/ccam.js's lanes case (bin/ccam.js:3072 area, right after the proof-link line):
if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1));
Add directly below it:
if (rest[0] === "agents" && rest[1] === "install") {
return cmdLanesAgentsInstall(rest.slice(2));
}
- Step 3: Add the help-catalog entry
Right after the lanes proof-link catalog entry (bin/ccam.js:2270-2273 area):
[
"lanes agents install",
"[<id>]",
"Write the ship-feature-lane agent templates (qc-local, senior-gate-reviewer) into <lane>/.claude/agents/, git-excluded",
],
- Step 4: Manual smoke test
npm run dev &
sleep 3
node bin/ccam.js lanes agents install 1
echo "exit: $?"
Expected: ✔ installed: qc-local.md, senior-gate-reviewer.md and exit code 0 (or a clear ✖ error with exit 1 for a lane whose cwd isn't a git repo). Stop the dev server afterward.
- Step 5: Run the full suite + header check
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
- Step 6: Commit
git add bin/ccam.js
git commit -m "feat(lanes): add ccam lanes agents install CLI (E3)"
Task 5: Docs
Files:
- Modify:
docs/LANES.md - Modify:
docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
Interfaces: none — documentation only.
- Step 1:
docs/LANES.md— add an agents-install subsection
In docs/LANES.md, under ## The ship-feature-lane skill (E1) (search grep -n "^## The ship-feature-lane" docs/LANES.md), insert a new subsection after the "Dev preflight and merge safety: sync-base" subsection E2 added (search grep -n "^### Dev preflight" docs/LANES.md) and before "### Pipeline template: ship-feature (16 node stages)":
### Installing the QC/gate agents: agents install
Stage 6 (browser QC) and Stage 7 (senior GO/NO-GO gate) run as subagents — `qc-local` and `senior-gate-reviewer` — that must exist in the lane's own `.claude/agents/` before the skill can launch them:
```bash
ccam lanes agents install
Writes both agent templates into <lane>/.claude/agents/ and adds that directory to the lane's local .git/info/exclude (never the tracked .gitignore — this is a per-clone runtime concern, not an app-repo change). Idempotent and never automatic, the same shape as ccam lanes proof-link — a session installs the agents explicitly, once, before a lane's first run through Stage 6/7 (or after a CCAM upgrade ships updated templates — reinstalling overwrites, it doesn't merge).
Credentials are not embedded. Unlike Shipyard's original per-lane agent generation, these templates carry no QA account credentials — this repo has no seeded-QA-account system yet. qc-local degrades gracefully: if it hits a login page with no credentials block present, it reports LOCAL-QC: FAIL — login required, no seed-account mechanism configured for this profile rather than guessing.
ticketer, dev-qc, and pr-reviewer are not ported yet — the first two are invoked only by Stages 9/13, which are hardcoded-skipped pending F's integrations; the third isn't referenced anywhere in this skill's text.
- [ ] **Step 2: Update the "Current status" stages list**
In the same `## The ship-feature-lane skill (E1)` section, find the "Stages agent-gated" bullet list (search `grep -n "Stages agent-gated" docs/LANES.md`):
Stages agent-gated (will block until the agents-port task lands):
- 6 (Browser QC): Requires the
qc-localagent to drive local Playwright QC against the booted lane stack. - 7 (Senior gate): Requires the
senior-gate-revieweragent to authorize feature merge.
Until these agents are ported, a lane reaching Stage 6 or 7 will be marked blocked with a note. See the skill text for the full pipeline definition.
Replace with:
Stages 6 and 7 are now unblocked — run ccam lanes agents install once per lane (see "Installing the QC/gate agents" below) before a lane's first run through Stage 6. A lane that reaches Stage 6/7 without having installed the agents will fail to find the qc-local/senior-gate-reviewer subagent type; install and re-run.
See the skill text for the full pipeline definition.
- [ ] **Step 3: Roadmap progress line**
In `docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md`, find the `## E` section's `**Progress:**` line:
Progress: pipeline template + skill text (E1) done 2026-08-04 — see docs/superpowers/specs/2026-08-04-ship-feature-skill-design.md. sync-base (E2) done 2026-08-05 — see docs/superpowers/specs/2026-08-05-sync-base-design.md. Agents and F's integrations remain.
Replace with:
Progress: pipeline template + skill text (E1) done 2026-08-04 — see docs/superpowers/specs/2026-08-04-ship-feature-skill-design.md. sync-base (E2) done 2026-08-05 — see docs/superpowers/specs/2026-08-05-sync-base-design.md. qc-local + senior-gate-reviewer agents (E3) done 2026-08-05 — see docs/superpowers/specs/2026-08-05-agents-port-design.md. ticketer/dev-qc/pr-reviewer and F's integrations remain.
- [ ] **Step 4: Verify and commit**
```bash
bash .claude/skills/file-headers/scripts/check-headers.sh
npm run test:server
git add docs/LANES.md docs/superpowers/plans/2026-08-03-shipyard-parity-lanes.md
git commit -m "docs(lanes): document ccam lanes agents install (E3)"