diff --git a/docs/superpowers/plans/2026-08-05-agents-port.md b/docs/superpowers/plans/2026-08-05-agents-port.md new file mode 100644 index 0000000..6b5b716 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-agents-port.md @@ -0,0 +1,669 @@ +# 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 with `bash .claude/skills/file-headers/scripts/check-headers.sh`. This does NOT apply to the two `.md` agent 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/exclude` lives alongside `.git/info/attributes` — both are shared across every worktree of a repository, unlike `MERGE_HEAD`/`HEAD`/the index, which are per-worktree-private. Resolve the write location with `git rev-parse --git-common-dir`, exactly the pattern E2's `setupMergeDriver` (`server/lib/lane-sync.js`) already established for `info/attributes`. Do NOT assume `/.git/info/exclude` is always a valid path — for a `git worktree add` lane, `/.git` is a FILE, not a directory, and that path would fail with `ENOTDIR`. +- Never build a git command as a shell string — the one git call this task needs goes through `worktree.js`'s `git(cwd, args)`. +- Neither ported agent ever writes a lane's `stage`, `status`, or `notes` — both already say so in their own prompt text; preserve that. +- 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: 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 `.md` files 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`** + +```bash +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` 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): + +```markdown +--- +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//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. 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.' Stage-6 browser QC is the qc-local agent's job; the main session stays off the browser." +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 `` of `feat/`), feature title, and the acceptance points to verify (from the lane spec at `docs/superpowers/specs/lane-.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____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-.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 `). + - **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//qc-local/-.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 `/.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//qc-local/RESULTS.partial.md` (via the Write/Edit tools on `/.playwright-mcp/proof/...`): ` — ✅|❌ — — `. 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//qc-local/` (check with `ls .playwright-mcp/proof//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 — ` + +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): + +```markdown +--- +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. 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.' This is the publish gate — it decides whether the feature is allowed in front of reviewers." +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 log/diff`). + +## Your checks + +**Review the PR the reviewers will see.** The PR is `` based on `origin/development` — judge the change against **the PR diff**: `git -C diff origin/development...` (`` 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 ...`, `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 fetch origin && ccam lanes sync-base --check ` (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 `/.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 at short viewport / with 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 "). 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 — ` + +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** + +```bash +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** + +```bash +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 +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). + +```bash +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}>` from `require("./worktree")`. +- Produces: `installAgents(lane) => Promise<{installed: string[]}>` — `lane` is `{cwd, ...}` (only `cwd` is read). Throws an error with `.code = "ENOTGITREPO"` when `/.git` doesn't exist. + +- [ ] **Step 1: Write the failing tests** + +Create `server/__tests__/lane-agents.test.js`: + +```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ĩ + */ + +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"))); + + // /.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`: + +```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ĩ + */ + +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 `/.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 +bash .claude/skills/file-headers/scripts/check-headers.sh +npm run test:server +``` + +- [ ] **Step 6: Commit** + +```bash +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` — `200` with `{installed: string[]}`, `404` for an unknown lane, `400` with `{error: {code: "ENOTGITREPO", message}}` for a lane whose `cwd` isn'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): + +```js +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): + +```js +/** + * 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** + +```bash +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 +bash .claude/skills/file-headers/scripts/check-headers.sh +npm run test:server +``` + +- [ ] **Step 5: Commit** + +```bash +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 in `bin/ccam.js`). +- Produces: `ccam lanes agents install []`. + +- [ ] **Step 1: Add the subcommand function** + +In `bin/ccam.js`, add a new function near `cmdLanesProofLink` (`bin/ccam.js:2147`): + +```js +/** `ccam lanes agents install [] [--cwd path]` — write the ship-feature-lane + * agent templates (qc-local, senior-gate-reviewer) into /.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): + +```js + if (rest[0] === "proof-link") return cmdLanesProofLink(rest.slice(1)); +``` + +Add directly below it: + +```js + 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): + +```js + [ + "lanes agents install", + "[]", + "Write the ship-feature-lane agent templates (qc-local, senior-gate-reviewer) into /.claude/agents/, git-excluded", + ], +``` + +- [ ] **Step 4: Manual smoke test** + +```bash +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 +bash .claude/skills/file-headers/scripts/check-headers.sh +npm run test:server +``` + +- [ ] **Step 6: Commit** + +```bash +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)": + +```markdown +### 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 `/.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-local` agent to drive local Playwright QC against the booted lane stack. +- **7 (Senior gate)**: Requires the `senior-gate-reviewer` agent 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](../.claude/skills/ship-feature-lane/SKILL.md) 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](../.claude/skills/ship-feature-lane/SKILL.md) 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 +``` + +```bash +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)" +```