From b046516e03b96c5f22d0dca093f09d041fb6af7e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 05:11:12 +0000 Subject: [PATCH] Scale the pipeline: enforced lanes, resumable runs, browser QA, backlog, CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hard enforcement (converts CLAUDE.md's deferred hardening note into mechanics): - .claude/hooks/enforce-lanes.js: PreToolUse hook denying Write/Edit outside each agent's lane, package.json/lockfiles/node_modules for everyone, and subagent edits to the enforcement layer; Bash heuristics block package managers and git commit/push for subagents. Fails open on internal errors. - .claude/hooks/check-footer.js: SubagentStop hook requiring the handoff footer. - .claude/settings.json: hook registration + package-manager deny rules. Throughput: - All four agents pinned to model: sonnet so runs don't inherit a pricier session model. - /feature Phases 4 & 6 now batch defects/findings by Area — one invocation per dev agent, parallel when both have work. - /feature Phase 0 writes docs/pipeline/NNN-.md state after every phase; new /feature-resume continues interrupted runs; SessionStart hook surfaces in-flight runs and queued backlog items. - New /backlog command + docs/backlog.md convention: feature queue that doubles as the spec-number registry for concurrent runs. Capability: - tools/browser.js: zero-dependency headless-Chromium wrapper (dom | shot). QA captures rendered evidence into docs/qa/evidence/NNN-/; the ux-designer verifies design against the screenshots, not just source. Defect template gains an Evidence field. - .github/workflows/ci.yml: node --test suite + zero-dependency audit + design-token audit on push/PR. Docs: CLAUDE.md (commands, workflow rules, artifact conventions, rewritten Enforcement note) and README (lane enforcement, multi-feature/worktree guidance, updated adoption guide and repo layout). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Vh1iYgAxqnvbBy1sPNtGPD --- .claude/agents/backend-developer.md | 4 +- .claude/agents/frontend-developer.md | 1 + .claude/agents/qa-engineer.md | 15 ++- .claude/agents/ux-designer.md | 8 +- .claude/commands/backlog.md | 38 +++++++ .claude/commands/feature-resume.md | 24 +++++ .claude/commands/feature.md | 73 ++++++++++--- .claude/hooks/check-footer.js | 73 +++++++++++++ .claude/hooks/enforce-lanes.js | 150 +++++++++++++++++++++++++++ .claude/hooks/session-start.js | 59 +++++++++++ .claude/settings.json | 46 ++++++++ .github/workflows/ci.yml | 33 ++++++ CLAUDE.md | 42 +++++++- README.md | 89 +++++++++++++--- docs/pipeline/.gitkeep | 0 docs/qa/TEMPLATE-defect.md | 1 + docs/qa/evidence/.gitkeep | 0 tools/browser.js | 99 ++++++++++++++++++ 18 files changed, 721 insertions(+), 34 deletions(-) create mode 100644 .claude/commands/backlog.md create mode 100644 .claude/commands/feature-resume.md create mode 100644 .claude/hooks/check-footer.js create mode 100644 .claude/hooks/enforce-lanes.js create mode 100644 .claude/hooks/session-start.js create mode 100644 .claude/settings.json create mode 100644 .github/workflows/ci.yml create mode 100644 docs/pipeline/.gitkeep create mode 100644 docs/qa/evidence/.gitkeep create mode 100644 tools/browser.js diff --git a/.claude/agents/backend-developer.md b/.claude/agents/backend-developer.md index b61c88c..38adb72 100644 --- a/.claude/agents/backend-developer.md +++ b/.claude/agents/backend-developer.md @@ -2,6 +2,7 @@ name: backend-developer description: Backend and infrastructure specialist. Use for API endpoints, server code, data handling, and CI/infra configuration, implementing the API contract from a UX spec in docs/specs/. Does not write UI code or tests. Also use to fix defects whose Area is backend. tools: Read, Glob, Grep, Write, Edit, Bash +model: sonnet # pinned so subagent runs don't inherit a pricier session model --- You are the backend/infrastructure developer for this project. You implement @@ -16,7 +17,8 @@ section governs everything (in this repo: Node built-in `node:http` in - NEVER edit `docs/specs/`, `docs/design-system.md`, or `docs/design-reviews/`. Ambiguity goes under OPEN QUESTIONS, not into improvised behavior. - NEVER write or edit tests (`app/test/` is QA's lane). -- Infra/CI configuration (e.g. `.github/workflows/`) IS your lane when asked. +- Infra/CI configuration (e.g. `.github/workflows/`) and process tooling + under `tools/` ARE your lane when asked. ## Mode 1 — Implement a spec diff --git a/.claude/agents/frontend-developer.md b/.claude/agents/frontend-developer.md index 8beabab..938a871 100644 --- a/.claude/agents/frontend-developer.md +++ b/.claude/agents/frontend-developer.md @@ -2,6 +2,7 @@ name: frontend-developer description: Frontend implementation specialist. Use to build or modify UI from a UX spec in docs/specs/ — requires a spec to exist first. Implements in whatever frontend stack CLAUDE.md declares. Does not design, does not write backend code, does not write tests. Also use to fix defects or design findings whose Area is frontend. tools: Read, Glob, Grep, Write, Edit, Bash +model: sonnet # pinned so subagent runs don't inherit a pricier session model --- You are the frontend developer for this project. You implement UI exactly as diff --git a/.claude/agents/qa-engineer.md b/.claude/agents/qa-engineer.md index 5ed074f..61ce01d 100644 --- a/.claude/agents/qa-engineer.md +++ b/.claude/agents/qa-engineer.md @@ -2,6 +2,7 @@ name: qa-engineer description: QA and testing specialist. Use after implementation to write test plans, author automated tests, run the suite, verify acceptance criteria from the UX spec, and file defect reports. Does not fix product code. Also use to re-verify previously filed defects after fixes. tools: Read, Glob, Grep, Write, Edit, Bash +model: sonnet # pinned; haiku is a possible experiment for mechanical Mode-2 re-verification, but extra fix-loop iterations usually eat the savings --- You are the QA engineer for this project. You verify features against the @@ -31,9 +32,19 @@ Process: verify manually (start server on a spare PORT, curl, inspect; for UI, statically check the HTML/CSS against the criteria — semantics, ARIA, token usage). -5. For each failure, file one defect: `docs/qa/defects/NNN--.md` +5. For browser-rendered criteria (states, layout, post-JS content), capture + evidence with the zero-dependency wrapper `tools/browser.js`: start the + server on a spare PORT, then + `node tools/browser.js dom http://localhost:PORT/` (post-JS DOM — assert + rendered states/classes against it) and + `node tools/browser.js shot http://localhost:PORT/ docs/qa/evidence/NNN-/.png`. + Save all evidence under `docs/qa/evidence/NNN-/` and reference the + paths in the test plan rows they verify. If the tool reports the browser + unavailable, fall back to static HTML/CSS checks and say so in the plan. +6. For each failure, file one defect: `docs/qa/defects/NNN--.md` using `docs/qa/TEMPLATE-defect.md`. Set `Area` carefully - (frontend | backend | design) — it routes the fix. + (frontend | backend | design) — it routes the fix — and point its + `Evidence` field at any screenshots/DOM dumps that show the failure. ## Mode 2 — Re-verify defects diff --git a/.claude/agents/ux-designer.md b/.claude/agents/ux-designer.md index 4630f86..e10151b 100644 --- a/.claude/agents/ux-designer.md +++ b/.claude/agents/ux-designer.md @@ -2,6 +2,7 @@ name: ux-designer description: UX/UI design specialist. Use for design specs, wireframes, design tokens, accessibility requirements, and for design verification — reviewing implemented UI against a spec after QA passes. Does NOT write application code. Invoke BEFORE any frontend work on a feature, and AFTER QA for design verification. tools: Read, Glob, Grep, Write, Edit +model: sonnet # pinned so subagent runs don't inherit a pricier session model --- You are the UX designer for this project. You own look and feel, layout, @@ -35,9 +36,14 @@ QA engineer builds the test plan from these). ## Mode 2 — Design verification (after QA passes) -Input: the spec path and the implemented files under `app/public/`. +Input: the spec path, the implemented files under `app/public/`, and — when +present — QA's rendered evidence under `docs/qa/evidence/NNN-/`. Read the implementation and compare it against the spec and the design system. +If `docs/qa/evidence/NNN-/` exists, Read the screenshots (PNGs render in +the Read tool) and DOM dumps there and verify the *actual rendered* layout, +spacing, and states against the spec — evidence beats source-reading wherever +the two could differ. Note in your report which findings are evidence-backed. Output: `docs/design-reviews/NNN-.md` following `docs/design-reviews/001-status-dashboard.md`: scope reviewed, a findings table (severity: blocker/major/minor · spec section · expected · actual · diff --git a/.claude/commands/backlog.md b/.claude/commands/backlog.md new file mode 100644 index 0000000..34bea06 --- /dev/null +++ b/.claude/commands/backlog.md @@ -0,0 +1,38 @@ +--- +description: Manage the feature backlog (add | list | next) — the queue and spec-number registry for multi-feature work +--- + +Subcommand and arguments: **$ARGUMENTS** + +You are the orchestrator managing `docs/backlog.md`. If the file does not +exist yet, create it with this structure: + +``` +# Feature backlog + +Statuses: queued → in-progress → done (or dropped). +`Spec NNN` is allocated when an item goes in-progress — this table is the +spec-number registry, which is what prevents NNN collisions when features +run concurrently (e.g. in parallel worktrees). + +| ID | Ask | Priority | Status | Spec NNN | +|---|---|---|---|---| +``` + +Subcommands: + +- **add "" [priority]** — append a row: next free ID (B1, B2, …), the + ask verbatim, priority (high | normal | low, default normal), status + `queued`, Spec NNN empty. Do not start work. +- **list** — show the table, queued items first by priority, then + in-progress, then the rest. Flag any in-progress item that has no matching + `docs/pipeline/` state file (likely interrupted — suggest + `/feature-resume`). +- **next** — take the highest-priority `queued` item, allocate the next spec + number NNN (max of: highest NNN in `docs/specs/`, highest NNN already + allocated in this table) + 1, set the row to `in-progress` with that NNN, + and run the `/feature` pipeline (`.claude/commands/feature.md`) on the ask, + passing the allocated NNN so Phase 0 uses it. On final acceptance, set the + row to `done`. + +If `$ARGUMENTS` is empty, treat it as `list`. diff --git a/.claude/commands/feature-resume.md b/.claude/commands/feature-resume.md new file mode 100644 index 0000000..6dfb163 --- /dev/null +++ b/.claude/commands/feature-resume.md @@ -0,0 +1,24 @@ +--- +description: Resume an interrupted /feature pipeline run from its recorded state +--- + +Target: **$ARGUMENTS** (a `docs/pipeline/NNN-.md` path or a feature +number/slug; if empty, pick the most recently modified state file whose +`Status` is not `complete`). + +You are the orchestrator, resuming a `/feature` run that was interrupted +(session ended, context compacted, or stopped mid-loop). + +1. Read the pipeline state file: `Status`, `Current phase`, the loop + counters, the phase log, and the open-questions log. +2. Re-ground yourself in the artifacts it references: the spec in + `docs/specs/`, any test plan, defects (check each defect's `Status` + field), design review, and evidence directory. +3. If `Status: stopped`, present the recorded open items to the product owner + (AskUserQuestion) before doing anything — a stopped run resumes only with + their direction. +4. Continue the pipeline from `Current phase` following + `.claude/commands/feature.md` exactly — same phase definitions, same + batching rules, same loop bounds (the recorded iteration counters still + count against the max-2 caps). Keep updating the state file after every + phase. diff --git a/.claude/commands/feature.md b/.claude/commands/feature.md index 0ba449f..c799c43 100644 --- a/.claude/commands/feature.md +++ b/.claude/commands/feature.md @@ -12,13 +12,48 @@ project subagents (`ux-designer`, `frontend-developer`, `backend-developer`, - After each phase, read the agent's handoff footer. If OPEN QUESTIONS is not "none", stop and put the questions to the product owner (AskUserQuestion), then re-invoke that agent with the answers. +- After each phase, update the pipeline state file (Phase 0) before starting + the next phase — it is what makes the run resumable via `/feature-resume`. - Do not skip phases, do not do an agent's work yourself, do not commit unless the product owner asks. +## Phase 0 — Pipeline state file + +Determine the next spec number NNN: if `docs/backlog.md` exists and this ask +is (or should be) tracked there, allocate NNN via the backlog (see `/backlog`); +otherwise NNN = highest in `docs/specs/` + 1. Create +`docs/pipeline/NNN-.md`: + +``` +# Pipeline state — NNN- + +Status: in-progress +Current phase: 1 — Design +QA fix-loop iteration: 0/2 +Design fix-loop iteration: 0/2 + +## Ask + + + +## Phase log + +| Phase | Agent | Artifacts | Status | When | +|---|---|---|---|---| + +## Open questions log + +(none yet) +``` + +Update `Current phase`, the loop counters, and append a phase-log row after +every phase. On stop (exhausted loop, open questions the owner must answer +offline) set `Status: stopped`; after Phase 7 acceptance set +`Status: complete`. + ## Phase 1 — Design -Determine the next spec number NNN (highest in `docs/specs/` + 1). Invoke -`ux-designer` (Mode 1) with the ask and instruct it to write +Invoke `ux-designer` (Mode 1) with the ask and instruct it to write `docs/specs/NNN-.md`. Wait, then read the spec. Proceed only when STATUS is `ready-for-dev`. @@ -35,26 +70,36 @@ run backend first and pass its contract block to the frontend afterwards. ## Phase 3 — QA Invoke `qa-engineer` (Mode 1) with the spec path and the changed-files lists -from both dev handoffs. +from both dev handoffs. Remind it to capture rendered evidence with +`tools/browser.js` into `docs/qa/evidence/NNN-/` for browser-behavior +criteria — the design verification in Phase 5 consumes those screenshots. ## Phase 4 — QA fix loop (max 2 iterations) -If VERDICT: FAIL — for each defect file, route by its `Area` field: -- `frontend` → `frontend-developer` (Mode 2) -- `backend` → `backend-developer` (Mode 2) -- `design` → `ux-designer` first (spec clarification), then the owning dev -After fixes, invoke `qa-engineer` (Mode 2) to re-verify. If still FAIL after +If VERDICT: FAIL — **batch the routing**: group all open defects by their +`Area` field, then dispatch ONE invocation per owning agent carrying ALL of +that agent's defect paths: +- `frontend` defects → one `frontend-developer` (Mode 2) invocation +- `backend` defects → one `backend-developer` (Mode 2) invocation +- `design` defects → `ux-designer` first (spec clarification), then include + the outcome in the owning dev's batch + +When both dev agents have defects, invoke them **in parallel in a single +message** — they own disjoint files. After fixes, invoke `qa-engineer` +(Mode 2) once with every defect path to re-verify. If still FAIL after 2 iterations, stop and report the open defects to the product owner. ## Phase 5 — Design verification -Once QA passes, invoke `ux-designer` (Mode 2) with the spec path to review the -implementation. It writes `docs/design-reviews/NNN-.md` and returns -APPROVED or CHANGES REQUIRED. +Once QA passes, invoke `ux-designer` (Mode 2) with the spec path and the +evidence directory `docs/qa/evidence/NNN-/` (if QA produced one). It +writes `docs/design-reviews/NNN-.md` and returns APPROVED or +CHANGES REQUIRED. ## Phase 6 — Design fix loop (max 2 iterations) -If CHANGES REQUIRED — route findings to the owning dev agent (Mode 2), then +If CHANGES REQUIRED — batch findings by owning area exactly as in Phase 4 +(one invocation per dev agent, parallel when both have work), then `qa-engineer` (Mode 2) for a regression run, then `ux-designer` (Mode 2) re-review. If still not APPROVED after 2 iterations, stop and report the open findings to the product owner. @@ -63,4 +108,6 @@ findings to the product owner. Present a summary table: phase · agent · artifacts written · status. State the final QA verdict and design verdict, list any open items, and ask the -product owner for acceptance. Do not commit unless they ask. +product owner for acceptance. On acceptance, set the state file's +`Status: complete` (and the backlog row to `done`, if one exists). Do not +commit unless they ask. diff --git a/.claude/hooks/check-footer.js b/.claude/hooks/check-footer.js new file mode 100644 index 0000000..df950cd --- /dev/null +++ b/.claude/hooks/check-footer.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node +// SubagentStop hook: mechanically enforces the handoff-footer contract from +// CLAUDE.md — every subagent reply must end with ARTIFACTS WRITTEN / STATUS / +// OPEN QUESTIONS. If the footer is missing, block the stop once so the agent +// finishes properly. +// +// Failure posture: FAIL OPEN (exit 0) on any parse/read error. +'use strict'; + +const fs = require('node:fs'); + +const REQUIRED = ['ARTIFACTS WRITTEN', 'STATUS', 'OPEN QUESTIONS']; +// Only the pipeline role agents owe a footer; utility agents are exempt. +const PIPELINE_AGENTS = new Set([ + 'ux-designer', + 'frontend-developer', + 'backend-developer', + 'qa-engineer', +]); + +function lastAssistantText(transcriptPath) { + const lines = fs.readFileSync(transcriptPath, 'utf8').split('\n'); + for (let i = lines.length - 1; i >= 0; i--) { + const line = lines[i].trim(); + if (!line) continue; + let entry; + try { + entry = JSON.parse(line); + } catch { + continue; + } + const msg = entry.message; + if (entry.type !== 'assistant' || !msg || !Array.isArray(msg.content)) continue; + const text = msg.content + .filter((b) => b.type === 'text') + .map((b) => b.text) + .join('\n'); + if (text.trim()) return text; + } + return ''; +} + +let raw = ''; +process.stdin.on('data', (c) => (raw += c)); +process.stdin.on('end', () => { + try { + const input = JSON.parse(raw); + if (input.stop_hook_active) process.exit(0); // never loop + if (input.agent_type && !PIPELINE_AGENTS.has(input.agent_type)) process.exit(0); + + const text = + input.last_assistant_message || lastAssistantText(input.transcript_path); + if (!text) process.exit(0); // nothing to judge — fail open + + const missing = REQUIRED.filter((k) => !text.includes(`${k}:`)); + if (missing.length === 0) process.exit(0); + + process.stdout.write( + JSON.stringify({ + decision: 'block', + reason: + `Your reply is missing the required handoff footer field(s): ${missing.join(', ')}. ` + + 'End your reply with the exact footer from CLAUDE.md: ' + + 'ARTIFACTS WRITTEN: / STATUS: / ' + + 'OPEN QUESTIONS: .', + }), + ); + process.exit(0); + } catch (err) { + process.stderr.write(`check-footer hook error (failing open): ${err.message}\n`); + process.exit(0); + } +}); diff --git a/.claude/hooks/enforce-lanes.js b/.claude/hooks/enforce-lanes.js new file mode 100644 index 0000000..65f6d3f --- /dev/null +++ b/.claude/hooks/enforce-lanes.js @@ -0,0 +1,150 @@ +#!/usr/bin/env node +// PreToolUse hook: mechanically enforces the per-agent path lanes defined in +// CLAUDE.md. Runs on Write | Edit | Bash for every actor; lane rules apply +// only to subagents (hook input carries `agent_type` inside a subagent and +// omits it in the main/orchestrator session). +// +// Failure posture: FAIL OPEN. If input is unparseable or this script throws, +// exit 0 with a stderr warning — a buggy hook must degrade to prompt-level +// discipline, never brick the pipeline. +// +// Known soft spot (documented in CLAUDE.md): Bash is policed by command +// heuristics only; the hard-enforced write channels are Write/Edit. +'use strict'; + +const path = require('node:path'); + +// Lane table — the single mechanical source of truth for who may write where. +// Paths are repo-relative; a trailing '/' means "anything under this dir". +// New agents MUST be registered here: unknown agents fail closed for writes. +const LANES = { + 'ux-designer': { + allow: ['docs/specs/', 'docs/design-system.md', 'docs/design-reviews/'], + hint: 'Findings about code are reported in your review, not fixed.', + }, + 'frontend-developer': { + allow: ['app/public/'], + hint: 'Backend files, specs, and tests are other lanes — raise an OPEN QUESTION if you are blocked.', + }, + 'backend-developer': { + allow: ['app/', '.github/', 'tools/'], + exclude: ['app/public/', 'app/test/'], + hint: 'UI files and tests are other lanes — raise an OPEN QUESTION if you are blocked.', + }, + 'qa-engineer': { + allow: ['app/test/', 'docs/qa/'], + hint: 'A failing behavior becomes a defect report in docs/qa/defects/ — never a code fix.', + }, +}; + +// No actor may write these (the repo is zero-dependency by design). +const ZERO_DEP_BASENAMES = new Set(['package.json', 'package-lock.json']); + +// Subagents may not touch the enforcement layer itself. +const SUBAGENT_PROTECTED = ['.claude/settings.json', '.claude/hooks/']; + +// Bash heuristics, subagents only. +const BASH_DENY = [ + { + re: /(^|[\s;&|(])(npm|npx|pnpm|yarn|bun|pip3?)(\s|$)/, + why: 'package managers are banned — this repo is zero-dependency by design', + }, + { + re: /\bgit\s+(commit|push)\b/, + why: 'committing and pushing are the orchestrator/product owner\'s call, never a subagent\'s', + }, +]; + +function deny(reason) { + process.stdout.write( + JSON.stringify({ + hookSpecificOutput: { + hookEventName: 'PreToolUse', + permissionDecision: 'deny', + permissionDecisionReason: reason, + }, + }), + ); + process.exit(0); +} + +function matches(rel, prefixes) { + return prefixes.some((p) => (p.endsWith('/') ? rel.startsWith(p) : rel === p)); +} + +function run(raw) { + const input = JSON.parse(raw); + const tool = input.tool_name; + const args = input.tool_input || {}; + const agent = input.agent_type; // undefined in the main session + const cwd = input.cwd || process.cwd(); + const root = process.env.CLAUDE_PROJECT_DIR || cwd; + + if (tool === 'Bash') { + if (!agent) return; // orchestrator Bash is not policed here + const cmd = String(args.command || ''); + for (const { re, why } of BASH_DENY) { + if (re.test(cmd)) { + deny( + `Blocked for ${agent}: ${why}. Do not retry — raise it under OPEN QUESTIONS if you believe it is required.`, + ); + } + } + return; + } + + if (tool !== 'Write' && tool !== 'Edit') return; + if (!args.file_path) return; + + const abs = path.resolve(cwd, args.file_path); + const rel = path.relative(root, abs); + const outsideRepo = rel.startsWith('..') || path.isAbsolute(rel); + + // Lane rules protect repo artifacts; writes outside the repo (scratchpads, + // temp files) are out of scope. + if (outsideRepo) return; + + // Global invariants — apply to every actor, orchestrator included. + if (ZERO_DEP_BASENAMES.has(path.basename(rel)) || rel.split(path.sep).includes('node_modules')) { + deny( + 'Blocked: this repo is zero-dependency by design (CLAUDE.md Tech stack). ' + + 'No package.json, lockfiles, or node_modules may be created.', + ); + } + + if (!agent) return; // orchestrator may write anywhere else in the repo + + if (matches(rel, SUBAGENT_PROTECTED)) { + deny( + `Blocked for ${agent}: the enforcement layer (.claude/settings.json, .claude/hooks/) ` + + 'is maintained by the product owner only.', + ); + } + + const lane = LANES[agent]; + if (!lane) { + deny( + `Blocked: agent "${agent}" has no registered lane in .claude/hooks/enforce-lanes.js. ` + + 'Writes fail closed until the product owner adds it to the lane table.', + ); + } + + if (lane.exclude && matches(rel, lane.exclude)) { + deny(`Lane violation: ${agent} may not modify ${rel}. ${lane.hint} Do not retry.`); + } + if (!matches(rel, lane.allow)) { + deny(`Lane violation: ${agent} may not modify ${rel}. ${lane.hint} Do not retry.`); + } +} + +let raw = ''; +process.stdin.on('data', (c) => (raw += c)); +process.stdin.on('end', () => { + try { + run(raw); + process.exit(0); + } catch (err) { + process.stderr.write(`enforce-lanes hook error (failing open): ${err.message}\n`); + process.exit(0); + } +}); diff --git a/.claude/hooks/session-start.js b/.claude/hooks/session-start.js new file mode 100644 index 0000000..e1afa11 --- /dev/null +++ b/.claude/hooks/session-start.js @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// SessionStart hook: surfaces in-flight pipeline state and queued backlog +// items so a fresh or resumed session immediately knows where work stands. +// Prints nothing (and exits 0) when there is nothing pending. +// +// Failure posture: FAIL OPEN — errors never block session start. +'use strict'; + +const fs = require('node:fs'); +const path = require('node:path'); + +function firstMatch(text, re) { + const m = text.match(re); + return m ? m[1].trim() : null; +} + +try { + const root = process.env.CLAUDE_PROJECT_DIR || process.cwd(); + const out = []; + + // Pipeline state files: docs/pipeline/NNN-.md with a "Status:" line. + // Anything not `complete` is worth surfacing (in-progress AND stopped runs + // both carry open items for the product owner). + const pipelineDir = path.join(root, 'docs', 'pipeline'); + if (fs.existsSync(pipelineDir)) { + for (const f of fs.readdirSync(pipelineDir).sort()) { + if (!f.endsWith('.md')) continue; + const text = fs.readFileSync(path.join(pipelineDir, f), 'utf8'); + const status = firstMatch(text, /^Status:\s*(.+)$/m); + if (!status || status === 'complete') continue; + const phase = firstMatch(text, /^Current phase:\s*(.+)$/m) || 'unknown phase'; + out.push(`- docs/pipeline/${f}: ${status} — ${phase}`); + } + } + + // Backlog: table rows in docs/backlog.md whose status is queued/in-progress. + const backlogPath = path.join(root, 'docs', 'backlog.md'); + if (fs.existsSync(backlogPath)) { + const rows = fs + .readFileSync(backlogPath, 'utf8') + .split('\n') + .filter((l) => /^\|/.test(l) && /\b(queued|in-progress)\b/.test(l)); + if (rows.length) { + out.push(`- docs/backlog.md: ${rows.length} item(s) queued or in progress`); + } + } + + if (out.length) { + process.stdout.write( + 'Agentic pipeline state (from docs/pipeline/ and docs/backlog.md):\n' + + out.join('\n') + + '\nUse /feature-resume to continue an in-progress run, or /backlog to manage the queue.\n', + ); + } + process.exit(0); +} catch (err) { + process.stderr.write(`session-start hook error (ignored): ${err.message}\n`); + process.exit(0); +} diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4bf0e14 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,46 @@ +{ + "permissions": { + "deny": [ + "Bash(npm *)", + "Bash(npx *)", + "Bash(yarn *)", + "Bash(pnpm *)", + "Bash(bun *)", + "Bash(pip *)", + "Bash(pip3 *)" + ] + }, + "hooks": { + "PreToolUse": [ + { + "matcher": "Write|Edit|Bash", + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/enforce-lanes.js\"" + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/check-footer.js\"" + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.js\"" + } + ] + } + ] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0f06d54 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Run test suite + run: node --test app/test/*.test.js + - name: Zero-dependency audit + run: | + if [ -e package.json ] || [ -e package-lock.json ] || [ -d node_modules ]; then + echo "::error::zero-dependency rule violated — package.json/lockfile/node_modules present" + exit 1 + fi + - name: Design-token audit + # Hex colors in styles.css may only appear where a custom property is + # being *defined*; everything else must consume tokens via var(--…). + run: | + violations=$(grep -nE '#[0-9a-fA-F]{3,8}' app/public/styles.css | grep -vE '^[0-9]+:\s*--' || true) + if [ -n "$violations" ]; then + echo "::error::hex colors outside token definitions in app/public/styles.css:" + echo "$violations" + exit 1 + fi diff --git a/CLAUDE.md b/CLAUDE.md index 8e23e8c..3090604 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -38,6 +38,10 @@ adapt to it — nothing about the stack is hardcoded in your role. - Run the app: `node app/server.js` → http://localhost:3000 - Run the tests: `node --test app/test/*.test.js` +- Capture rendered evidence (QA): `node tools/browser.js dom ` (post-JS + DOM) and `node tools/browser.js shot [WxH]` — zero-dep + headless-Chromium wrapper; exits 2 with a clear message when no browser is + installed (fall back to static checks). ## Workflow rules @@ -50,6 +54,14 @@ adapt to it — nothing about the stack is hardcoded in your role. report open items to the product owner. 4. Ambiguity is never improvised away — it goes in OPEN QUESTIONS, and the orchestrator escalates it to the product owner. +5. Fix routing is batched: defects/findings are grouped by `Area` and each + owning dev agent gets ONE invocation carrying all its items; frontend and + backend fixers run in parallel (single message) when both have work. +6. Every `/feature` run maintains a pipeline state file (see Artifact + conventions) so interrupted runs resume via `/feature-resume` instead of + restarting. +7. Agents are pinned to `model: sonnet` in their frontmatter so runs don't + inherit a pricier session model. Change deliberately, not per-run. ## Artifact conventions (the handoff contract) @@ -63,6 +75,16 @@ explicit file paths. - Defects: `docs/qa/defects/NNN--.md` per `docs/qa/TEMPLATE-defect.md`. The `Area` field (frontend | backend | design) routes the fix. - Design verification reports: `docs/design-reviews/NNN-.md`. +- QA evidence (screenshots, DOM dumps): `docs/qa/evidence/NNN-/` — + produced by the qa-engineer with `tools/browser.js`, consumed by the + ux-designer during design verification. +- Pipeline state: `docs/pipeline/NNN-.md` — created at `/feature` + Phase 0, updated after every phase (`Status: in-progress | complete | + stopped`, `Current phase`, loop counters, phase log). Read by + `/feature-resume` and surfaced at session start by a hook. +- Backlog / spec-number registry: `docs/backlog.md`, managed via `/backlog`. + When features run concurrently (e.g. parallel worktrees), spec NNNs are + allocated here, not by scanning `docs/specs/`. - Every subagent ends its reply with a structured handoff footer: ``` @@ -73,7 +95,19 @@ explicit file paths. ## Enforcement note -Tool access is restricted per agent in `.claude/agents/*.md` frontmatter (e.g. -the ux-designer has no Bash). Path-level lane boundaries are prompt-level -discipline in this baseline; harden them later with hooks or permission -`deny` rules if needed. +Lane boundaries are mechanically enforced, not just prompted: + +- Tool access is restricted per agent in `.claude/agents/*.md` frontmatter + (e.g. the ux-designer has no Bash). +- `.claude/hooks/enforce-lanes.js` (a PreToolUse hook registered in + `.claude/settings.json`) blocks `Write`/`Edit` outside each agent's lane, + blocks every actor from creating `package.json`/lockfiles/`node_modules`, + and blocks subagents from touching the enforcement layer itself. Unknown + agents fail closed — register new agents in its lane table. +- `.claude/hooks/check-footer.js` (SubagentStop) blocks a role agent from + finishing without the required handoff footer. +- Honest limitation: Bash is policed by command heuristics only (package + managers, `git commit/push`); file writes routed through shell redirection + are not caught. The hard-enforced write channels are `Write`/`Edit`, and + the hooks fail open on internal errors so a hook bug can never brick the + pipeline. diff --git a/README.md b/README.md index c9c4c70..c9f8771 100644 --- a/README.md +++ b/README.md @@ -58,8 +58,18 @@ You (product owner): /feature "add X" Open questions from any agent stop the line and come back to you. Exhausted fix loops stop the line and come back to you with the open items. -Standalone commands: `/design-review` (UX audit of the current UI) and -`/qa-verify` (standalone QA pass against a spec). +Standalone commands: `/design-review` (UX audit of the current UI), +`/qa-verify` (standalone QA pass against a spec), `/feature-resume` (continue +an interrupted run from its `docs/pipeline/` state file), and `/backlog` +(queue feature asks and allocate spec numbers). + +Every `/feature` run writes a state file to `docs/pipeline/NNN-.md` and +updates it after each phase, so interrupted runs are resumable and auditable; +a SessionStart hook surfaces in-flight runs and queued backlog items when a +session opens. QA captures rendered evidence (screenshots + post-JS DOM) with +the zero-dependency `tools/browser.js` wrapper into `docs/qa/evidence/`, and +the ux-designer verifies design against those screenshots — not just the +source. ## Quickstart @@ -74,14 +84,20 @@ Standalone commands: `/design-review` (UX audit of the current UI) and ``` CLAUDE.md project constitution: roles, stack, workflow, handoff contract .claude/agents/ the four role agents (markdown + YAML frontmatter) -.claude/commands/ /feature, /design-review, /qa-verify +.claude/commands/ /feature, /feature-resume, /backlog, /design-review, /qa-verify +.claude/hooks/ lane enforcement, footer check, session-start state surfacing +.claude/settings.json hook registration + package-manager deny rules +tools/browser.js zero-dep headless-Chromium wrapper (QA evidence) docs/design-system.md design tokens + component rules (UX-owned) docs/specs/ UX feature specs (NNN-.md) -docs/qa/ test plans, defect template, defect reports +docs/qa/ test plans, defect template, defect reports, evidence/ docs/design-reviews/ UX design-verification reports +docs/pipeline/ per-feature pipeline state files (resumability + audit) +docs/backlog.md feature queue + spec-number registry (created by /backlog) app/server.js zero-dependency Node http server (API + static) app/public/ vanilla HTML/CSS/JS frontend app/test/ node --test suite +.github/workflows/ci.yml CI: test suite + zero-dependency + design-token audits ``` ## Using this process in another codebase @@ -92,7 +108,14 @@ use Claude Code: 1. **Copy verbatim** (fully stack-agnostic): - `.claude/agents/` — the four role agents - - `.claude/commands/` — `/feature`, `/design-review`, `/qa-verify` + - `.claude/commands/` — `/feature`, `/feature-resume`, `/backlog`, + `/design-review`, `/qa-verify` + - `.claude/hooks/` + `.claude/settings.json` — mechanical lane enforcement + (if the target repo already has a `settings.json`, merge the `hooks` and + `permissions.deny` entries instead of overwriting; adjust the lane table + in `enforce-lanes.js` if the target's paths differ) + - `tools/browser.js` — QA evidence capture (degrades gracefully where no + Chromium is installed) - `docs/qa/TEMPLATE-defect.md` 2. **Copy, then edit for the target project**: - `CLAUDE.md` — rewrite the **Tech stack** section (agents build with @@ -102,7 +125,8 @@ use Claude Code: - `docs/design-system.md` — swap the placeholder tokens for the target product's brand values. 3. **Create the artifact directories**: `docs/specs/`, `docs/qa/test-plans/`, - `docs/qa/defects/`, `docs/design-reviews/`. + `docs/qa/defects/`, `docs/qa/evidence/`, `docs/design-reviews/`, + `docs/pipeline/`. 4. **Optional but recommended**: copy the three seeded `001-status-dashboard` artifacts (spec, test plan, design review) as format exemplars — the agent prompts reference them as the pattern to follow. They work without them @@ -117,7 +141,9 @@ One-liner from a checkout of this repo, run inside the target repo: ```sh SRC=/path/to/agentic-dev cp -r "$SRC/.claude" . && cp "$SRC/CLAUDE.md" . -mkdir -p docs/specs docs/qa/test-plans docs/qa/defects docs/design-reviews +mkdir -p tools docs/specs docs/qa/test-plans docs/qa/defects docs/qa/evidence \ + docs/design-reviews docs/pipeline +cp "$SRC/tools/browser.js" tools/ cp "$SRC/docs/design-system.md" docs/ cp "$SRC/docs/qa/TEMPLATE-defect.md" docs/qa/ # then edit CLAUDE.md (Tech stack + Commands) and docs/design-system.md @@ -127,14 +153,51 @@ cp "$SRC/docs/qa/TEMPLATE-defect.md" docs/qa/ overwrite: append the Roles/Workflow/Artifact sections to the existing `CLAUDE.md` and drop the agent/command files into the existing directories.) -## Hardening lane boundaries - -Tool access is already restricted per agent via frontmatter (the ux-designer -has no Bash); path-level rules (e.g. "UX writes only under docs/") are -prompt-level discipline here, upgradeable with hooks or permission deny rules. +## Lane enforcement + +Lane boundaries are enforced mechanically, on top of the prompt discipline: + +- **Tool access** is restricted per agent via frontmatter (the ux-designer + has no Bash). +- **Path lanes** are enforced by `.claude/hooks/enforce-lanes.js`, a + PreToolUse hook (registered in `.claude/settings.json`) that identifies the + active subagent from the hook input and denies `Write`/`Edit` outside its + lane — with deny messages that steer the agent back into the process (file + a defect, raise an OPEN QUESTION) instead of retrying. It also blocks + `package.json`/lockfiles/`node_modules` for everyone (the zero-dependency + rule) and protects the enforcement layer from subagent edits. Unknown + agents fail closed. +- **Handoff contract**: `.claude/hooks/check-footer.js` (SubagentStop) blocks + a role agent from finishing without the ARTIFACTS WRITTEN / STATUS / + OPEN QUESTIONS footer. +- Honest limitation: Bash is policed by heuristics only (package managers, + `git commit/push`); shell-redirection writes aren't caught. Hooks fail open + on internal errors, so a hook bug degrades to prompt discipline rather than + blocking work. + +## Running multiple features + +- Queue asks with `/backlog add ""` and start the top item with + `/backlog next`. The backlog doubles as the **spec-number registry**: NNN is + allocated when an item goes in-progress, which prevents numbering + collisions between concurrent runs. +- For truly parallel features, run one `/feature` per `git worktree` + (`git worktree add ../repo-feat-x -b feat-x`). Only do this for features + that touch disjoint files — shared files like `app/public/styles.css` and + `app/server.js` will conflict at merge time. Always allocate NNN via the + backlog first, since concurrent worktrees can't see each other's + `docs/specs/`. +- Interrupted runs resume with `/feature-resume` from their + `docs/pipeline/NNN-.md` state file; the SessionStart hook lists + in-flight runs when you open a session. ## Limitations (by design, it's a baseline) - Subagents cannot spawn subagents — the main session must orchestrate. -- Path discipline is enforced by prompts, not the platform. +- Bash file writes are heuristically policed, not fully sandboxed (see Lane + enforcement). - Fix loops are capped at 2 iterations before escalating to the product owner. +- Browser evidence is capture-only (DOM dump + screenshot). Click-driven + verification would need a CDP driver (`tools/cdp.js` over Node ≥21's + built-in WebSocket is the intended zero-dep path) — add it when a feature's + criteria actually require interaction. diff --git a/docs/pipeline/.gitkeep b/docs/pipeline/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/qa/TEMPLATE-defect.md b/docs/qa/TEMPLATE-defect.md index 9ffbadf..6ca112b 100644 --- a/docs/qa/TEMPLATE-defect.md +++ b/docs/qa/TEMPLATE-defect.md @@ -5,6 +5,7 @@ - Severity: - Area: - Spec criterion violated: +- Evidence: /, or "none"> ## Steps to reproduce diff --git a/docs/qa/evidence/.gitkeep b/docs/qa/evidence/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/browser.js b/tools/browser.js new file mode 100644 index 0000000..422c439 --- /dev/null +++ b/tools/browser.js @@ -0,0 +1,99 @@ +#!/usr/bin/env node +// Zero-dependency headless-Chromium wrapper for QA evidence capture. +// +// Usage: +// node tools/browser.js dom # post-JS DOM → stdout +// node tools/browser.js shot [WxH] # screenshot (default 1280x800) +// +// Exit codes: 0 success · 1 usage/capture error · 2 no browser found +// (on exit 2, fall back to static HTML/CSS checks and note it in the test plan). +'use strict'; + +const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +// Executes the page's JS for up to 3s of virtual time before capturing, so +// fetch-driven states (loading → success) have settled. +const BASE_FLAGS = [ + '--headless=new', + '--no-sandbox', + '--disable-gpu', + '--disable-dev-shm-usage', + '--hide-scrollbars', + '--virtual-time-budget=3000', +]; + +function findBrowser() { + const candidates = []; + if (process.env.CHROME_BIN) candidates.push(process.env.CHROME_BIN); + const pw = process.env.PLAYWRIGHT_BROWSERS_PATH; + if (pw && fs.existsSync(pw)) { + for (const entry of fs.readdirSync(pw).sort().reverse()) { + if (!entry.startsWith('chromium')) continue; + candidates.push( + path.join(pw, entry), + path.join(pw, entry, 'chrome-linux', 'chrome'), + path.join(pw, entry, 'chrome-linux', 'headless_shell'), + ); + } + } + candidates.push('chromium', 'chromium-browser', 'google-chrome', 'chrome'); + for (const c of candidates) { + try { + if (spawnSync(c, ['--version'], { stdio: 'ignore', timeout: 10000 }).status === 0) { + return c; + } + } catch { + // not runnable — try the next candidate + } + } + return null; +} + +function usage() { + process.stderr.write( + 'usage: node tools/browser.js dom \n' + + ' node tools/browser.js shot [WxH]\n', + ); + process.exit(1); +} + +const [cmd, url, out, size] = process.argv.slice(2); +if (!url || (cmd !== 'dom' && cmd !== 'shot') || (cmd === 'shot' && !out)) usage(); + +const browser = findBrowser(); +if (!browser) { + process.stderr.write( + 'browser unavailable — no Chromium/Chrome found (checked CHROME_BIN, ' + + 'PLAYWRIGHT_BROWSERS_PATH, PATH). Fall back to static checks.\n', + ); + process.exit(2); +} + +let flags; +if (cmd === 'dom') { + flags = [...BASE_FLAGS, '--dump-dom', url]; +} else { + const [w, h] = (size || '1280x800').split('x'); + fs.mkdirSync(path.dirname(path.resolve(out)), { recursive: true }); + flags = [ + ...BASE_FLAGS, + `--screenshot=${path.resolve(out)}`, + `--window-size=${w || 1280},${h || 800}`, + url, + ]; +} + +const res = spawnSync(browser, flags, { encoding: 'utf8', timeout: 30000 }); +if (res.error || res.status !== 0) { + process.stderr.write( + `capture failed (${res.error ? res.error.message : `exit ${res.status}`})\n${res.stderr || ''}`, + ); + process.exit(1); +} +if (cmd === 'dom') { + process.stdout.write(res.stdout); +} else { + process.stderr.write(`screenshot written: ${path.resolve(out)}\n`); +}