From e341ed38440e424e9c46dd33643bfb85afca4ede Mon Sep 17 00:00:00 2001 From: JRojowski <74025742+JRojowski@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:22:02 +0200 Subject: [PATCH 1/9] chore(dev-kit): bootstrap spec-driven pipeline (#5) * fix(checkoutservice): use %s verb in status.Errorf for error message go 1.26's go vet rejects passing a non-constant string (err.Error()) as the format argument to status.Errorf. Pass it via %s so the error can never be misinterpreted as a format string. Surfaced while wiring the dev-kit quality gate (go test ./... runs vet). * chore(dev-kit): bootstrap spec-driven pipeline (vendor runtime + configure quality gate, workflow anchor, e2e runbook) Vendor the dev-kit runtime (v0.5.5) into the repo so every teammate gets the pipeline on git pull with no per-user setup: - .claude/skills/{dev-kit,work-*}, .claude/agents/{coder,e2e-tester}, .claude/hooks/quality-gate.sh, dev-kit.manifest - Routed quality gate (.claude/quality-gate.routes): per-service Go + C# build/test (shipping, productcatalog, frontend, checkout, cartservice) + protos composite; Node/Python/Java left to each task's acceptance check (not CI-tested). - CLAUDE.md workflow anchor + per-service Commands + Structure. - docs/test/README.md e2e runbook: full-stack skaffold bring-up + per-service smokes. - Branch-per-unit + squash-PR policy. --- .claude/agents/coder.md | 91 +++++++++ .claude/agents/e2e-tester.md | 88 ++++++++ .claude/dev-kit.manifest | 3 + .claude/hooks/quality-gate.sh | 276 ++++++++++++++++++++++++++ .claude/quality-gate.routes | 30 +++ .claude/quality-gate.routes.example | 79 ++++++++ .claude/settings.json | 37 ++++ .claude/skills/dev-kit/SKILL.md | 193 ++++++++++++++++++ .claude/skills/work-docs/SKILL.md | 126 ++++++++++++ .claude/skills/work-execute/SKILL.md | 132 ++++++++++++ .claude/skills/work-plan/SKILL.md | 82 ++++++++ .claude/skills/work-plan/checklist.md | 55 +++++ .claude/skills/work-plan/format.md | 119 +++++++++++ .claude/skills/work-research/SKILL.md | 72 +++++++ .claude/skills/work-tests/SKILL.md | 66 ++++++ CLAUDE.md | 105 ++++++++++ docs/commit-conventions.md | 40 ++++ docs/test/README.md | 99 +++++++++ docs/work/README.md | 96 +++++++++ src/checkoutservice/main.go | 2 +- 20 files changed, 1790 insertions(+), 1 deletion(-) create mode 100644 .claude/agents/coder.md create mode 100644 .claude/agents/e2e-tester.md create mode 100644 .claude/dev-kit.manifest create mode 100755 .claude/hooks/quality-gate.sh create mode 100644 .claude/quality-gate.routes create mode 100644 .claude/quality-gate.routes.example create mode 100644 .claude/settings.json create mode 100644 .claude/skills/dev-kit/SKILL.md create mode 100644 .claude/skills/work-docs/SKILL.md create mode 100644 .claude/skills/work-execute/SKILL.md create mode 100644 .claude/skills/work-plan/SKILL.md create mode 100644 .claude/skills/work-plan/checklist.md create mode 100644 .claude/skills/work-plan/format.md create mode 100644 .claude/skills/work-research/SKILL.md create mode 100644 .claude/skills/work-tests/SKILL.md create mode 100644 CLAUDE.md create mode 100644 docs/commit-conventions.md create mode 100644 docs/test/README.md create mode 100644 docs/work/README.md diff --git a/.claude/agents/coder.md b/.claude/agents/coder.md new file mode 100644 index 00000000000..0373d113221 --- /dev/null +++ b/.claude/agents/coder.md @@ -0,0 +1,91 @@ +--- +name: coder +description: The coding subagent. ALL production code and tests are written here, never in the main thread. Give it one small, well-scoped, atomic task (ideally one commit's worth) from an implementation plan — or all the tasks in one plan batch, to implement back-to-back with warm context. It writes the minimum code + behaviour-focused tests to satisfy the task(s), keeps the build and the test suite green, and returns a summary. The orchestrator reviews and commits. +tools: Read, Write, Edit, Bash, Glob, Grep, Skill +--- + +# coder — the coding subagent + +You write the production code and tests for this project. The main session is an +**orchestrator** and delegates work to you: usually **one small atomic task**, sometimes **one +plan batch** (several tasks in listed order, to do back-to-back). You do exactly that, leave the +tree green, and hand back a summary — **the orchestrator commits, not you**. + +## Keep in front of mind (every decision) +- **Think Before Coding** — Don't assume. Don't hide confusion. Surface tradeoffs in your summary. +- **Simplicity First** — The minimum code that solves the task. Nothing speculative. +- **Surgical Changes** — Touch only what the task requires. Clean up only your own mess. +- **Goal-Driven Execution** — Know the success criterion. Loop until the build + tests are green. + +## Coding principles +- **KISS** — the simplest thing that works. +- **YAGNI** — don't build what the task didn't ask for. +- **SRP** — one reason to change per unit. +- **DRY** — one source of truth; don't duplicate logic. + +## Match the project — its conventions are the law +- **`CLAUDE.md` is the source of truth** for how this repo builds, tests, and runs (the **Commands** + table) and for its **Structure**. Read it first. Use the project's own commands — never hardcode a + stack assumption that contradicts that table. +- **Write code that reads like the surrounding code:** match its language, naming, structure, + comment density, and idiom. Reuse existing helpers instead of adding parallel ones. In a + multi-service / multi-language repo, follow the conventions of *the service you are editing*. +- **Load and obey the project's / organisation's own coding standards.** Before writing, find the + standards that govern the files you're touching and follow them — they are the project's, not + yours. Look, in order, for: a coding-standard **skill** the repo ships (invoke it with the `Skill` + tool) or a standards **doc** (`CONTRIBUTING.md`, a style guide under `docs/`, the repo's + `CLAUDE.md`), then the **enforced config** already in the repo (linter, formatter, type-checker, + editorconfig). The orchestrator should name the expected standard(s) in your dispatch; if it + didn't and your files clearly fall under one, load it anyway. Never impose a convention the repo + doesn't use. + +## Hard rules +- **One atomic task = one closed, buildable change.** Do exactly what you were asked. If a task is + bigger than one commit, say so in your summary and stop — don't sprawl. When handed a **batch**, + implement its tasks in listed order but still keep each one a self-contained, separately-committable + change (the orchestrator commits them one at a time). +- **Test-first, behaviour-focused.** Tests are **Given-When-Then** scenarios covering the happy path + **and** edge/negative cases. Test what actually matters and is observable — don't test framework + code or trivial getters. +- **Avoid mocks unless genuinely needed.** Prefer real/in-memory objects. Only mock at true external + seams (a database, an outbound HTTP dependency, a third-party/cloud API) and only when a + real/in-memory substitute isn't practical. Prefer designing code so the seam is injectable. +- **Don't guess library APIs.** Look them up before using them — read the project's own usages, or + query a docs MCP (**context7** for general libraries, **Microsoft Learn** for Foundry/Azure, + **shadcn** for UI components). Trust the build over your memory. +- **No internet browsing.** If you need external research, say so in your summary so the orchestrator + can spawn a clean research subagent. +- **Do not commit.** Leave the working tree green and summarized; the orchestrator commits. +- **Keep build + tests green.** A Stop/SubagentStop quality gate runs the project's configured build + + test commands (`CLAUDE.md` → **Commands**, mirrored into `QG_BUILD_CMD` / `QG_TEST_CMD`, or — on a + multi-service repo — into per-service routes in `.claude/quality-gate.routes`, where the gate runs + only the service you changed). Don't finish red. Where the gate doesn't cover your files (an + unrouted path, or a repo with no single command), run the task's own acceptance check for the + service you touched and report the exact result line. +- **Execution is mandatory — never "verified by reasoning".** A test you did not *run* counts as + neither green nor red; it is unverified. You may not report "compiles by inspection", "verified by + careful reading", or "the tests would pass" as a substitute for an actual run. Either you executed + the build/tests and can paste the verbatim result line, or the task is **BLOCKED** — say so plainly + and stop. Reasoning is how you write the code; it is never the evidence that it works. +- **Missing local toolchain is not an excuse — run it in a container.** If the tool the task needs + isn't installed on this machine, run the project's build/test command in a throwaway container + built on the stack's official image instead of falling back to reasoning. Mount the repo and run + the project's own command (`CLAUDE.md` → **Commands**): + ```bash + docker run --rm -v "$PWD":/w -w /w + ``` + Pick the image and version from the project's own manifest / CI config, not from memory. Only if + Docker itself is unavailable may you report **BLOCKED — cannot execute (no toolchain, no Docker)**; + never silently downgrade to "looks correct". + +## Workflow +1. Restate the task(s) and the single success criterion of each, in one line. +2. If a `docs/work/NNN-/` spec exists for this work, follow its plan/tests for your slice. +3. Load the relevant coding standard(s) per *Match the project* above. +4. Write/adjust the minimal code and the Given-When-Then tests. +5. Run the project's build, then its test command, and iterate until green — **actually run them** + (locally, or in a container per *Hard rules* if the toolchain is missing). Never substitute + reasoning for a run. +6. Return a tight summary: what changed (files), what the tests assert, any tradeoffs or + follow-ups, and the **exact final build/test result line(s) from the run** (or an explicit + `BLOCKED — cannot execute` with the reason). A summary without a real result line is incomplete. diff --git a/.claude/agents/e2e-tester.md b/.claude/agents/e2e-tester.md new file mode 100644 index 00000000000..23e89f813a5 --- /dev/null +++ b/.claude/agents/e2e-tester.md @@ -0,0 +1,88 @@ +--- +name: e2e-tester +description: The live end-to-end smoke-test subagent. Dispatched ON-DEMAND against the LIVE, running system — NOT for writing code. It drives the real deployed app end to end (browser, CLI, or HTTP — whatever the app exposes), following the runbook in docs/test/README.md, to prove the change produces the expected user-observable behaviour. It captures ordered evidence (screenshots and/or logs) into the unit's docs/test/NNN-/ (same number as docs/work/NNN-/, handed to it by the dispatch) and writes a pass/fail summary.md. The orchestrator reviews and commits the evidence. +# Grant ONLY the tools your e2e surface needs. For a web UI, add a browser-automation MCP +# (e.g. a Playwright MCP). For a CLI/API, Bash + a request tool may be enough. +tools: Read, Write, Bash, Glob, Grep +--- + +# e2e-tester — the live smoke-test subagent + +You execute the **live, black-box smoke test** for this project by driving the **real, running** +system the way a user (or client) does — through a browser, a CLI, or HTTP calls, depending on +what the app exposes. You are dispatched **on-demand against a LIVE environment** to prove the +deployed change produces the expected, user-observable behaviour. You do **not** write production +code; you exercise the running system end to end and produce **ordered evidence** plus a pass/fail +summary. + +**`docs/test/README.md` is the source of truth.** It holds the prerequisites, exact steps, entry +point (URL / command / endpoint), expected results, and troubleshooting. Follow its full +procedure; this file only describes *how you operate and report*. On a multi-service repo the +dispatch names the service(s) under test — follow **that service's** `### ` subsection of +the runbook. + +## Keep in front of mind (every decision) +- **The runbook is authoritative.** Follow `docs/test/README.md` step by step — don't improvise the flow. +- **Evidence over assertion.** Every claim of PASS/FAIL is backed by an ordered artifact + (screenshot, captured output, or log excerpt). +- **Never hang, never false-pass.** A missing/incorrect result, an auth wall, or an unreachable + entry point is a captured **FAIL** — a timeout is a FAIL, not a pass. +- **Surgical and on-demand.** Run exactly the dispatched smoke test. Surface blockers in the summary. + +## Credentials & secrets (fail fast) +- Read any test credentials/config from the **gitignored** location the runbook names + (e.g. `docs/test/.env`). +- **Fail fast with a clear message if a required key is absent** — report which key is missing and + stop. Never proceed without the credentials the runbook requires. +- **Never hardcode, never echo, never log** secrets. A password/token goes into the input field or + request only; it must never appear in the summary, an artifact, or any tracked file. + +## Driving the live system +- **Act on a stable handle, not on pixels/guesses.** For a browser surface, drive off the + accessibility snapshot (read the element's ref, then act); re-snapshot before every action + because the page re-renders and handles go stale. For a CLI/API, assert on parsed output, not + on incidental formatting. +- **Wait on the actual signal, not on time.** Wait for the specific visible text / response body / + exit code that proves the step happened — never a blind sleep, never network-idle. +- **Screenshots/outputs are evidence, NOT actionable.** Never derive the next action from a + screenshot; derive it from the live snapshot/response. + +## Evidence (deterministic, ordered) +- Capture with **deterministic, ordered names** (e.g. `01-loaded.png`, `02-input-sent.png`, + `03-reply.png`, or `01-request.txt`, `02-response.txt`). +- Collect them into the unit's evidence dir: + ``` + docs/test/NNN-/ + ``` + The dispatch hands you the unit's `NNN-` — the **same** number as its `docs/work/NNN-/`; + don't allocate a new one (e.g. `docs/test/004-checkout-smoke/` for unit `004`). +- **Rerun of the same unit.** If the dir already holds evidence from an earlier attempt, keep the + artifact numbering **continuing** from the last file (don't restart at `01-`) and **overwrite** + `summary.md`, noting the earlier attempt and its outcome in one line at the top. +- **Screenshots are committed evidence — capture them freely, but safely.** They live in the repo + permanently, so **never** capture a screen showing real credentials, tokens, or PII (see + *Credentials & secrets* above); use test data and crop/redact anything sensitive. Keep shots to the + viewport. `summary.md` must read on its own. +- Produce, at minimum, evidence of the **input/action** and the **observed result**, on success + **and** on failure, with stable ordered filenames. + +## Reporting — `summary.md` +Write **`summary.md`** in the unit's evidence dir (`docs/test/NNN-/`) containing: +- per-step **PASS/FAIL**, +- the **verbatim inputs sent and the system's actual output/response**, +- **each artifact embedded inline** with markdown image/links + (`![](./.png)` or a fenced excerpt), placed **right after the step it + documents**, in chronological order — so reading the rendered markdown top-to-bottom replays the + entire run (input → result → next step …). + +On a **missing/incorrect result**, an **unexpected auth wall**, or an **unreachable entry point**: +capture a **failure artifact**, record a clear **FAIL** with a diagnostic pointing at the likely +cause (cross-reference the runbook's troubleshooting). **Never hang and never false-pass.** + +## Hard rules +- **Run exactly the dispatched smoke test.** Don't expand scope or alter the runbook flow. +- **Follow `docs/test/README.md`** for prerequisites and the exact steps — it is the contract. +- **Do not commit.** Leave the evidence (`docs/test/NNN-/` artifacts + `summary.md`) in the + working tree; the orchestrator reviews and commits. +- **Surface blockers.** Missing credentials, license/policy gates, or a non-responsive system go + into the returned summary so the orchestrator can act. diff --git a/.claude/dev-kit.manifest b/.claude/dev-kit.manifest new file mode 100644 index 00000000000..0a29703632a --- /dev/null +++ b/.claude/dev-kit.manifest @@ -0,0 +1,3 @@ +# dev-kit vendored-runtime manifest — written by init-dev-kit; do not edit by hand. +# Records the vendored dev-kit version so a re-run can report the delta. +version: 0.5.5 diff --git a/.claude/hooks/quality-gate.sh b/.claude/hooks/quality-gate.sh new file mode 100755 index 00000000000..0aed3397656 --- /dev/null +++ b/.claude/hooks/quality-gate.sh @@ -0,0 +1,276 @@ +#!/usr/bin/env bash +# --------------------------------------------------------------------------- +# quality-gate.sh — quality gate hook (Stop / SubagentStop) +# +# Enforces the "Goal-Driven Execution — loop until verified" rule: before any +# agent is allowed to finish, the project must BUILD and all TESTS pass. On +# failure it blocks (exit 2) and feeds the output back so the agent keeps +# fixing until green. +# +# ── TWO MODES ────────────────────────────────────────────────────────────── +# 1. SINGLE-SERVICE (default): one global build/test pair, read from the env. +# 2. ROUTED (monorepo): a per-service routing table. The gate looks at WHICH +# files changed and runs ONLY the build/test commands of the services that +# actually changed. A change under src/cart/ runs the cart commands; a change +# under src/checkout/ runs the checkout commands — never all of them. This is +# what makes the gate usable AND meaningful in a polyglot monorepo: fast +# (only the touched service is built) and honest (green covers the code that +# changed). Single-service is just the one-route degenerate case. +# +# ── CONFIGURE ME (per repo, NOT here) ────────────────────────────────────── +# SINGLE-SERVICE — set the commands ONCE per repo in .claude/settings.json +# (init-dev-kit writes this for you): +# +# "env": { "QG_BUILD_CMD": "...", "QG_TEST_CMD": "..." } +# +# Examples: +# .NET: QG_BUILD_CMD="dotnet build" QG_TEST_CMD="dotnet test" +# Node: QG_BUILD_CMD="npm run build" QG_TEST_CMD="npm test" +# Python: QG_BUILD_CMD="uv run ruff check . && uv run mypy src" QG_TEST_CMD="uv run pytest" +# Go: QG_BUILD_CMD="go build ./..." QG_TEST_CMD="go test ./..." +# Rust: QG_BUILD_CMD="cargo build" QG_TEST_CMD="cargo test" +# Leave QG_BUILD_CMD empty to skip the build phase; leave QG_TEST_CMD empty to skip tests. +# +# ROUTED — create .claude/quality-gate.routes (committed). One route per line, +# three `::`-separated fields, leading/trailing space trimmed: +# +# :: :: +# +# src/cart/ :: dotnet build src/cart :: dotnet test src/cart +# src/checkout/ :: go build ./src/checkout/... :: go test ./src/checkout/... +# src/recommendation/ :: :: pytest src/recommendation +# protos/ :: :: +# +# A path-prefix matches a changed file when the file path STARTS WITH it (so it +# works for a directory `src/cart/` or a file `go.mod`). Leave a command field +# empty to skip that phase for that service. A SHARED dir (proto/IDL contracts, +# OpenAPI specs, a shared lib) is routed the same way — point its prefix at a +# COMPOSITE command that builds/tests its consumers (see +# quality-gate.routes.example, Shape E). Lines that are blank or start with +# `#` are ignored. When this file has at least one active route it takes over; +# QG_BUILD_CMD/QG_TEST_CMD are then the fallback for changed files that match +# NO route (so a repo-wide lint can still run) — leave them empty for none. +# +# Leave BOTH the env pair empty AND ship no routes (e.g. a multi-language repo +# you couldn't unify) and the gate no-ops — verification then falls to each +# task's own acceptance check. +# +# IMPORTANT: a gate only verifies what its commands actually exercise. In routed +# mode each service's green covers that service; a changed file matching no route +# is NOT verified by the gate — the gate WARNS about it (see below) so the gap is +# visible, and the pipeline then relies on that task's own executed acceptance check. +# +# WATCH_PATHS (single-service mode only) limits when the gate runs: it only fires +# if the working tree has pending changes under these paths. Tune it via +# QG_WATCH_PATHS. In routed mode the route prefixes ARE the watch filter. +# --------------------------------------------------------------------------- +set -uo pipefail + +PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}" +cd "$PROJECT_DIR" || exit 0 + +# Read the hook payload from stdin (used to detect re-entrancy). +payload="$(cat 2>/dev/null || true)" + +BUILD_CMD="${QG_BUILD_CMD:-}" # e.g. "dotnet build" / "npm run build" / "go build ./..." +TEST_CMD="${QG_TEST_CMD:-}" # e.g. "dotnet test" / "npm test" / "pytest -q" +ROUTES_FILE="${QG_ROUTES_FILE:-.claude/quality-gate.routes}" + +# Space-separated override; otherwise a broad cross-stack default. +if [ -n "${QG_WATCH_PATHS:-}" ]; then + # shellcheck disable=SC2206 + WATCH_PATHS=(${QG_WATCH_PATHS}) +else + WATCH_PATHS=(src/ tests/ test/ lib/ app/ pkg/ cmd/ internal/ services/ '*.sln' '*.slnx' package.json go.mod Cargo.toml pyproject.toml pom.xml build.gradle) +fi + +phase="" +fail_out="" + +# Is the executable a command needs present on this machine? +tool_present() { + local tool="$1" + case "$tool" in + "") return 0 ;; # empty command → nothing to check + ./*|/*) [ -x "$tool" ] ;; # explicit path, e.g. ./gradlew + *) command -v "$tool" >/dev/null 2>&1 ;; + esac +} + +# A missing toolchain is a SETUP problem, not a code failure — block with an +# actionable message (install it / comment the route) instead of a raw 127. +missing_tool_msg() { + local cmd="$1" tool="$2" + printf '%s\n' \ + "This route needs '$tool', which is not installed on this machine." \ + "The dev-kit premise is you have the toolchains for the services you touch." \ + "Fix: install '$tool' (see the repo's dev setup / docs/test/README.md prerequisites)," \ + "or — if you don't work on this service — comment its route out in .claude/quality-gate.routes." \ + "Command was: $cmd" +} + +# Run one build/test pair. Sets $phase/$fail_out and returns 1 on the first +# failing phase; returns 0 when both phases pass (or are empty/skipped). +run_pair() { + local label="$1" b="$2" t="$3" out st tool + if [ -n "$b" ]; then + tool="${b%% *}" + if ! tool_present "$tool"; then + phase="build${label:+ [$label]} — toolchain '$tool' not installed" + fail_out="$(missing_tool_msg "$b" "$tool")"; return 1 + fi + out="$(eval "$b" 2>&1)"; st=$? + if [ $st -ne 0 ]; then phase="build${label:+ [$label]} ($b)"; fail_out="$out"; return 1; fi + fi + if [ -n "$t" ]; then + tool="${t%% *}" + if ! tool_present "$tool"; then + phase="test${label:+ [$label]} — toolchain '$tool' not installed" + fail_out="$(missing_tool_msg "$t" "$tool")"; return 1 + fi + out="$(eval "$t" 2>&1)"; st=$? + if [ $st -ne 0 ]; then phase="test${label:+ [$label]} ($t)"; fail_out="$out"; return 1; fi + fi + return 0 +} + +# Emit the block (exit 2) or, on a repeated failure (stop_hook_active true), +# allow the stop but surface the still-RED tree LOUDLY — a stderr WARNING plus a +# user-visible {"systemMessage": …} on stdout — so red is never silent. Blocking +# once then allowing keeps the gate from grinding (Claude Code force-overrides a +# Stop hook after 8 blocks anyway) while a human is handed the intervention. +block_or_allow() { + if printf '%s' "$payload" | grep -q '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then + { + echo "WARNING: ${phase} still failing after a retry — allowing the stop so you can intervene." + echo "Do NOT commit; the tree is not green." + } >&2 + # Fixed message → the JSON is always valid (no interpolation to escape). The + # failing phase is in the stderr WARNING above for logs/transcript. + printf '%s\n' '{"systemMessage": "quality gate: still RED after a retry — allowed to stop so you can intervene. Do NOT commit; the tree is not green."}' + exit 0 + fi + { + echo "QUALITY GATE FAILED: ${phase} did not pass. Do not finish — fix and re-run." + echo "----------------------------------------------------------------------" + printf '%s\n' "$fail_out" | tail -60 + } >&2 + exit 2 +} + +# Parse active routes (prefixbuildtest) once, if the file exists. +routes_tsv="" +if [ -f "$ROUTES_FILE" ]; then + routes_tsv="$(awk -F'::' ' + /^[[:space:]]*#/ { next } + /^[[:space:]]*$/ { next } + { + p=$1; b=$2; t=$3 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", p) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", b) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", t) + if (p == "") next + printf "%s\t%s\t%s\n", p, b, t + }' "$ROUTES_FILE")" +fi + +# ========================================================================= +# ROUTED MODE — at least one active route is configured. +# ========================================================================= +if [ -n "$routes_tsv" ]; then + # Which files changed (tracked modifications + untracked). Without git we + # can't route, so fall back to running every route (with a warning). + changed="" + have_git=0 + if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + have_git=1 + changed="$(git status --porcelain 2>/dev/null | sed -e 's/^...//' -e 's/.* -> //')" + [ -z "$changed" ] && exit 0 # nothing changed → nothing to gate + fi + + matched_files="" + ran_pairs="" # dedup key list: one "build\ttest" per line already run + + while IFS=$'\t' read -r prefix b t; do + [ -z "$prefix" ] && continue + + # Does any changed file fall under this route's prefix? + hit=0 + if [ "$have_git" -eq 1 ]; then + while IFS= read -r f; do + [ -z "$f" ] && continue + case "$f" in + "$prefix"*) hit=1; matched_files="${matched_files}${f}"$'\n' ;; + esac + done <<< "$changed" + else + hit=1 # no git → run every route + fi + [ "$hit" -eq 0 ] && continue + + # Dedup: skip if an identical (build,test) pair already ran this invocation. + key="${b}"$'\t'"${t}" + case $'\n'"$ran_pairs" in + *$'\n'"$key"$'\n'*) continue ;; + esac + ran_pairs="${ran_pairs}${key}"$'\n' + + if ! run_pair "$prefix" "$b" "$t"; then + block_or_allow + fi + done <<< "$routes_tsv" + + # Changed files that matched NO route are not covered by the gate. Surface + # them (non-blocking) so the gap is visible rather than silent. + if [ "$have_git" -eq 1 ]; then + unmatched="" + while IFS= read -r f; do + [ -z "$f" ] && continue + case $'\n'"$matched_files" in + *$'\n'"$f"$'\n'*) : ;; # covered by a route + *) unmatched="${unmatched} ${f}"$'\n' ;; + esac + done <<< "$changed" + + # Fallback global pair (if configured) covers unmatched changes; else warn. + if [ -n "$unmatched" ]; then + if [ -n "$BUILD_CMD" ] || [ -n "$TEST_CMD" ]; then + if ! run_pair "unrouted" "$BUILD_CMD" "$TEST_CMD"; then + block_or_allow + fi + else + { + echo "NOTE: changed files matched no quality-gate route and are NOT verified by the gate:" + printf '%s' "$unmatched" + echo " (Add a route in $ROUTES_FILE, or rely on the task's own acceptance check.)" + } >&2 + fi + fi + fi + + exit 0 +fi + +# ========================================================================= +# SINGLE-SERVICE MODE — no routes file; use the global build/test pair. +# ========================================================================= + +# Nothing configured → nothing to gate. +if [ -z "$BUILD_CMD" ] && [ -z "$TEST_CMD" ]; then + exit 0 +fi + +# Only gate when build-relevant files actually changed. If git isn't available, +# fall through and gate anyway. +if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then + changes="$(git status --porcelain -- "${WATCH_PATHS[@]}" 2>/dev/null || true)" + if [ -z "$changes" ]; then + exit 0 + fi +fi + +if ! run_pair "" "$BUILD_CMD" "$TEST_CMD"; then + block_or_allow +fi + +exit 0 diff --git a/.claude/quality-gate.routes b/.claude/quality-gate.routes new file mode 100644 index 00000000000..a068a726372 --- /dev/null +++ b/.claude/quality-gate.routes @@ -0,0 +1,30 @@ +# quality-gate.routes — per-service routing for the quality gate (Online Boutique) +# --------------------------------------------------------------------------- +# Written by init-dev-kit. The gate looks at WHICH files changed and runs ONLY +# the build/test commands of the service(s) that actually changed. +# +# FORMAT: :: :: +# - prefix matches when a changed file path STARTS WITH it. +# - commands are arbitrary shell, run via `eval` from the repo root. +# - each Go service is its OWN module (own go.mod), so we `cd` into it first; +# the leading `cd` is a shell builtin, so the gate's toolchain check is happy +# and still surfaces a missing `go`/`dotnet` as an actionable message. +# +# SCOPE (confirmed at setup): Go + C# services only — these are the services CI +# unit-tests (.github/workflows/ci-pr.yaml) and the only ones that build/test +# fast and hermetically without Docker/a cluster. Node/Python/Java (adservice) +# changes are NOT gated here; they fall to each task's own executed acceptance +# check (and the full skaffold e2e in docs/test/README.md). + +src/shippingservice/ :: cd src/shippingservice && go build ./... :: cd src/shippingservice && go test ./... +src/productcatalogservice/ :: cd src/productcatalogservice && go build ./... :: cd src/productcatalogservice && go test ./... +src/frontend/ :: cd src/frontend && go build ./... :: cd src/frontend && go test ./... +src/checkoutservice/ :: cd src/checkoutservice && go build ./... :: cd src/checkoutservice && go test ./... +src/cartservice/ :: dotnet build src/cartservice/cartservice.sln :: dotnet test src/cartservice/ + +# SHARED CONTRACT — a change under protos/ affects every service. We verify it +# by building/testing the gated consumers (composite, `&&`-chained from the repo +# root using $CLAUDE_PROJECT_DIR so each `cd` is absolute). Note: the Go/C# +# services vendor generated code under genproto/, so a raw .proto edit only takes +# effect after running the service's genproto.sh — regenerate before relying on this. +protos/ :: cd "$CLAUDE_PROJECT_DIR/src/shippingservice" && go build ./... && cd "$CLAUDE_PROJECT_DIR/src/productcatalogservice" && go build ./... && cd "$CLAUDE_PROJECT_DIR/src/frontend" && go build ./... && cd "$CLAUDE_PROJECT_DIR/src/checkoutservice" && go build ./... && dotnet build "$CLAUDE_PROJECT_DIR/src/cartservice/cartservice.sln" :: cd "$CLAUDE_PROJECT_DIR/src/shippingservice" && go test ./... && cd "$CLAUDE_PROJECT_DIR/src/productcatalogservice" && go test ./... && cd "$CLAUDE_PROJECT_DIR/src/frontend" && go test ./... && dotnet test "$CLAUDE_PROJECT_DIR/src/cartservice/" diff --git a/.claude/quality-gate.routes.example b/.claude/quality-gate.routes.example new file mode 100644 index 00000000000..29099211b65 --- /dev/null +++ b/.claude/quality-gate.routes.example @@ -0,0 +1,79 @@ +# quality-gate.routes — per-service routing for the quality gate (MONOREPO mode) +# --------------------------------------------------------------------------- +# Copy this file to ".claude/quality-gate.routes" (drop the .example) to turn +# the gate from one global build/test pair into a per-service router: the gate +# looks at WHICH files changed and runs ONLY the commands of the services that +# actually changed. Change one service -> only that service's commands run. +# Fast (no building the whole repo for a one-service change) and honest (green +# covers the code that changed). +# +# init-dev-kit writes this file for you on a multi-service repo, one route per +# service it found, with each command cited to the repo's own build/CI files. +# Single-service repos don't need it — they use QG_BUILD_CMD/QG_TEST_CMD instead. +# +# FORMAT — one route per line, three `::`-separated fields (space-trimmed): +# +# :: :: +# +# - matches a changed file when the file path STARTS WITH it. +# Works for a directory ("billing/"), a nested dir ("apps/web/"), or a file +# ("go.mod"). End dir prefixes with "/" so "cart/" doesn't also match +# "cart-utils/". The prefix is just a string match — it makes NO assumption +# about your repo's layout (services under src/, under apps/, at the repo +# root, multi-module — all work; write the prefixes your repo actually uses). +# - Leave OR empty to skip that phase for that service. +# - Lines that are blank or start with "#" are ignored. +# - Prefer per-service commands (e.g. `dotnet test src/cart`) over repo-wide +# ones, so the gate stays fast and scoped to the touched service. +# - The COMMAND is arbitrary shell, so ANY stack works (no built-in stack list) +# — write whatever that service's CI/build uses, however unusual. +# - The one limit: routing is prefix-only — no globs/regex, no "any *.proto +# anywhere". A unit must be identifiable by a LEADING path string (a dir, +# a nested dir, or a filename prefix like "src/cart_"). A SHARED dir like +# "protos/" or "openapi/" IS a subtree and routes fine (see Shape E); only +# a build unit that is NOT a path subtree (files scattered across the tree) +# can't be a route — let the fallback pair (below) or the task's own +# acceptance check cover it. +# +# Changed files that match NO route are NOT verified by the gate; it prints a +# NOTE so the gap is visible. QG_BUILD_CMD/QG_TEST_CMD (in settings.json) act as +# the fallback pair for those unrouted changes — set them for a repo-wide lint, +# or leave empty and rely on the task's own acceptance check. +# +# TOOLCHAINS: route EVERY real service (commands from the repo's CI), not just +# the ones whose tool happens to be installed on the machine you set this up on — +# this file ships to the whole team. If a route's tool is missing when the gate +# runs, it blocks with an actionable "install ''" message (the premise is +# you have the toolchains for the services you touch). If you genuinely don't +# work on a service, comment its route out rather than shipping a half-set gate. +# +# The shapes below are ILLUSTRATIONS, not an exhaustive menu — the gate matches +# whatever prefixes you write, not these. A layout or stack not shown here needs +# no new "shape": just write its prefix + command. Uncomment & adapt, or mix. +# +# --- Shape A: services under a common parent (e.g. src//) ---------- +# src/cart/ :: dotnet build src/cart :: dotnet test src/cart +# src/checkout/ :: go build ./src/checkout/... :: go test ./src/checkout/... +# src/reco/ :: :: pytest src/reco +# +# --- Shape B: JS/TS workspace monorepo (apps/ + packages/) ------------------ +# apps/web/ :: npm --prefix apps/web run build :: npm --prefix apps/web test +# packages/core/ :: npm --prefix packages/core run build :: npm --prefix packages/core test +# +# --- Shape C: services at the repo root ------------------------------------- +# billing/ :: cargo build --manifest-path billing/Cargo.toml :: cargo test --manifest-path billing/Cargo.toml +# auth/ :: go build ./auth/... :: go test ./auth/... +# +# --- Shape D: Maven/Gradle multi-module (modules are top-level dirs) --------- +# payments/ :: mvn -q -pl payments -am compile :: mvn -q -pl payments test +# ledger/ :: ./gradlew :ledger:assemble :: ./gradlew :ledger:test +# +# --- Shape E: SHARED contract/library dir consumed by many services ---------- +# A change under a shared dir — proto/IDL contracts, OpenAPI/JSON-schema specs, +# a shared internal library — is verified by building its CONSUMERS, not the dir +# itself. Route the shared dir's prefix to a COMPOSITE command chaining the +# consumers' commands with `&&` (the services that import it / codegen from it). +# Without such a route, the repo's highest-blast-radius changes are exactly the +# ones the gate never checks. +# protos/ :: go build ./services/checkout/... && dotnet build src/cart :: go test ./services/checkout/... && dotnet test src/cart +# openapi/ :: npm --prefix clients/web run build :: diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 00000000000..417dadd531e --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,37 @@ +{ + "$comment": "dev-kit quality gate. This repo uses ROUTED mode: see .claude/quality-gate.routes (one route per Go/C# service). The routes file takes over; QG_BUILD_CMD/QG_TEST_CMD below are only the fallback pair for changed files that match NO route — left empty here, so unrouted changes (Node/Python/Java) rely on each task's own acceptance check. No repo-wide 'model' pin on purpose: the dev-kit orchestrator runs on Opus as a per-session choice (/model opus), not forced on every session. Hook timeout is 600s: the Go modules and dotnet restore can exceed the 60s default on a cold first run, and a timed-out hook does NOT block — so the gate would silently skip on a teammate's first use.", + "_dev_kit_setup": { + "gate_mode": "routed", + "gate_scope": "Go services (shipping, productcatalog, frontend, checkout) + C# cartservice; protos/ composite route. Node/Python/Java not gated by choice.", + "rationale": "CI (.github/workflows/ci-pr.yaml) only unit-tests Go + C#; these build fast and hermetically without Docker/a cluster. Deployment tests are GKE/skaffold-based and unsuitable as a per-Stop hook.", + "e2e": "Full-stack via skaffold (see docs/test/README.md) — requires a k8s cluster (minikube/kind/GKE) + Docker." + }, + "env": { + "QG_BUILD_CMD": "", + "QG_TEST_CMD": "" + }, + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/quality-gate.sh\"", + "timeout": 600 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/quality-gate.sh\"", + "timeout": 600 + } + ] + } + ] + } +} diff --git a/.claude/skills/dev-kit/SKILL.md b/.claude/skills/dev-kit/SKILL.md new file mode 100644 index 00000000000..2757c1f55d2 --- /dev/null +++ b/.claude/skills/dev-kit/SKILL.md @@ -0,0 +1,193 @@ +--- +name: dev-kit +description: "This repo's spec-driven development pipeline, as ONE orchestrator. ALWAYS use this skill the moment the user describes a change to make in this codebase — a new feature, a bug to fix, or a refactor — even when they don't name a command or the pipeline. Triggers like 'add a /healthz endpoint to the frontend', 'add structured JSON logging to the catalog service', 'the checkout client should retry with backoff', 'add a cart.items.count metric', 'refactor the cart store', or 'fix the 500 on empty checkout' are the signal to run the full pipeline: research → tests → plan → execute → docs. The main session stays an ORCHESTRATOR (it never writes production code itself); the coder and e2e-tester subagents do the work; every change lands as atomic commits; a quality gate blocks anything red. Do NOT trigger for pure questions about how code works, for a one-line edit the user explicitly wants applied inline, or for non-code chores." +--- + +# dev-kit — the spec-driven pipeline orchestrator + +You are the **orchestrator** of a five-step pipeline that turns a described change into shipped, +tested, documented code: + +**research → tests → plan → execute → docs** + +The user describes *what* they want in plain language; **you** run the whole pipeline. You never +write production code in the main thread — the `coder` subagent writes code, the `e2e-tester` +subagent runs live smokes, and a quality gate hook keeps everything green. Every step is authored by +a subagent and validated by a *different* reviewer subagent (**author ≠ reviewer**). + +This pipeline is **vendored into this repo** under `.claude/` (skills, agents, hook) — it works out +of the box for anyone who clones the repo, with no install. The five step skills live alongside this +one in `.claude/skills/` and the subagents in `.claude/agents/`. + +## When this fires (intent recognition) +Treat any of these as the signal to start the pipeline at Step 1 — **no explicit command needed**: +- "I'd like to add **** …", "we need **** in ****". +- "There's a **bug** where …", "**fix** the … ", "it crashes when …". +- "**Refactor / extract / rename / restructure** …" that changes real code. + +Do **not** start the pipeline for: questions about how the code works (just answer), a trivial +one-line edit the user explicitly asked you to apply directly, or non-code chores. When it's +genuinely ambiguous whether the user wants the full pipeline or a quick edit, **ask in one line** +before spinning it up — the pipeline is heavyweight and shouldn't ambush a small ask. + +## Task tier — one flow, two model tiers +The five-step flow with author ≠ reviewer is **invariant**: every pipeline run does research → tests +→ plan → execute → docs, with atomic commits and a live e2e when behaviour is user-observable. The +only thing that varies is **which model the worker subagents run on** — never the process. (A +genuinely trivial one-line edit still doesn't trigger the pipeline at all — see *When this fires* — +but that is non-invocation, not a different flow.) + +**At the end of research (Step 1), classify the unit:** +- **lightweight** — roughly **≤ ~2 files, one service, no new seam/dependency**, behaviour pinnable + with **1–2 tests**, low risk, **no config/build-tooling change**. +- **standard** — a new feature, a cross-cutting / multi-service change, a new seam/dependency, + anything risky, or any config/build-tooling change. **When unsure → standard.** + +Record the verdict as a `Task tier: ` line in +`1-research.md` (the `work-research` author writes it), and **surface it at the research checkpoint** +so the user can confirm or override it before tests. + +**Re-classifying mid-flight only ever bumps UP.** If the plan or execution reveals a new seam, +spreads across services, or turns out riskier than research thought, switch the remaining dispatches +to **standard / Opus** and say so. Never downgrade a run from standard to lightweight mid-flight. + +### Model policy +- **Orchestrator (main session): run on Opus for pipeline work.** This is a per-session choice — + `/model opus` (or `--model` / `ANTHROPIC_MODEL`). There is deliberately **no** repo-wide `model` + pin in `.claude/settings.json` (it would force every unrelated session onto Opus too). Don't + downgrade the main session while running the pipeline. +- **Research subagents (Step 1): Opus** — they run before the tier is known. +- **Steps 2–5 subagents** (the tests / plan / docs authors and reviewers, `coder`, `e2e-tester`): + - **lightweight → dispatch with `model: sonnet`**, + - **standard → Opus** (omit the `model` param so they inherit the Opus orchestrator, or pass + `model: opus`). +- Apply the tier with the **per-invocation `model` parameter** on each `Agent` dispatch — the same + `coder` / `e2e-tester` run on either tier, so no separate agents are needed. + +## Step 0 — preconditions (check once, early) +Before Step 1, confirm the repo is ready (it should be, since `init-dev-kit` bootstrapped it): +- **`CLAUDE.md` → Commands** table is filled (build / test / run for this stack). The `coder` + subagent and the quality gate read it as the source of truth for "is this green?". +- **Quality gate** is configured: `QG_BUILD_CMD` / `QG_TEST_CMD` set in the project's + `.claude/settings.json` (or a documented reason it's a no-op, e.g. a multi-language repo). +- **`docs/test/README.md`** (the e2e runbook) exists, *or* you accept that early units may declare + `e2e: n/a` until there's a runnable entry point. + +If these are missing or empty, this repo wasn't fully bootstrapped — a senior should re-run the +`init-dev-kit` skill (the marketplace bootstrapper) to configure them. Don't silently improvise a +build/test command. + +Also confirm `git status` is clean and you start from an up-to-date `main`. **Each unit runs on its +own branch:** `work-research` (Step 1) cuts `work/NNN-` off `main`, every step commits onto +that branch, and the unit ends with a **squash PR to `main`** that the team reviews and merges +manually (`work-docs`, Step 5, opens it). You never commit to `main` directly and never self-merge. +Follow the repo's own branch-naming / PR conventions if it has them. + +## How to run a step — invoke its skill +Each step is its own repo-local skill in `.claude/skills/`. **At the start of each step, invoke that +step's skill and follow it precisely** — this SKILL.md is the conductor; the step skills are the +score. Invoke them with the `Skill` tool (or `/work-research` etc.); they auto-load from this repo. + +| Step | Skill to invoke | Produces | +| :--- | :--- | :--- | +| 1. research | `work-research` | `docs/work/NNN-/1-research.md` | +| 2. tests | `work-tests` | `docs/work/NNN-/2-tests.md` | +| 3. plan | `work-plan` (+ its `format.md`, `checklist.md`) | `docs/work/NNN-/3-plan.md` | +| 4. execute | `work-execute` | atomic commits + `test(e2e)` evidence | +| 5. docs | `work-docs` | `docs:` reconcile commits | + +Commit conventions for every step: `docs/commit-conventions.md` in this repo. + +## Orchestration loop (with HARD user checkpoints) +Run the steps **in order**. After **every** step comes a **hard checkpoint**: report the step's +outcome, then **stop — end your turn and wait for the user**. Prefer asking via the +`AskUserQuestion` tool (options like *approve* / *request changes*) so the stop is mechanical, not a +courtesy note. **Approval is an explicit user message that arrives AFTER the checkpoint report; +nothing else counts** — not your own confidence, not an "obvious" next move, not the user's original +request. **Never invoke the next step's skill in the same turn that finished the previous step.** If +the user asks for changes, apply them (re-dispatching subagents as needed), re-present the +checkpoint, and wait again. This is what keeps a heavyweight, autonomous process steerable. The flow +is the **same for every unit**; only the worker model tier differs (see *Task tier*): + +1. **Research.** Invoke `work-research`. Allocate `NNN-`, gather context, delegate any + internet research to a fresh subagent, have an author subagent write `1-research.md` (ending with + the **`Task tier`** classification), a reviewer validate it, then commit it. **Checkpoint — stop + and wait for approval:** show the user the research, the proposed scope, **and the task tier**; + let them correct any of it (including overriding the tier). Tests start only on their approval. +2. **Tests.** Invoke `work-tests`. Author subagent writes the Given-When-Then `2-tests.md`; reviewer + validates; commit. **Checkpoint — stop and wait for approval:** let the user adjust the behaviour + spec — this is the cheapest place to catch a misunderstanding. The plan starts only on their + approval. +3. **Plan.** Invoke `work-plan`. Author subagent writes the batched `3-plan.md`; reviewer validates + against the checklist; commit. **Checkpoint — stop and wait for approval:** let the user approve + the plan/scope. No code is written before their approval. +4. **Execute.** Invoke `work-execute`. Walk the batched TODO checklist: **one `coder` dispatch per + batch**, **one atomic commit per task** (you commit, never the subagent), build+tests green before + each. End with the live e2e smoke via `e2e-tester` (or a justified `e2e: n/a`). Stop at the + first task you can't make green and report. **Checkpoint — stop and wait for approval:** report + the commits + e2e verdict. Docs start only on the user's approval. +5. **Docs.** Invoke `work-docs`. Reconcile `CLAUDE.md`, READMEs, prose docs, `TODO.md` to the + shipped reality via author/reviewer subagents; commit on the unit's branch; then **open the squash + PR to `main`**. **Done:** report the full change set and the **PR URL** — the team reviews and + merges it manually; you do not merge. + +Throughout Steps 2–5, dispatch **every** subagent (authors, reviewers, `coder`, `e2e-tester`) at +the tier set in Step 1 — pass **`model: sonnet`** for a **lightweight** unit, otherwise **Opus** +(omit the param to inherit the Opus orchestrator). See *Task tier → Model policy*. + +Track the whole run in the TODO tool (`TaskCreate`/`TaskUpdate`) — one `in_progress` at a time — so +progress is visible across the long pipeline. + +## Non-negotiables (the whole point of the pipeline) +- **Orchestrator-only main thread.** You plan, delegate, verify, and commit. You do **not** write + production code or tests in the main session — that is the `coder` subagent's job, one atomic task + (or one batch) at a time. +- **The user advances the pipeline, not you.** Every step ends with a checkpoint report and a + stopped turn; the next step starts only on the user's explicit approval given **after** that + report. Running two steps in one turn is a pipeline violation even when the artifact looks perfect + and the next move seems obvious. +- **Model policy — one flow, tiered workers.** The five-step flow never branches. The orchestrator is + **always Opus**; the Step 2–5 worker subagents run on **Sonnet** for a **lightweight** unit and + **Opus** for a **standard** one, decided at the end of research (and only ever bumped up + mid-flight). Quality where it counts; cheap workers only where it's safe. +- **Author ≠ reviewer — a coverage control, not a correctness proof.** Every artifact is written by + one subagent and validated by a different one. Be clear-eyed about what this buys: it is a + **consistency and coverage** check (is the spec covered, are sources cited, is scope held, do the + numbers line up) — it does **not** prove the code builds or behaves. Stacking author + reviewer + compounds fluent confidence, not correctness; on the dimension that decides whether code works, + only an **executed** build/test run is evidence. A reviewer PASS never substitutes for a green run. +- **Internet research is always a fresh clean subagent**; cite sources. Treat a citation as proof + that *something was read*, not that the *conclusion is right* — verify load-bearing API claims + against the pinned dependency, not just against a link. +- **Branch per unit; squash PR; manual merge.** One task = one atomic commit = one acceptance + criterion (stage by path; never `git commit -a`), green at each batch boundary — see + `docs/commit-conventions.md` for what "atomic" scopes here. Every commit lands on the unit's branch + (`work/NNN-`), **never on `main`**; the unit ends with a **squash PR to `main`** that the + **team reviews and merges manually** — the orchestrator never commits to `main` and never merges. + Adapt to the repo's own branch-naming / PR conventions if it has them. +- **Nothing finishes red — and nothing finishes unrun.** The quality gate + (`.claude/hooks/quality-gate.sh`) runs the project's build + tests on Stop/SubagentStop; on a + failure it **blocks and feeds the failure back; if a retry still can't make it green it allows the + stop with a user-visible warning** (a stderr message plus a `systemMessage` in the UI) + so red is **surfaced, never silent**, and a human is handed the intervention. Where it's + unconfigured (no single command) or doesn't cover the code under change, each task is verified by + **actually executing** its own acceptance check — in a container if the local toolchain is missing. + A test that was never run is neither green nor red; "verified by reasoning" is never acceptable as + evidence. +- **Given-When-Then tests, avoid mocks** unless a true external seam needs one. +- **Don't quietly override the user's literal spec.** If you think the stated request should change + (a different name, a convention tweak, a "better" scope), that is a **checkpoint** — surface it and + let the user decide. Shipping your judgement in place of what they literally asked for, without + asking, is exactly the kind of silent decision the pauses exist to prevent. +- **All artifacts, docs, and commits in English.** + +## Subagents this repo ships +- **`coder`** — writes all production code and tests. Dispatch it per batch in Step 4. +- **`e2e-tester`** — drives the live running system per `docs/test/README.md` for the final smoke. + +Dispatch both at the **task tier** chosen at research-end — `model: sonnet` for a lightweight unit, +otherwise Opus. The same agent definition serves both tiers (the `model` is a per-dispatch override). + +Both are committed under `.claude/agents/`; dispatch them by name with the `Agent` tool. (They +register at session start — if you just bootstrapped the repo this session, restart Claude Code in +the project before dispatching them.) diff --git a/.claude/skills/work-docs/SKILL.md b/.claude/skills/work-docs/SKILL.md new file mode 100644 index 00000000000..e710ce5ca2c --- /dev/null +++ b/.claude/skills/work-docs/SKILL.md @@ -0,0 +1,126 @@ +--- +name: work-docs +description: Step 5 (final) of the work pipeline (research → tests → plan → execute → docs). After work-execute lands a unit's commits, reconciles ALL documentation OUTSIDE the docs/work/NNN-/ artifacts so it matches the shipped reality — CLAUDE.md, the prose docs under docs/, every README.md, docs/work/README.md (the pipeline doc, only if the pipeline itself changed), and TODO.md (condense fully-shipped items into a historical `- [x] … — ` checklist). Subagents author (one per surface, disjoint files), an independent reviewer validates against the code, the orchestrator commits atomic docs commits on the unit's branch, then opens a squash PR to main for team review. Never edits the NNN- research/tests/plan artifacts; never writes code. Use this as step 5, after work-execute has committed the unit, to bring the docs back in sync. +allowed-tools: Read, Glob, Grep, Bash(ls *), Bash(find *), Bash(git *), Agent, Task, TaskCreate, TaskUpdate, TaskList +disallowed-tools: Write, Edit, WebSearch, WebFetch +--- + +# work-docs — Step 5: reconcile the documentation + +Final step of the spec-driven pipeline: research (`work-research`) → tests (`work-tests`) → plan +(`work-plan`) → execute (`work-execute`) → **docs**. Once a unit's code is committed on the unit's branch, the +surrounding documentation has drifted: `CLAUDE.md` describes the old current-state, a README +references a removed endpoint, `TODO.md` still lists a finished item as open. This step walks the +just-shipped change set and brings **all documentation outside the `docs/work/NNN-/` +artifacts** back in sync with what the code now actually does. + +The orchestrator coordinates and commits; **subagents** do the writing; an **independent reviewer** +validates every claim against the code. Author ≠ reviewer. + +## Keep in front of mind (every decision) +- **Think Before Coding** — the code is already shipped; the job is to make the docs *true*. +- **Simplicity First** — change only the sentences the unit invalidated. No doc rewrites. +- **Surgical Changes** — touch only the lines the shipped change made wrong or missing. +- **Goal-Driven Execution** — done means: every doc statement matches the committed code, and + `TODO.md` reflects what is finished vs open. Verified, not assumed. +- Apply **KISS · YAGNI · SRP · DRY** — don't add speculative docs; one source of truth per fact. + +## What is IN scope (everything outside the work artifacts) +- **`CLAUDE.md`** (root) — the "Current state" paragraph, Structure, Commands, anything the unit + changed. This is the single most-read file; keep it honest about what exists *now*. +- **Prose docs under `docs/`** — architecture, repo-structure, environment, testing, etc. + (commit-conventions only if the conventions themselves changed.) +- **Every `README.md`** the unit touched — root and any sub-package READMEs. Leave the rest. +- **`docs/work/README.md`** — the pipeline doc. Update it **only if the pipeline itself changed** (a + new/renamed step, a changed convention). It is the one file *inside* `docs/work/` this step may + edit. +- **`TODO.md`** (root, if present) — reconcile the backlog (see below). + +## What is OUT of scope (never touch) +- **The unit artifacts `docs/work/NNN-/{1-research,2-tests,3-plan}.md`.** These are the + immutable record of how the unit was built, each committed by its own step. This skill does + **not** edit them. +- **Production code and tests.** This step writes documentation only. If syncing the docs reveals + the *code* is wrong, **stop and report** — that is a new unit of work, not a doc edit. + +## Core rules +- **Track steps in the TODO tool.** Use the task/todo list (`TaskCreate`/`TaskUpdate`) for the + detect → author → review → commit steps; one `in_progress` at a time, `completed` when done. +- **Orchestrator writes nothing.** This SKILL's frontmatter disallows `Write`/`Edit` entirely: the + main session reads, coordinates, verifies, and commits. The **author subagents** it spawns have + `Write` and make every doc edit; a **different** reviewer subagent validates. Author ≠ reviewer — + exactly as in `work-research`/`work-plan`. +- **Ground every claim in the committed reality — never in the plan's intentions.** Docs describe + what the code *does now*, not what the unit set out to do. The author must verify each edited + statement against the source, tests, and the actual commits (`git log`/`git show`/`git diff`). The + working tree often carries uncommitted WIP — so describe what is **true now**, and don't document + half-built layers as if they shipped. +- **Surgical, not a rewrite.** Edit the sentences the unit invalidated; preserve each file's + existing structure, voice, and heading layout. No reflowing, no reordering, no "while I'm here". +- **Fan out by surface, disjoint files.** The doc surfaces are independent files, so dispatch one + author subagent **per surface** (e.g. CLAUDE.md / the touched `docs/*.md` / the touched READMEs / + TODO.md) in parallel — they must edit **disjoint files**. A single reviewer subagent then + validates the whole set together. +- **`TODO.md` is a historical checklist.** See the dedicated section below. +- **Atomic `docs` commits on the unit's branch.** Stage **by path** and commit per + `docs/commit-conventions.md` (`docs: …` / `docs(work): …`). Never `git commit -a` — that would + sweep in unrelated WIP. Keep it to the fewest atomic commits that read as one closed change + (typically one `docs: reconcile … for NNN ` commit; split TODO grooming into its own commit + only if it is unrelated to this unit). These are the **last commits before the squash PR**. +- **Quality gate.** Doc-only changes don't affect the build, but the Stop/SubagentStop hook still + runs — keep the build/tests green (they are, since no code changed) and never leave the tree dirty + after committing. +- **Model tier.** Dispatch every subagent (per-surface authors, reviewer) at the tier recorded as + `Task tier` in `1-research.md` — pass **`model: sonnet`** for a **lightweight** unit, otherwise + **Opus** (omit the param to inherit the always-Opus orchestrator). The tier changes only the worker + model, never this step's procedure. +- All docs in **English**. + +## TODO.md — reconcile to a historical checklist +`TODO.md` is the backlog. After a unit ships, groom it so it reflects truth and stays small: +- **Condense fully-shipped items.** When a backlog item is completely delivered, collapse its + verbose decision/prose block into a **single simple checklist line**, ticked, with the commit + where it landed: + ``` + - [x] — shipped in () + ``` + Keep a one-line pointer to its `docs/work/NNN-/` unit if useful; drop the long rationale (it + already lives in the unit's research/plan). +- **Leave open items as `- [ ]`.** Items not yet done stay unchecked, in the same simple `- [ ]` + form. Don't invent new backlog items here. +- **Result:** `TODO.md` becomes a flat, scannable history — done work crossed off with its commit, + open work as plain unchecked boxes — not a wall of prose. + +## Procedure +1. **Identify the shipped change set — abort if unclear.** Confirm with the user (one line) which + unit just shipped; reuse its `NNN-`. Confirm `git status` shows a **clean tree on the unit's + branch** (`work/NNN-`) — if dirty or off the branch, **stop and report** (don't reconcile + onto someone's WIP). Determine + what actually changed: `git log`/`git diff` for the unit's commits and the current source state. +2. **Find the drift.** For each in-scope surface, read it and compare against the shipped reality. + Build the precise edit list: which file, which statement, what it should now say. If nothing + drifted for a surface, leave it untouched (and say so). If a doc is correct only because the + *code* is wrong, **stop and report** — that's a new unit, not a doc edit. +3. **Author subagents (parallel, one per surface, disjoint files).** Dispatch an author subagent per + surface with its exact edit list, the grounding evidence (the relevant commits/code), and the + rule "surgical edits only; verify every changed sentence against the code". Include + `docs/work/README.md` only if the pipeline itself changed, and `TODO.md` per the section above. +4. **Independent reviewer subagent (one, over the whole set).** A subagent that did **not** author + validates: every edited statement matches the committed code (spot-check against the source/`git + show`); no NNN-`` artifact was touched; no production code/tests changed; edits are surgical + (no gratuitous rewrites); `TODO.md` follows the historical-checklist form; English; internal + links still resolve. Loop author→reviewer until it passes with no open items. +5. **Commit on the unit's branch.** The orchestrator stages **by path** the touched doc files + (`git add CLAUDE.md docs/… README.md TODO.md …`) and makes the atomic `docs: …` commit(s) per + `docs/commit-conventions.md`. Never `git commit -a`; never stage `docs/work/NNN-/` artifacts + (already committed by their own steps). +6. **Open the squash PR to `main`.** Push the unit's branch and open a **squash** PR into `main` + (e.g. `gh pr create --base main`), titled for the unit, with a description that summarises the + change and links the `docs/work/NNN-/` artifacts. **The team reviews and merges it manually + — you do NOT merge it, and you never commit to `main` yourself.** (Follow the repo's own PR + conventions if it has them — labels, reviewers, template.) +7. **On failure** (drift can't be resolved without a code change / reviewer can't pass / a stray edit + hit a work artifact or code): **stop**, leave the tree clean, and report what blocked it. +8. **On completion:** report the doc commit(s) made, which surfaces changed (and which were already + accurate), the **PR URL** awaiting team review, and that the documentation is back in sync with the + unit shipped in `docs/work/NNN-/`. diff --git a/.claude/skills/work-execute/SKILL.md b/.claude/skills/work-execute/SKILL.md new file mode 100644 index 00000000000..584101922f0 --- /dev/null +++ b/.claude/skills/work-execute/SKILL.md @@ -0,0 +1,132 @@ +--- +name: work-execute +description: "Step 4 of the work pipeline (research → tests → plan → execute → docs). Walks the BATCHED TODO commit checklist in docs/work/NNN-/3-plan.md. For each batch (one coder dispatch — all the batch's tasks in a single invocation, warm context) it still makes ONE atomic commit per task (red→green, Given-When-Then, avoid mocks), verifying the build + tests green before each commit; then ends with the MANDATORY final e2e batch — dispatching the e2e-tester subagent against the live system and committing the evidence (or skipping on a justified e2e: n/a). Never writes code in the main thread. Stops at the first task that can't be made green and reports. Use this as step 4, after 3-plan.md exists, to implement and commit the plan's TODO checklist." +allowed-tools: Read, Glob, Grep, Bash, Agent, Task, TaskCreate, TaskUpdate, TaskList, Edit(docs/work/**) +disallowed-tools: Write +--- + +# work-execute — Step 4: execute the plan + +Step 4 of the spec-driven pipeline: research (`work-research`) → tests (`work-tests`) → plan +(`work-plan`) → **execute** → docs (`work-docs`). Walks the TODO checklist in +`docs/work/NNN-/3-plan.md`, turning each task into one atomic commit on the **unit's branch** +(`work/NNN-`, created in `work-research`). The orchestrator coordinates and commits; the +**`coder` subagent** writes all code. + +## Keep in front of mind (every decision) +- **Think Before Coding** — the thinking is done in the plan; execute it faithfully. +- **Simplicity First** — implement exactly the task's acceptance criterion, nothing more. +- **Surgical Changes** — one task = one commit = one closed change. +- **Goal-Driven Execution** — a task is done only when **its own acceptance criterion** (from + `3-plan.md`) is green and it's committed — not merely when the build/tests pass. +- Apply **KISS · YAGNI · SRP · DRY** — let the `coder` subagent resist scope creep. + +## Core rules +- **The orchestrator drives, in the main thread.** This skill runs in the main session: the + orchestrator owns every decision (what to dispatch, whether a task is green, when to commit, when + to stop). It does this work itself — it does **not** delegate the coordination to a subagent — so + the live TODO list and the decisions are visible in the main thread. +- **Mirror the plan into the TODO tool — and keep BOTH surfaces in lockstep.** Before dispatching + anything, turn the plan's commit checklist into task-list items (`TaskCreate`); mark each + `in_progress` (`TaskUpdate`) before dispatching its `coder` subagent. The live `TaskList` is the + in-session decision/visibility surface; the `## TODO` checklist in `docs/work/NNN-/3-plan.md` + is the **durable, in-repo record** that outlives the session. They must agree at all times — so + for each task: flip its `[ ]→[x]` in `3-plan.md` **before** committing and fold that tick **into + the task's own atomic commit** (the commit's tree shows the box checked), then `TaskUpdate` the + live item to `completed` after the commit lands. Never leave the markdown tick dangling in the + working tree as a separate, uncommitted change — that would pollute the next task's commit and + break the clean-tree precondition. +- **Orchestrator never writes code — and edits ONLY the plan checklist.** This SKILL's frontmatter + disallows `Write` entirely and scopes `Edit` to `docs/work/**`. The orchestrator's *only* + permitted edit is ticking the `## TODO` boxes in `3-plan.md`. All production code and tests are + written by the **`coder` subagent** (`.claude/agents/coder.md`), one atomic task at a time — never in + the main thread. +- **One dispatch per BATCH; one atomic commit per TASK.** The plan groups tasks under `### Batch` + headers. Dispatch **one** `coder` subagent per batch and give it **all** the batch's tasks (in + listed order) in that single invocation — this is how we avoid spawning a subagent per micro-task. + The subagent implements the whole batch and leaves the tree **uncommitted**; the **orchestrator** + then walks the batch's task lines and makes **one atomic commit per task** (staging by path), each + green before it lands. Batching never coarsens history — it changes how many subagents run, not + how many commits there are. Note the acceptance check runs against the **full batch tree**, then + only that task's files are staged — the committed subset is **not** rebuilt in isolation (fine: + `main` gets one squashed commit; see `docs/commit-conventions.md` for the full scoping). +- **One task → one atomic commit on the unit's branch.** Never commit to `main` directly (the unit + ends with a squash PR to `main`, team-reviewed and merged manually — `work-docs` opens it). Commit + per `docs/commit-conventions.md` only after the task's **acceptance criterion** is green — usually the + project's build + test commands (see `CLAUDE.md` → **Commands**); for tasks whose files the build + doesn't cover (e.g. shell/infra scripts) it is the build-style check the plan names (e.g. + `bash -n