From 2af0c58385e26bb6458a2916d80d1eba9902d747 Mon Sep 17 00:00:00 2001 From: Ian Cooper Date: Thu, 8 Oct 2026 09:18:08 +0100 Subject: [PATCH 01/12] feat(skills): one tasks.md and a shiftable TDD review gear for /spec Port Brighter's review gear (Brighter ADR 0071) to Darker's spec commands. - One task list: /spec:ralph-tasks is removed; /spec:ralph-implement now runs the approved tasks.md (requires .tasks-approved), skips ticked tasks, and dispatches on the task's leading label, marking unrecognised shapes [!] rather than improvising. - The approval gate is a gear: new /spec:gear reports or shifts it via the gitignored, per-spec specs/{spec}/.current-gear, optionally scoped to a section of tasks.md. review-before (gated) is the default; absent, unparseable, or out-of-scope all resolve to review-before. - /spec:implement resolves the gear per task and accepts --review-before / --review-after; /spec:ralph-implement writes review-after and stops cleanly (DOWNSHIFTED) on a downshift. - review-after removes only the pause: RED-first, the full suite, the two-commit shape (change, then docs: tick) and test conventions hold in both gears. - CHARACTERISE task shape: RED observed via a named, reverted production mutation; never ALREADY_COMPLETE. - /spec:tasks requires tag-first task labels and keeps the gate line; /spec:approve drops the attended/unattended fork; /spec:status reports the gear. - Standalone /test-first and /bugfix:test are always gated; CLAUDE.md, testing.md and the skill indexes updated to match. Co-Authored-By: Claude Opus 5.5 --- .agent_instructions/skills_overview.md | 30 +- .agent_instructions/testing.md | 70 ++++- .claude/commands/README.md | 20 +- .claude/commands/bugfix/test.md | 11 +- .claude/commands/spec/README.md | 260 ++++++++++------ .claude/commands/spec/approve.md | 47 +-- .claude/commands/spec/gear.md | 191 ++++++++++++ .claude/commands/spec/implement.md | 161 +++++++++- .claude/commands/spec/ralph-implement.md | 381 +++++++++++++++++------ .claude/commands/spec/ralph-tasks.md | 166 ---------- .claude/commands/spec/status.md | 19 ++ .claude/commands/spec/tasks.md | 78 ++++- .claude/commands/tdd/README.md | 7 +- .claude/commands/tdd/test-first.md | 78 ++++- .gitignore | 6 + CLAUDE.md | 20 +- 16 files changed, 1117 insertions(+), 428 deletions(-) create mode 100644 .claude/commands/spec/gear.md delete mode 100644 .claude/commands/spec/ralph-tasks.md diff --git a/.agent_instructions/skills_overview.md b/.agent_instructions/skills_overview.md index 70b35bba..c764e6c8 100644 --- a/.agent_instructions/skills_overview.md +++ b/.agent_instructions/skills_overview.md @@ -12,7 +12,7 @@ Skills are slash commands that automate multi-step workflows and enforce Darker' | Skill | Purpose | Usage | |-------|---------|-------| -| `/test-first` | TDD with mandatory approval | `/test-first ` | +| `/test-first` | TDD with an approval gate (armed by default) | `/test-first ` | | `/tidy-first` | Separate refactoring from features | `/tidy-first ` | | `/adr` | Create Architecture Decision Record | `/adr ` | | `/bugfix:*` | Diagnosis-first bug workflow (Confirm gate) | `/bugfix:triage [issue \| description]` | @@ -41,8 +41,8 @@ Skills are slash commands that automate multi-step workflows and enforce Darker' | `/spec:approve` | Approve phases | `/spec:approve <phase> [adr-number]` | | `/spec:review` | Review phases | `/spec:review [phase] [adr-number]` | | `/spec:switch` | Switch to different spec | `/spec:switch <spec-name>` | -| `/spec:ralph-tasks` | Generate unattended TDD tasks | `/spec:ralph-tasks` | -| `/spec:ralph-implement` | Unattended TDD implementation | `/spec:ralph-implement [count]` | +| `/spec:gear` | Report or shift the TDD review gear | `/spec:gear [review-before\|review-after] ["section"] [--because "..."]` | +| `/spec:ralph-implement` | Unattended TDD implementation over the approved `tasks.md` | `/spec:ralph-implement [count]` | ## Quick Reference Card @@ -61,7 +61,7 @@ BUGFIX (DIAGNOSIS-FIRST) TEST-DRIVEN DEVELOPMENT /test-first <behavior> Write test -> Approve -> Implement -> Refactor - Enforces mandatory approval before implementation + Approval before implementation — armed by default (shift with /spec:gear) Example: /test-first when query handler throws it should invoke fallback policy REFACTORING @@ -85,8 +85,9 @@ SPECIFICATION WORKFLOW /spec:approve <phase> -> Approve phase RALPH LOOP (UNATTENDED) - /spec:ralph-tasks -> Generate ralph tasks (standalone, from approved design) - /spec:ralph-implement [count] -> Unattended self-driving loop (opus + auto mode) + /spec:gear review-after -> Upshift: disarm the per-test pause (scoped, with a reason) + /spec:ralph-implement [count] -> Unattended self-driving loop over tasks.md (opus + auto mode) + /spec:gear review-before -> Downshift: loop stops after current task, nothing unwound ``` ## Decision Tree: Which Skill Should I Use? @@ -124,7 +125,7 @@ Each skill enforces specific practices from `.agent_instructions/`: **Enforces**: - Red-Green-Refactor TDD cycle -- **MANDATORY approval before implementation** +- **Approval before implementation** — armed by default; a standalone `/test-first` is always gated - BDD-style test naming (`When_X_should_Y`) - One test per file - Developer tests (not unit tests) @@ -236,19 +237,18 @@ Each skill enforces specific practices from `.agent_instructions/`: ### Workflow 5: Ralph Loop (Unattended) ```bash -# 1. Complete spec workflow up to an APPROVED DESIGN (no tasks step needed for this path) -# /spec:requirements -> /spec:approve requirements -> /spec:design -> /spec:approve design +# 1. Complete the spec workflow through to an APPROVED TASK LIST +# /spec:requirements -> /spec:design -> /spec:tasks -> /spec:review tasks -> /spec:approve tasks -# 2. Generate ralph-tasks directly from the approved design -/spec:ralph-tasks +# 2. Upshift the review gear (scoped, with a reason) once the task shape is settled +/spec:gear review-after "Phase 2 — Cache key generation (behaviour)" --because "shape is settled" -# 3. Review ralph-tasks.md in your IDE - -# 4. Switch to opus + enable auto mode, then run the self-driving loop +# 3. Switch to opus + enable auto mode, then run the self-driving loop over tasks.md /model opus /spec:ralph-implement # choose the bound: tasks / turns / budget -# 5. Stop if needed +# 4. Take back per-test review, or stop outright +/spec:gear review-before # loop stops after the current task; resume with /spec:implement touch RALPH_STOP # unattended kill-switch (or press Esc for a pending wake-up) # 6. Review results diff --git a/.agent_instructions/testing.md b/.agent_instructions/testing.md index 7fbd47f5..780aceff 100644 --- a/.agent_instructions/testing.md +++ b/.agent_instructions/testing.md @@ -18,9 +18,11 @@ - **DO NOT run tests without approval** - **STOP after writing the test and ASK FOR APPROVAL** - The user will review the test in their IDE, not in CLI output -- This is NOT optional - the approval gate is MANDATORY when working with Claude Code +- The approval gate is **armed by default**. Assume it is armed unless a spec command has explicitly + told you the spec is in the `review-after` gear -This ensures the mandatory approval step is never skipped and tests are reviewed before implementation. +This ensures the approval step is never skipped by accident and tests are reviewed before +implementation. - We write developer tests - Failure of a test case implicates the most recent edit. @@ -31,13 +33,73 @@ This ensures the mandatory approval step is never skipped and tests are reviewed - **APPROVAL**: Get approval for the test before implementing - Green: Make the test pass, commit any sins necessary to move fast - Refactor: Improve the design of the code. -- **Approval Workflow** (MANDATORY - NOT OPTIONAL): +- **Approval Workflow** (⛔ ARMED BY DEFAULT): - When working on a feature, ALWAYS use `/test-first <behavior>` - do not write tests manually - The skill will write the test and ASK FOR APPROVAL before proceeding - The user will review the test in their IDE - DO NOT run tests or start implementation without explicit user approval - After approval, implement the minimum code to make the test pass - - The approval step is MANDATORY when working with Claude Code - you cannot bypass it + - You cannot bypass the gate on your own initiative. It is disarmed only by a deliberate, + recorded, scoped gear shift the user makes with `/spec:gear` — never by assumption, never by a + prose instruction in a scratch file, and never because the tasks look repetitive + +### When a new test passes on first run — characterisation + +Sometimes the test you write for a behaviour passes before you have written any code, because +earlier work already delivers it. This is common when a spec takes one task per acceptance +criterion. A green test has not yet shown that it can fail, so it has not yet shown that it guards +anything. **Do not weaken, rewrite or delete it to get a failure, and do not treat the task as +"already complete".** + +Instead, observe RED through a **named mutation**: + +1. Apply a temporary change to **production code, never to the test**. It should be the realistic + defect the test exists to catch. +2. Run the test. It must fail **on the assertion it is about**. A compile error, or an unrelated + exception thrown before the assertion is reached, does not count; pick a better mutation. +3. **Revert the mutation** and confirm green. `git status` must show no production file still + modified. +4. Run the **full test suite**, as for any other test. +5. Commit the test alone, as `test:`, noting the mutation in the message. The mutation is never + committed. + +In a spec, such tasks are labelled `CHARACTERISE` in `tasks.md` and name their mutation. If you meet +an unexpected green with no named mutation, stop and ask. Choosing the mutation is part of +reviewing the test, so do not improvise it. + +### The review gear + +Whether the approval pause fires is a **gear** (see +[.claude/commands/spec/gear.md](../.claude/commands/spec/gear.md) and Brighter's +[ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)), selected for the certainty you have and the blast radius you face — high value +early in a spec, low value on a run of near-identical tasks whose shape has already been reviewed +repeatedly. + +| Gear | Gate | Meaning | +|------|------|---------| +| `review-before` | ✅ armed | Each test reviewed in the IDE before implementation. **The default.** | +| `review-after` | ➖ not armed | RED still proved first; reviewed as a batch afterwards. | + +- The gear lives in `specs/{spec}/.current-gear` — untracked working state, scoped to one spec and + optionally to one section of `tasks.md`, carrying the reason for the shift. Shift it with + `/spec:gear`, in either direction, at any time. +- `/spec:implement` honours it; `/spec:ralph-implement` is always `review-after`. A **standalone + `/test-first` and `/bugfix:test` are always gated** — they do not read the gear file. +- Absent, unparseable, unknown value, or a task outside the gear's scope all resolve to + `review-before`. Fail safe, never fail open. + +**What `review-after` does NOT remove.** Only the human pause. All of these hold in both gears: + +- **RED first** — the test is written and observed to fail *for the right reason* before any + production code exists, or, for a characterisation test, under its named mutation (see above). + Ungated is not test-after. +- **The full regression suite**, not just the new test's own `--filter`. +- **The two-commit shape** — a `feat:`/`test:` commit for the behaviour, then a separate `docs:` + commit ticking the task off in `tasks.md`. +- **Every convention in this document** — naming, one test per file, test doubles in `TestDoubles/`, + Real > Simple > InMemory > Mock, no mocks for isolation. + +A run in `review-after` that drops any of these is defective — it is not "a different gear". - Where possible, avoid writing tests after. - This will not give you scope control - only writing the code required by tests. - You should only write the code necessary for a test to pass; do not write speculative code. diff --git a/.claude/commands/README.md b/.claude/commands/README.md index 21a648a0..ddbefea4 100644 --- a/.claude/commands/README.md +++ b/.claude/commands/README.md @@ -7,7 +7,7 @@ This directory contains Claude Code skills (slash commands) that enforce Darker' Skills are invoked using slash commands in Claude Code: ```bash -/test-first <behavior description> # TDD with mandatory approval +/test-first <behavior description> # TDD with an approval gate (armed by default) /adr <title> # Create Architecture Decision Record /tidy-first <change description> # Separate structural from behavioral changes /bugfix:triage <issue | description> # Diagnosis-first bug workflow (Confirm gate) @@ -19,7 +19,7 @@ Skills are invoked using slash commands in Claude Code: **Command**: `/test-first <behavior description>` -**Purpose**: Enforces TDD workflow with mandatory user approval before implementation. +**Purpose**: Enforces TDD workflow with user approval before implementation (the gate is armed by default). **When to use**: - Adding new behavior or functionality @@ -37,7 +37,7 @@ Skills are invoked using slash commands in Claude Code: /test-first when query handler throws it should invoke fallback policy ``` -**Why it matters**: The approval step is MANDATORY per testing.md when working with AI. This skill enforces that requirement, preventing implementation before you validate the test specification. +**Why it matters**: The approval step is armed by default per testing.md when working with AI, and only a deliberate `/spec:gear` shift disarms it for a spec. This skill enforces that requirement, preventing implementation before you validate the test specification. Documentation: [.claude/commands/tdd/README.md](tdd/README.md) @@ -146,12 +146,12 @@ Documentation: [.claude/commands/bugfix/README.md](bugfix/README.md) ### Specification Workflow Skills - **`/spec:requirements`** - Capture requirements - **`/spec:design`** - Create design ADRs -- **`/spec:tasks`** - Break down implementation (attended path) -- **`/spec:implement`** - TDD implementation (attended path) -- **`/spec:ralph-tasks`** - Generate unattended TDD tasks (standalone, from approved design) -- **`/spec:ralph-implement`** - Unattended self-driving TDD loop (opus + auto mode) -- **`/spec:status`** - Show spec status -- **`/spec:approve`** - Approve phases (prompts the attended/unattended fork at design) +- **`/spec:tasks`** - Break down implementation into the single `tasks.md` +- **`/spec:implement`** - TDD implementation, one task at a time (honours the review gear) +- **`/spec:ralph-implement`** - Unattended self-driving TDD loop over the same `tasks.md` (opus + auto mode, `review-after` gear) +- **`/spec:gear`** - Report or shift the TDD review gear (`review-before` / `review-after`) +- **`/spec:status`** - Show spec status (including the review gear) +- **`/spec:approve`** - Approve phases - **`/spec:review`** - Review phases Documentation: [.claude/commands/spec/README.md](spec/README.md) @@ -213,7 +213,7 @@ These core skills enforce Darker's mandatory engineering practices: | Skill | Enforces | Creates | |-------|----------|---------| -| `/test-first` | TDD with approval | Tests -> Implementation -> Refactoring | +| `/test-first` | TDD with an approval gate (armed by default) | Tests -> Implementation -> Refactoring | | `/adr` | Documented decisions | Numbered ADR files | | `/tidy-first` | Structural/behavioral separation | Two commits: refactor + feat | | `/bugfix:*` | Confirm root cause before fixing | Bug record + regression test + scoped `fix:` commit | diff --git a/.claude/commands/bugfix/test.md b/.claude/commands/bugfix/test.md index 0859401a..0dae4813 100644 --- a/.claude/commands/bugfix/test.md +++ b/.claude/commands/bugfix/test.md @@ -14,9 +14,15 @@ Confirm gate: !`test -f bugfixes/$(cat bugfixes/.current-bug 2>/dev/null)/.confi **Workflow**: Issue → Triage → Confirm (✋ gate) → **Test-first (✋ gate)** → Fix → Verify Write the **failing regression test** that pins the confirmed bug. This step is a thin wrapper -around `/test-first` — it carries the confirmed diagnosis in, and `/test-first`'s own mandatory +around `/test-first` — it carries the confirmed diagnosis in, and `/test-first`'s own IDE-approval gate governs the test before any implementation. +> **Always gated.** The TDD review gear ([`spec/gear.md`](../spec/gear.md)) is a spec-workflow +> mechanism: `/spec:gear` disarms the approval pause for one *spec*, and bugfixes are outside it. +> Never read `specs/*/.current-gear` here, and never infer a gear from an ungated spec run happening +> on the same branch. A regression test pins a defect a human already had to reason about at the +> Confirm gate; it gets read before the fix, every time. + > **Recommended model: `sonnet`.** This step writes a test in the main agent and runs `dotnet`, > so it is implementation work — match the model policy for implementation > (`.claude/commands/bugfix/README.md` → "Sub-agents & model policy"). If your session is on @@ -45,7 +51,8 @@ Follow `/test-first` exactly: real/Simple/InMemory* test doubles over mocks, public exports only) and run it to confirm it fails for the right reason. - ✋ **APPROVAL GATE**: `/test-first` requires explicit user approval of the test before any - implementation. Honour it — do not write the fix here. + implementation. Honour it — do not write the fix here. This gate is unconditional in the bugfix + workflow; there is no gear to shift. If the Scope Notes listed additional defects (including a sync/async pipeline parity twin), write a failing test for each (run `/test-first` once per behaviour) so the fix can't leave a known diff --git a/.claude/commands/spec/README.md b/.claude/commands/spec/README.md index d16ad058..72ec695b 100644 --- a/.claude/commands/spec/README.md +++ b/.claude/commands/spec/README.md @@ -9,7 +9,8 @@ The spec commands provide a structured approach to designing and implementing fe 1. **Requirements**: Capture user needs and problem statements 2. **Design (ADRs)**: Document architectural decisions (can have multiple ADRs per requirement) 3. **Tasks**: Break down implementation into actionable steps -4. **Implementation**: Follow TDD to write tests and code +4. **Implementation**: Follow TDD to write tests and code, in whichever **review gear** the work + currently warrants — shifted with `/spec:gear`, armed by default ## Workflow @@ -29,30 +30,42 @@ Specification Workflow | (Repeat for multiple architectural decisions) | v - Choose by certainty -----> (prompted at /spec:approve design) - | - +-- Attended --------> /spec:tasks - | (review each /spec:approve tasks - | test) /spec:implement (sonnet; TDD: Tests -> Code) - | - +-- Unattended ------> /spec:ralph-tasks (standalone, from approved design) - (review in /spec:ralph-implement (opus + auto mode, self-driving loop) - batches) + tasks.md -----------------> /spec:tasks + | /spec:review tasks + | /spec:approve tasks | v + Pick a driver, ----------> /spec:implement (sonnet; one task at a time) + shift gear as you go /spec:ralph-implement (opus + auto mode; unattended loop) + | ^ + | +-- /spec:gear shifts the review gear, either way + v Pull Request ``` -**The certainty fork.** After the design is approved, you pick *one* of two paths — and -`/spec:approve design` prompts you to choose: +**One task list, one gear lever.** There is a single task list — `tasks.md` — and both drivers run +it. What changes between "attended" and "unattended" is the **review gear**, and the gear is +shiftable at any point in either direction (Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)): + +| Gear | Gate | Meaning | +|------|------|---------| +| `review-before` | ✅ armed | Every test reviewed in the IDE before implementation. **The default.** | +| `review-after` | ➖ not armed | RED still proved first; the work is reviewed as a batch afterwards. | + +**Gears, not paths.** You select a gear for the certainty you have and the blast radius you face. +Early in a spec, while the design is still moving, run `review-before`. Later, on a run of +near-identical tasks whose shape has been approved several times, upshift. The moment the work +starts producing tests you would not have approved, downshift — mid-section, with nothing unwound. + +The two drivers: + +- **`/spec:implement`** — one task at a time in the main agent on **sonnet**. Honours whatever gear + is set. +- **`/spec:ralph-implement`** — a self-driving unattended loop over the **same** `tasks.md`, on + **opus** under **auto mode**, delegating each task to a **sonnet** sub-agent. Always + `review-after`; a downshift stops it cleanly. -- **Attended** (`/spec:tasks` → `/spec:implement`): a strict Red → **user approval** → Green - → Refactor loop in the main agent on **sonnet**. Every test is reviewed in the IDE before - implementation. Use when the work is uncertain. -- **Unattended** (`/spec:ralph-tasks` → `/spec:ralph-implement`): `ralph-tasks.md` is - generated **directly from the approved design** (no `tasks.md`, no per-test gates), then a - self-driving loop on **opus** under **auto mode** delegates each task to a **sonnet** - sub-agent. Reviewed in batches rather than per test. Use when the work is well-understood. +Neither regenerates a task list, and switching between them costs nothing. ## Sub-agents & model policy @@ -66,11 +79,6 @@ context clean and gives the heavy work a focused, single-purpose context. what is in its prompt. The command reads the needed files (and runs `gh`/`git`) first, then passes the text or paths. 2. **Launch `Agent`** with an explicit `subagent_type` and `model`: - - **`/spec:ralph-tasks`** uses `subagent_type: "Plan"`. `Plan` has all tools **except** - `Agent`, `ExitPlanMode`, `Edit`, `Write`, and `NotebookEdit` — so it can - Read/Glob/Grep/Bash but has no file-editing tool. That makes it much **harder** for the - sub-agent to accidentally write the spec file than relying on the prompt alone (it still - has `Bash`, so the prompt also forbids `echo >`/`tee`/`sed -i`). - **`/spec:review`** uses `subagent_type: "general-purpose"` (adversarial reasoning that needs no source mutation). - **`/spec:ralph-implement`** uses `subagent_type: "general-purpose"` because its per-task @@ -78,9 +86,8 @@ context clean and gives the heavy work a focused, single-purpose context. 3. **The main agent owns all user interaction.** A sub-agent is one-shot — once launched it runs to completion and returns; it cannot pause to ask the user anything. So before launching, the main agent clarifies any ambiguous inputs with the user via `AskUserQuestion`, - then launches the sub-agent with the clarified inputs folded in. The `Plan`-based - `ralph-tasks` sub-agent has no `AskUserQuestion` so it *structurally* can't prompt; the - `general-purpose` `review` sub-agent is explicitly instructed not to. **Exception:** + then launches the sub-agent with the clarified inputs folded in. The + `general-purpose` `review` sub-agent is explicitly instructed not to prompt. **Exception:** `/spec:ralph-implement` runs fully **unattended** — neither its orchestrator nor its sub-agent prompts the user once the loop starts. 4. **The sub-agent RETURNS its artifact as text** — it does *not* write the spec file. The one @@ -97,23 +104,22 @@ run inline in the main agent rather than delegating — they have no sub-agent t | Command | Sub-agent (type) | Model | Rationale | |---------|------------------|-------|-----------| | `/spec:review` | Yes — `general-purpose` | **opus** | Adversarial reasoning | -| `/spec:ralph-tasks` | Yes — `Plan` (read-only) | **opus** | Planning / decomposition | | `/spec:ralph-implement` (orchestrator) | — (the loop itself) | **opus** | Cheap bookkeeping + **required for auto mode** | | `/spec:ralph-implement` (per-task sub-agent) | Yes — `general-purpose` (writes source) | **sonnet** | Mechanical TDD implementation, kept off the opus loop context for cost | | `/spec:implement` | No | **sonnet** (Step 0 prompts to switch) | Implementation work; runs in the main agent, so set the session model | | `/spec:requirements`, `/spec:design`, `/spec:tasks` | No (main agent) | — | Run inline | -| `/spec:new`, `/spec:switch`, `/spec:approve`, `/spec:status` | No | — | Mechanical bookkeeping | +| `/spec:new`, `/spec:switch`, `/spec:approve`, `/spec:status`, `/spec:gear` | No | — | Mechanical bookkeeping | `/spec:ralph-implement` runs **two models on purpose**: the orchestrator loop on **opus** (required for **auto mode**, and the policy for the unattended path) does only cheap -bookkeeping — STOP-file check, task selection, marking checkboxes, committing, counting — while +bookkeeping — STOP-file and gear checks, task selection, marking checkboxes, committing, counting — while each task's actual test + implementation is delegated to a **sonnet** sub-agent. That keeps the expensive per-task churn on the cheaper model and out of the opus loop's context, lowering cost without taking the orchestrator off opus. `/spec:implement` is deliberately **not** delegated: its per-behavior -Red → user-approval → Green → Refactor loop is interactive, and the mandatory approval gate must -run in the main agent where it can reach the user. Because there is no sub-agent to assign a model +Red → user-approval → Green → Refactor loop is interactive, and the approval gate — armed by +default — must run in the main agent where it can reach the user. Because there is no sub-agent to assign a model to, run the command itself on **sonnet** (the session model) — it is implementation work. **Step 0 of `/spec:implement` actively checks the session model and prompts you to switch to sonnet if you are on another model** (e.g. opus). @@ -164,10 +170,13 @@ Approve a specification phase or specific ADR. /spec:approve tasks ``` -Approving the **design** also **prompts you to choose the implementation path** (the certainty -fork): the **attended** path (`/spec:tasks` → `/spec:implement`, review each test) or the -**unattended** path (`/spec:ralph-tasks` → `/spec:ralph-implement`, review in batches). Either -path can start straight from the approved design. +Approving the **design** points you at `/spec:tasks` — there is one task list and one route to +it. Attended vs. unattended is not a fork here; it is a **gear** you pick (and change) at +implementation time with `/spec:gear`. + +Approving **tasks** freezes the **content** of `tasks.md`. Checkbox state (`[ ]` → `[x]`/`[!]`) is +progress bookkeeping and both drivers write it; rewording, adding, removing or reordering tasks +after that point needs a fresh review. --- @@ -186,20 +195,23 @@ Review the current specification phase or specific ADR. ### `/spec:tasks` -Create implementation task list based on approved design (the **attended** path). +Create the implementation task list based on the approved design. This is the **single** task +list; both `/spec:implement` and `/spec:ralph-implement` run it. ```bash /spec:tasks ``` -> Pick *either* `/spec:tasks` (attended) *or* `/spec:ralph-tasks` (unattended) after design -> approval — they are alternative branches, not sequential steps. +Every task opens with a tag — `TEST + IMPLEMENT`, `CHARACTERISE`, `STRUCTURAL`, `SETUP`, `DOC` or +`VERIFY` — and behavioral tasks carry the `⛔` approval-gate line, which fires in the default +`review-before` gear. Do not encode the gear in `tasks.md`. --- ### `/spec:status` -Show status of all specifications. +Show status of all specifications, including the resolved review gear of the active spec (and of +any spec not in the default gear). ```bash /spec:status @@ -217,61 +229,86 @@ Switch to a different specification. --- -### `/spec:implement [task-number]` +### `/spec:implement [task-number] [--review-before|--review-after]` -Begin TDD implementation of approved specification (the **attended** path). Recommended on +Begin TDD implementation of the approved specification, one task at a time. Recommended on **sonnet**; Step 0 prompts you to switch if the session is on another model. ```bash -/spec:implement # All tasks -/spec:implement 3 # Specific task +/spec:implement # All tasks +/spec:implement 3 # Specific task +/spec:implement --review-after # Shift the gear and start in the same gesture (sugar for /spec:gear) ``` **Requirements:** Tasks approved (`.tasks-approved`) and all ADRs Accepted. -Follows a strict Red → **user approval** → Green → Refactor cycle, committing after each -behavior. The approval gate before each implementation is mandatory. +**Gear-aware.** The command resolves the review gear before every task — so a shift made from +another terminal takes effect at the next task, in either direction. It announces the resolved gear +and the reason it was shifted before starting. + +- In `review-before` (the default): Red → **user approval** → Green → Refactor. No implementation + is written without approval of the test. +- In `review-after`: the pause is skipped — and *only* the pause. RED must still be proved first, + and the run says so in one line. It still stops and asks if the test looks wrong, needs a design + decision, or duplicates an existing test. + +**Two commits per task, in both gears**: a `feat:`/`test:`/`fix:`/`refactor:` commit for the change, +then a separate `docs:` commit ticking the task off in `tasks.md`. --- -### `/spec:ralph-tasks` +### `/spec:gear [review-before|review-after] ["section"] [--because "..."]` -Generate `ralph-tasks.md` for unattended TDD implementation. **Standalone** — the unattended -peer of `/spec:tasks`, derived **directly from the approved design** (requirements + ADRs). It -does **not** require `tasks.md` or `.tasks-approved`. +Report or shift the **review gear** — whether `/test-first`'s approval gate is armed for the +current spec. See [`gear.md`](gear.md) and Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md). ```bash -/spec:ralph-tasks +/spec:gear # what gear am I in, and why? +/spec:gear review-after "Phase 2 — Cache key generation (behaviour)" \ + --because "Phase 1's five tests were all approved unchanged; the shape is settled" +/spec:gear review-before --because "the last two tests asserted the wrong thing" ``` -Creates `specs/{current-spec}/ralph-tasks.md`, formatted for unattended execution: - -- **No approval gates**: No `STOP HERE` or `/test-first` directives -- **RALPH-VERIFY**: Each task includes an exact `dotnet test --filter` command -- **References**: Each task lists files/ADRs to read (self-contained for fresh context) -- **Strict atomicity**: One behavior per task, ~200 lines max, ordered by dependency - -**Requirements:** Design approved (`.design-approved`) and all ADRs Accepted. - -**Ralph task format:** -```markdown -- [ ] **[Brief behavior description]** - - **Behavior**: [Precise behavioral specification] - - **Test file**: `test/[Project]/[When_condition_should_behavior.cs]` - - **Test should verify**: - - [Point 1] - - [Point 2] - - **Implementation files**: - - `src/[Project]/[File.cs]` - [What to add/change] - - **RALPH-VERIFY**: `dotnet test test/[Project]/ --filter "FullyQualifiedName~When_condition_should_behavior"` - - **References**: [ADR numbers, requirement sections, existing code files] +**The gear file** — `specs/{current-spec}/.current-gear`, **gitignored**: + ``` +# Working state — untracked. Shift with /spec:gear. +gear: review-after +scope: Phase 2 — Cache key generation (behaviour) +driver: ralph +shifted: 2026-10-07 — Phase 1's five tests were all approved unchanged; the shape is settled +``` + +| Field | Required | Meaning | +|-------|----------|---------| +| `gear` | yes | `review-before` \| `review-after` | +| `scope` | no | A section of `tasks.md` — a heading's text (any depth) or a `tasks N-M` range for flat lists. Absent = spec-wide. | +| `driver` | no | `implement` \| `ralph` | +| `shifted` | yes | Date + one-line reason | + +**Resolution** — absent file, unparseable file, unknown value, a task outside `scope:`, or a +`scope:` that matches nothing in `tasks.md` all resolve to `review-before`. Fail safe, never fail +open. + +**Why untracked**: the gear is where the work currently *is*, not what the project decided. +**Why per-spec**: a root-level gear file would survive branch switches and silently disarm the gate +for unrelated work. + +**Who honours it**: `/spec:implement` and `/spec:ralph-implement`. A standalone `/test-first` and +`/bugfix:test` are **always gated** — they never read the file. + +**What `review-after` does NOT remove**: RED-first, the full regression suite, the two-commit shape, +and every test-authoring convention. Only the human pause goes. + +**Discoverability**: because the file is untracked, `/spec:gear` also maintains a pointer line in +`PROMPT.md`, and `/spec:status` reports the resolved gear. --- ### `/spec:ralph-implement [count]` -Unattended TDD implementation from `ralph-tasks.md` via a **self-driving loop**. Run it on +Unattended TDD implementation from the spec's **approved `tasks.md`** — the same task list +`/spec:implement` works from — via a **self-driving loop** in the `review-after` gear. Run it on **opus** with **auto mode** enabled for a true unattended run. ```bash @@ -282,10 +319,16 @@ Unattended TDD implementation from `ralph-tasks.md` via a **self-driving loop**. /spec:ralph-implement 3 ``` +**No separate task list.** This command reads `tasks.md`, skips tasks already ticked, and honours +the gear's `scope:` if one is set. Nothing is regenerated; there is no `ralph-tasks.md` (older specs +may still hold one from the retired `/spec:ralph-tasks` command — it is historical). + **Up-front setup (Step 0, the only interactive part):** - Advises that the orchestrator should be on **opus** and that **auto mode** should be on (auto mode is a permission mode set in Claude Code settings / `CLAUDE_CODE_ENABLE_AUTO_MODE`, Opus-gated — the command can't toggle it; it advises and proceeds). +- **Sets the gear** to `review-after`, asking for the reason (and optionally a section scope) if + the spec is not already in that gear, so `/spec:gear` and `/spec:status` tell the truth mid-run. - Asks (via `AskUserQuestion`) which **run bound** to use — *unless* a `count` was passed, which sets the tasks bound directly: - **Tasks** — stop after N tasks complete (the `count` argument) @@ -294,24 +337,50 @@ Unattended TDD implementation from `ralph-tasks.md` via a **self-driving loop**. **Two models on purpose:** the **opus** orchestrator does only bookkeeping; each task's test + implementation is delegated to a **sonnet** sub-agent (cheaper, and kept off the opus loop -context). The sub-agent never commits, pushes, or edits the task files. +context). The sub-agent never commits, pushes, or edits the task list. + +**Task shapes.** The loop matches the task's leading label case-insensitively and treats synonyms +as one shape: + +| Shape | Labels | Commit | +|-------|--------|--------| +| Behavioural | `TEST + IMPLEMENT` | `feat:` / `fix:` | +| Test only | `TEST`, `TEST (RED)` | `test:` | +| Implementation only | `IMPLEMENT` | `feat:` — **only** when the specifying test already exists; otherwise skipped | +| Structural | `TIDY FIRST`, `TIDY`, `TIDY-FIRST`, `STRUCTURAL` | `refactor:` | +| Characterisation | `CHARACTERISE` | `test:` — RED observed under the task's named production mutation | +| Documentation | `DOC`, `DOCS`, `DOCUMENT` | `docs:` | +| Scaffolding | `SETUP` | `chore:` | +| Checkpoint | `VERIFY`, `VALIDATION` | bookkeeping tick only | -**Loop per task:** check `RALPH_STOP` → select next `- [ ]` task → delegate 🔴 Red → 🟢 Green -→ 🔵 Refactor to a sonnet sub-agent → orchestrator marks the checkbox and commits → check -continuation → repeat. Long runs can self-pace across context windows with `ScheduleWakeup`. +Anything else it marks `- [!]` and skips rather than improvising. + +**Loop per task:** check `RALPH_STOP` and the gear → select next in-scope `- [ ]` task → delegate +🔴 Red → 🟢 Green → 🔵 Refactor to a sonnet sub-agent → orchestrator commits the change, then +commits the checkbox tick → check continuation → repeat. Long runs can self-pace across context +windows with `ScheduleWakeup`. **Stop mechanisms:** - The chosen **bound** (tasks / turns / budget) +- `/spec:gear review-before` — **downshift**: the loop finishes the task in flight, then stops + cleanly with `DOWNSHIFTED`. Nothing completed is unwound; `/spec:implement` resumes from the next + unchecked task under the restored gate - `RALPH_STOP` file at repo root — the unattended kill-switch (`touch RALPH_STOP` from another terminal); halts after the current task - **Esc** — cancel a pending self-paced wake-up at the keyboard +- **Scope exhausted** — the next unchecked task falls outside the gear's `scope:` - Automatically stops when all tasks complete **Error handling:** Failed tasks are marked `- [!]` with an explanation and skipped. +**What it does not drop:** RED is still proved before any production code, the full regression +suite still runs, each task still produces the two-commit shape, and every test-authoring +convention still applies. `review-after` removes the human pause and nothing else. + **Requirements:** -- Design approved (`.design-approved`) -- `ralph-tasks.md` exists (run `/spec:ralph-tasks` first) +- Requirements, design **and tasks** approved (`.requirements-approved`, `.design-approved`, + `.tasks-approved`) +- All ADRs must be approved (Status: Accepted) - Recommended: session on **opus** with **auto mode** enabled --- @@ -323,22 +392,29 @@ per-action permission prompts and the self-driving loop (optionally self-paced w `ScheduleWakeup`) replaces an overnight runner. ```bash -# 1. Spec workflow up to an APPROVED DESIGN (no tasks step needed for this path) +# 1. The normal spec workflow, through to an APPROVED TASK LIST /spec:requirements 123 /spec:approve requirements /spec:design query-caching -/spec:approve design # <- prompts you to pick the unattended path - -# 2. Generate ralph-tasks directly from the approved design -/spec:ralph-tasks +/spec:approve design +/spec:tasks +/spec:review tasks +/spec:approve tasks -# 3. Review ralph-tasks.md in your IDE +# 2. Work the early, uncertain tasks attended — the gate is armed by default +/spec:implement -# 4. Switch to opus and enable auto mode, then run the loop +# 3. Once the task shape is settled, upshift and hand the rest to the loop +/spec:gear review-after "Phase 2 — Cache key generation (behaviour)" \ + --because "Phase 1's five tests were all approved unchanged" /model opus /spec:ralph-implement # choose tasks / turns / budget when prompted -# 5. Stop the loop (if needed) +# 4. Take back per-test review at any point, from another terminal +/spec:gear review-before # loop stops after the current task; nothing is unwound +/spec:implement # resumes from the next unchecked task, gated + +# 5. Or stop the loop outright touch RALPH_STOP # unattended kill-switch (halts after current task) # …or press Esc to cancel a pending self-paced wake-up ``` @@ -356,11 +432,11 @@ Darker/ │ ├── .issue-number # GitHub issue number │ ├── .requirements-approved # Approval marker │ ├── .design-approved # Approval marker -│ ├── .tasks-approved # Approval marker (attended path only) +│ ├── .tasks-approved # Approval marker │ ├── .adr-list # List of associated ADRs │ ├── requirements.md # User requirements -│ ├── tasks.md # Implementation tasks (attended path) -│ ├── ralph-tasks.md # Unattended TDD tasks (unattended path) +│ ├── tasks.md # Implementation tasks (the single task list) +│ ├── .current-gear # Review gear — GITIGNORED working state │ └── README.md # Spec overview ├── docs/ │ └── adr/ @@ -386,6 +462,10 @@ Darker/ - Break down into small, testable increments - Follow TDD: write tests before implementation - Identify dependencies between tasks +- Give sections meaningful, stable headings — `/spec:gear` can scope a gear to a single section, + and a well-named section is what makes a narrow, self-expiring gear shift possible +- Do **not** try to encode the gear in `tasks.md`; it is frozen by `.tasks-approved`, and the gear + lives in the untracked `.current-gear` file so it can still be shifted afterwards ### Git Workflow 1. Create feature branch diff --git a/.claude/commands/spec/approve.md b/.claude/commands/spec/approve.md index 96fae72b..7f362c25 100644 --- a/.claude/commands/spec/approve.md +++ b/.claude/commands/spec/approve.md @@ -44,33 +44,38 @@ Parse $ARGUMENTS to extract phase and optional ADR number: 5. Create approval marker: `touch specs/{current-spec}/.design-approved` 6. Show user which ADRs were approved 7. Remind the user to commit the approved ADRs to git. -8. **Choose the implementation path (the certainty fork).** If more ADRs are still - needed, tell the user to run `/spec:design [another-focus-area]` first and stop here. - Otherwise the design is complete and there are **two** ways to break the work down — use - `AskUserQuestion` to let the user pick: - - - **Attended — review each test** (`/spec:tasks` → `/spec:approve tasks` → - `/spec:implement`): generates `tasks.md`, then a strict Red → **user approval** → Green - → Refactor loop in the main agent on **sonnet**. Use this when the work is uncertain and - each test should be reviewed in the IDE before implementation. - - **Unattended — review in batches** (`/spec:ralph-tasks` → `/spec:ralph-implement`): - generates `ralph-tasks.md` directly from this approved design (no `tasks.md` step, no - per-test approval gates), then a self-driving loop on **opus** (auto mode) that delegates - each task to a **sonnet** sub-agent. Use this when the work is well-understood and can be - reviewed after a batch of tasks rather than at every test. - - This is "choose by certainty": some work warrants reviewing every test; other work can be - reviewed at the end of a batch. Present both, let the user choose, and point them at the - first command of the path they pick. Do **not** run that command yourself — just direct - them to it. +8. **Point at the task breakdown.** If more ADRs are still needed, tell the user to run + `/spec:design [another-focus-area]` first and stop here. Otherwise the design is complete: + the next step is `/spec:tasks` → `/spec:review tasks` → `/spec:approve tasks`. + + There is **one** task list (`tasks.md`) and **one** route to it. What used to be a fork in + the artifacts is now a **gear** you select at implementation time, and can change as you go + (see [`gear.md`](gear.md)) — so there is nothing to choose here. Mention both drivers so the + user knows what is coming, but do not ask them to commit to one: + + - `/spec:implement` — one task at a time, on **sonnet**. Red → **user approval** → Green → + Refactor. The approval gate is armed by default (`review-before`). + - `/spec:ralph-implement` — a self-driving unattended loop over the **same** `tasks.md`, on + **opus** with auto mode, delegating each task to a **sonnet** sub-agent. Always + `review-after`. + + Shift gear with `/spec:gear` at any point, in either direction, without regenerating or + abandoning anything. Do **not** run these commands yourself — just direct the user to + `/spec:tasks`. ### For "tasks" Phase 1. Verify `tasks.md` exists in the spec directory 2. Create approval marker: `touch specs/{current-spec}/.tasks-approved` 3. Inform user about next steps: - - Next: Begin implementation using `/spec:implement` - - Follow TDD: Write tests first, then code + - Next: begin implementation with `/spec:implement` (one task at a time, approval gate armed), + or `/spec:ralph-implement` (unattended loop over the same list, `review-after` gear) + - Follow TDD: write tests first, then code + - The gate is armed by default; `/spec:gear` shifts it, scoped to this spec and optionally to + one section of `tasks.md` +4. Note that `.tasks-approved` freezes the **content** of `tasks.md`. Checkbox state (`[ ]` → + `[x]`/`[!]`) is progress bookkeeping and both drivers write it; rewording, adding, removing or + reordering tasks after this point needs a fresh review. ### Invalid Phase diff --git a/.claude/commands/spec/gear.md b/.claude/commands/spec/gear.md new file mode 100644 index 00000000..cb65867a --- /dev/null +++ b/.claude/commands/spec/gear.md @@ -0,0 +1,191 @@ +--- +allowed-tools: Bash(cat:*), Bash(test:*), Bash(ls:*), Bash(echo:*), Bash(date:*), Bash(grep:*), Bash(rm:*), Read, Write, Edit, Glob, AskUserQuestion +description: Report or shift the TDD review gear for the current spec +argument-hint: [review-before|review-after] ["section scope"] [--because "reason"] +--- + +## Context + +Current spec: !`cat specs/.current-spec 2>/dev/null || echo "(none — run /spec:switch or /spec:new)"` + +**The gear** is which TDD review mode the current spec is being run in. It is *operational* state — +you shift it as the work goes, adjusting for how certain you are and how large the blast radius is. +Early in a spec, when the design is still moving, you want every test reviewed. Later, on a run of +near-identical tasks whose shape has been approved six times already, that pause is friction. Shift +up. The moment the work starts producing tests you would not have approved, shift back down. + +| Gear | Gate | Meaning | +|------|------|---------| +| `review-before` | ✅ armed | The user reviews each test in their IDE **before** implementation. **The default.** | +| `review-after` | ➖ not armed | The user reviews a **batch** of completed work after the fact. | + +This mechanism is shared with Brighter — see Brighter's +[ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md) +for why it exists and why it lives where it does. + +## The gear file + +`specs/{current-spec}/.current-gear` — **gitignored**. It is where the work currently *is*, not what +the project decided, so it is never committed. + +``` +# Working state — untracked. Shift with /spec:gear. +gear: review-after +scope: Phase 2 — Cache key generation (behaviour) +driver: ralph +shifted: 2026-10-07 — Phase 1's five tests were all approved unchanged; the shape is settled +``` + +| Field | Required | Meaning | +|-------|----------|---------| +| `gear` | yes | `review-before` \| `review-after` | +| `scope` | no | A **section** of `tasks.md` the gear applies to — either a heading's text (any depth: `##`–`####`, a phase, a task group, whatever the file uses) or a task range, `tasks N-M`, for flat lists with no useful headings. Tasks outside it fall back to `review-before`. Absent = the whole spec. | +| `driver` | no | `implement` \| `ralph` — which command is expected to be driving. | +| `shifted` | yes | Date + a one-line reason. A gear change is a judgement about certainty; the reason is the interesting part. | + +## Resolution rules (the canonical algorithm) + +Every command that honours the gear resolves it **this way, in this order**. Copy this, do not +improvise: + +1. No `specs/.current-spec`, or no `.current-gear` in that spec directory → **`review-before`**. +2. The file exists but cannot be parsed, or `gear:` holds anything other than the two known + values → **`review-before`**, and **say so out loud**. Fail safe, never fail open. +3. `scope:` is present and the task about to be worked falls **outside** it → **`review-before`**. + See *Reading a `scope:`* below — `tasks.md` files are not uniformly structured, so resolve the + scope against what the file actually contains rather than assuming it has phases. +4. Otherwise → the gear named in the file. + +## Reading a `scope:` + +`tasks.md` is **not** uniformly structured across this repository, so do not assume a phase layout. +The task lists in `specs/` variously use `## Phase N — …`, `## Task N — …`, a single flat `## Tasks` +section with `### …` subsections, or named groups such as `## Structural Tasks (Tidy-First — Batch)`. +Heading depth varies from `##` to `####`. + +A `scope:` value is therefore matched in this order: + +1. **`tasks N-M`** (e.g. `tasks 6-11`) — an inclusive range over the task numbering the file itself + uses. Use this for flat lists. This is the only form that works when a file has no useful + headings at all. +2. **A heading's text** — match it against every markdown heading in `tasks.md` at any depth, + comparing the heading text with the leading `#`s, any leading number, and surrounding whitespace + stripped. The scope covers tasks from that heading until the next heading **of the same or + shallower depth**. + +If a `scope:` matches **nothing** in `tasks.md` — a renamed heading, a typo, a range past the end of +the list — that is resolution rule 2: treat the gear as **`review-before`**, and say the scope did +not match. A scope that silently matches nothing must never read as "no scope, so spec-wide"; that +would widen the gear at the exact moment it stopped being understood. + +## Who honours the gear + +| Caller | Behaviour | +|--------|-----------| +| `/spec:implement` | Resolves the gear per task and runs gated or ungated accordingly. | +| `/spec:ralph-implement` | Always `review-after`; re-reads the gear before **each** task, so a downshift stops the loop. | +| `/test-first` standalone | **Always gated.** It does not read this file. | +| `/bugfix:test` | **Always gated.** Bugfixes have their own Confirm gate; they are outside this mechanism. | + +A standalone `/test-first` must not inherit a mode it did not ask for from whichever spec happens to +be current. The spec commands resolve the gear and pass the result *into* the `/test-first` workflow +explicitly — the caller opts in; the shared skill stays safe by default. + +## Your Task + +Parse `$ARGUMENTS`: + +- **empty** → report (below) +- **`review-before`** → downshift +- **`review-after`** → upshift +- an optional quoted **section scope** as the second positional argument +- an optional **`--because "reason"`** + +### Report (no arguments) + +1. Read `specs/.current-spec`. If absent, say so and stop. +2. Resolve the gear with the rules above and print: + +``` +Spec: specs/014-Caching-Decorator/ +Gear: review-after (➖ approval gate NOT armed) +Scope: Phase 2 — Cache key generation (behaviour) (tasks outside this section run review-before) +Driver: ralph +Since: 2026-10-07 — Phase 1's five tests were all approved unchanged; the shape is settled + +Shift down with: /spec:gear review-before +``` + +If there is no gear file, report the default plainly — `Gear: review-before (✅ approval gate armed — +default; no gear file)` — and do **not** create one. Absence is a valid, correct state. + +### Upshift — `/spec:gear review-after ["section"] [--because "..."]` + +1. Verify `specs/.current-spec` exists and that spec directory is present. If not, stop. +2. **Require a reason.** If `--because` was not given, ask for one with `AskUserQuestion` — offer + the common cases (e.g. "the section's task shape has been reviewed and accepted repeatedly", + "these are mechanical, one-acceptance-criterion tasks", "re-running a section that was already + reviewed") plus free text. Do not write the file without a reason; an unexplained ungated run is + exactly the invisible waiver this mechanism replaces. +3. **Encourage a scope.** If none was given, read `tasks.md` and offer what that file actually + provides — its section headings if it has meaningful ones, otherwise a task-number range — + and ask whether to scope the gear or leave it spec-wide. A scoped gear expires naturally as + its section completes; a spec-wide one does not. Do not insist: a genuinely flat, short task + list is fine to run spec-wide. +4. Write `specs/{current-spec}/.current-gear` with the fields above, stamping `shifted:` with + today's date and the reason. +5. Confirm what changed, and state the non-negotiables (below) so it is on the record that the + pause is the *only* thing being removed. + +### Downshift — `/spec:gear review-before` + +1. Write the file with `gear: review-before` (keep the `scope:`/`driver:` if present, restamp + `shifted:` with the reason), or simply delete the file — both resolve to the default. Prefer + **rewriting** it when a loop may be running, so the running loop reads an explicit + `review-before` rather than racing a deletion. +2. Tell the user it takes effect at the **next task**: + - `/spec:implement` picks it up on its next task. + - A running `/spec:ralph-implement` finishes the task in flight, then stops cleanly and hands + back. **Nothing already completed is unwound or redone.** + +### Record it in `PROMPT.md` + +Because the gear file is untracked, a fresh session will not learn of it from the diff. Whenever the +gear is set to `review-after`, add or update a line in `PROMPT.md` (gitignored; the project's place +for session state that must survive a context reset) pointing at it: + +```markdown +## Review gear +`specs/014-…/.current-gear` → **review-after**, scoped to Phase 2. Downshift with `/spec:gear review-before`. +``` + +Remove that line on downshift. If `PROMPT.md` does not exist, offer to create it rather than +creating it unasked. + +## What a gear change does NOT touch + +Shifting to `review-after` removes the **human approval pause** and nothing else. State these +explicitly whenever you shift: + +- **RED first.** The test is written and observed to fail *for the right reason* before any + production code exists. Ungated is not test-after. +- **The full regression suite**, not just the new test's own `--filter`. +- **The two-commit shape**: a `feat:`/`test:` commit for the behaviour, then a separate `docs:` + commit ticking the task off in `tasks.md`. +- **Every standing test-authoring convention** in + [.agent_instructions/testing.md](../../../.agent_instructions/testing.md): + `When_[condition]_should_[behavior]` method and file naming, `[Behavior]Tests` class naming; one + test per file; Arrange/Act/Assert; test doubles in `test/Paramore.Darker.Tests/TestDoubles/`, one + class per file; Real > Simple > InMemory > Mock; no mocks for isolation; public exports only. + +If a run in `review-after` is dropping any of these, that is a defect in the run, not a consequence +of the gear. + +## Examples + +```bash +/spec:gear # what gear am I in, and why? +/spec:gear review-after "Phase 2 — Cache key generation (behaviour)" \ + --because "Phase 1's five tests were all approved unchanged; the shape is settled" +/spec:gear review-before --because "the last two tests asserted the wrong thing" +``` diff --git a/.claude/commands/spec/implement.md b/.claude/commands/spec/implement.md index 6a594e8a..1e0327e5 100644 --- a/.claude/commands/spec/implement.md +++ b/.claude/commands/spec/implement.md @@ -1,7 +1,7 @@ --- allowed-tools: Bash(cat:*), Bash(test:*), Bash(ls:*), Bash(echo:*), Bash(dotnet:*), Bash(git:*), Read, Write, Edit, Glob, Grep, AskUserQuestion description: Start TDD implementation from approved tasks -argument-hint: [task-number] +argument-hint: [task-number] [--review-before|--review-after] --- ## Context @@ -11,6 +11,7 @@ Current spec directory: specs/ **Workflow**: Issue -> Requirements -> ADR(s) -> Tasks -> **Tests -> Code** **TDD Cycle**: RED -> User Approval -> GREEN -> REFACTOR +(the approval step is armed or not according to the **review gear** — see Step 1a) > **Recommended model: `sonnet`.** Unlike the unattended `/spec:ralph-implement`, > `/spec:implement` does its work in the **main agent** (the interactive approval gate must @@ -25,6 +26,24 @@ Current spec directory: specs/ - **Testing**: [.agent_instructions/testing.md](../../../.agent_instructions/testing.md) - **Code Style**: [.agent_instructions/code_style.md](../../../.agent_instructions/code_style.md) +## The review gear + +Whether the approval gate fires is the **gear** (full reference in [`gear.md`](gear.md); rationale +in Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)): + +| Gear | Gate | Meaning | +|------|------|---------| +| `review-before` | ✅ armed | Stop after each test and get approval in the IDE before implementing. **The default.** | +| `review-after` | ➖ not armed | Prove RED, then implement without pausing. Reviewed as a batch afterwards. | + +The gear is held in `specs/{current-spec}/.current-gear` — **gitignored** working state, per spec, +optionally scoped to one section of `tasks.md`. It is shifted with `/spec:gear`, or with the +`--review-before` / `--review-after` flags on this command, which write the same file. + +**`review-after` removes the approval pause and nothing else.** RED-first, the full regression suite, +the two-commit shape, and every test-authoring convention hold in both gears. Do not let a gear shift +quietly take anything else with it. + ## Your Task ### Step 0: Confirm the Session Model @@ -49,7 +68,45 @@ This is a one-time check at the start of the command. 3. Read `specs/{current-spec}/tasks.md` to see task list 4. Read `specs/{current-spec}/.adr-list` to see all ADRs 5. Read ADRs from `docs/adr/` to understand design decisions -6. If task number provided in $ARGUMENTS, focus on that task only +6. Parse `$ARGUMENTS`: an optional task number, and an optional `--review-before` / + `--review-after` flag. If a task number was given, focus on that task only. + +### Step 1a: Resolve the Review Gear + +**Do this before writing anything, and re-resolve it before every task** (the gear can be shifted +from another terminal mid-run). + +**If a flag was passed**, shift the gear first — this is sugar for `/spec:gear`, and writes the same +file: + +- `--review-after` requires a reason. If the user did not give one in their message, ask for one + with `AskUserQuestion` and offer to scope it to a section of `tasks.md` (a heading's text, or a + `tasks N-M` range for a flat list). Then write + `specs/{current-spec}/.current-gear` with `gear: review-after`, the optional `scope:`, + `driver: implement`, and a `shifted:` line carrying today's date and the reason. +- `--review-before` writes `gear: review-before` with a `shifted:` line (or deletes the file). + +**Then resolve**, using exactly this order (the canonical algorithm, also in +[`gear.md`](gear.md) — do not improvise): + +1. No `.current-gear` in the spec directory → **`review-before`**. +2. File present but unparseable, or `gear:` is not one of the two known values → + **`review-before`**, and say so out loud. Fail safe, never fail open. +3. `scope:` present and the selected task falls **outside** it → **`review-before`**. A `scope:` + that matches nothing in `tasks.md` (renamed heading, range past the end) is rule 2, not "no + scope" — resolve to `review-before` and say the scope did not match. See [`gear.md`](gear.md) → + *Reading a `scope:`*; `tasks.md` files are not uniformly structured, so do not assume phases. +4. Otherwise → the gear named in the file. + +**Announce the resolved gear** in one line before starting, e.g. + +``` +Gear: ➖ review-after (specs/014-…/.current-gear, scoped to "Phase 2 — Cache key generation (behaviour)") +Since: 2026-10-07 — Phase 1's five tests were all approved unchanged; the shape is settled +Shift down at any time with /spec:gear review-before — it takes effect at the next task. +``` + +or, in the default case, `Gear: ✅ review-before (default — approval gate armed)`. ### Step 2: Verify Prerequisites @@ -89,15 +146,30 @@ For each task, follow this strict workflow: 4. **Create/Update Test File**: Use Write or Edit tool to create the test 5. **Run the Test**: Use Bash to run: `dotnet test test/Paramore.Darker.Tests/ --filter "FullyQualifiedName~When_[test_name]"` - - Verify the test FAILS (Red) - -6. **Show Test to User** + - Verify the test FAILS (Red), for the expected reason (behavior doesn't exist yet) + - **`CHARACTERISE` task, test passes on first run**: an earlier task already delivers the + behaviour — that is expected, not a reason to rewrite the test. Apply the task's **named RED + mutation** (a temporary change to *production* code, never to the test), confirm the test fails + **on the assertion the task names**, **revert the mutation**, and confirm green again. That is + RED observed. No named mutation, or a mutation that fails some other way, is a stop-and-ask. + A `CHARACTERISE` test that fails on first run is simply RED — continue as normal. + +6. **Show Test to User**: the test code, the behavior it tests, and the failure output — for a + characterisation test, the green-on-arrival run **and** the failure observed under the mutation, + with the mutation named and confirmed reverted #### USER APPROVAL: Get Approval for Test +**This step runs when the resolved gear is `review-before`** (the default). Re-check the gear here — +do not rely on a value resolved several tasks ago. + **CRITICAL**: Before writing any implementation code, you MUST: 1. Use AskUserQuestion tool to ask: "I've written a failing test for [behavior]. Should I proceed to make this test pass?" + For a characterisation test (green on arrival, RED observed via mutation) there is nothing to + implement: ask instead "I've written a characterisation test for [behavior]; it went red under + [mutation] and the mutation is reverted. Should I commit it?". On approval, run the **full test + suite**, then go to Step 5. 2. Wait for user approval @@ -108,6 +180,22 @@ For each task, follow this strict workflow: **DO NOT proceed to implementation without explicit user approval of the test.** +##### When the gear is `review-after` + +Skip the pause — and **only** the pause: + +1. Confirm RED is already proved: the test ran and failed **for the right reason** — on arrival, + or, for a `CHARACTERISE` task, under its named mutation (reverted). If it has not, go back and + prove it. `review-after` never licenses writing implementation first. +2. Print one line recording that the pause was skipped and why, e.g. + `➖ review-after (Phase 2) — proceeding to GREEN without pausing`. +3. Proceed to GREEN — or, for a characterisation test with nothing to implement, run the full + suite and go to Step 5. + +**Stop and ask anyway** if the test asserts something the task did not ask for, needs a design +decision you cannot make from the ADRs, or duplicates an existing test. `review-after` is a default +for the routine case, not a gag order. + #### GREEN: Make the Test Pass 1. **Read Code Style Guidelines**: Review [.agent_instructions/code_style.md](../../../.agent_instructions/code_style.md) @@ -135,26 +223,73 @@ For each task, follow this strict workflow: 2. **Apply "Tidy First" Principles**: Structural changes only, no behavioral changes 3. **Run All Tests After Each Refactoring**: Verify no behavioral changes -### Step 5: Commit the Change +### Step 5: Commit the Change — Two Commits + +After completing Red-Green-Refactor for a behavior. **The two-commit shape is required in both +gears** — the behaviour and the bookkeeping are separate concerns, and a reviewer reading the +history should see the behaviour commit without a task-list tick mixed into it. -After completing Red-Green-Refactor for a behavior: +1. **Commit one — the behaviour.** Stage only the test and implementation files: + ```bash + git add [test-file] [implementation-files] + git commit -m "feat: [behavior description] -1. **Stage Changes**: `git add [test-file] [implementation-files]` -2. **Commit with Descriptive Message** -3. **Update Tasks**: Use Edit tool to check off completed task in `specs/{current-spec}/tasks.md` + - Test: When_[condition]_should_[expected_behavior] + - Implementation: [brief description]" + ``` + Use `test:` instead of `feat:` when the task added only a test — including a characterisation + test — and `fix:` for a bug fix. For a characterisation test, add a + `- Characterisation — RED observed via: [mutation]` line. **Before staging, run + `git status --porcelain`** and confirm no production file is still modified by a mutation. + +2. **Commit two — the bookkeeping.** Use Edit to check off the completed task in + `specs/{current-spec}/tasks.md`, then commit that **alone**: + ```bash + git add specs/{current-spec}/tasks.md + git commit -m "docs: mark task [N] complete" + ``` + +End both commit messages with the session's co-author attribution line. Never stage +`.current-gear` — it is gitignored working state, not part of the change. ### Step 6: Continue to Next Behavior -Ask user: "This behavior is complete. Should I continue to the next test, or would you like to review?" +**Re-resolve the gear (Step 1a) before the next task.** The user may have shifted it from another +terminal while you were working, and a downshift must take effect at the very next task. + +- **Gear `review-before`**: ask the user "This behavior is complete. Should I continue to the next + test, or would you like to review?" — if continue, return to Step 4; if review, show progress and + wait. +- **Gear `review-after`**: continue straight to the next task under the gear's scope without asking. + Stop and hand back when any of these holds: + - the gear has been shifted to `review-before` (say so, and say which task you stopped at), + - the next task falls outside the gear's `scope:`, + - no unchecked tasks remain, + - a task failed and you could not resolve it. + + For a long unattended run over many tasks, `/spec:ralph-implement` is the better driver — it is + the same `tasks.md`, with a run bound, a kill-switch and per-task sub-agents. ## Important Reminders ### Test-First Requirements -- **NEVER write implementation before writing a failing test** -- **ALWAYS get user approval of the test before implementing** +- **NEVER write implementation before writing a failing test** — in either gear +- **Get user approval of the test before implementing whenever the gear is `review-before`** (the + default). Only a deliberate, scoped, reasoned `review-after` gear removes that pause, and it + removes nothing else - Each test should represent the smallest possible behavioral step +### What `review-after` does NOT remove + +- **RED first** — the test is observed failing for the right reason before any production code +- **The full regression suite** — `dotnet test Darker.Filter.slnf`, not just the new test's `--filter` +- **The two-commit shape** — `feat:`/`test:` for the behaviour, then a separate `docs:` for the + task-list tick +- **The test-authoring conventions** — `When_[condition]_should_[behavior]` naming; one test per + file; test doubles in `test/Paramore.Darker.Tests/TestDoubles/`, one class per file; + Real > Simple > InMemory > Mock; no mocks for isolation + ### Code Quality Requirements - Follow ALL guidelines in .agent_instructions/testing.md diff --git a/.claude/commands/spec/ralph-implement.md b/.claude/commands/spec/ralph-implement.md index 70c2a1d6..6dc5c3c1 100644 --- a/.claude/commands/spec/ralph-implement.md +++ b/.claude/commands/spec/ralph-implement.md @@ -1,6 +1,6 @@ --- -allowed-tools: Bash(cat:*), Bash(test:*), Bash(ls:*), Bash(echo:*), Bash(dotnet:*), Bash(git:*), Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, ScheduleWakeup -description: Unattended TDD implementation from ralph-tasks (auto mode + self-driving loop) +allowed-tools: Bash(cat:*), Bash(test:*), Bash(ls:*), Bash(echo:*), Bash(date:*), Bash(grep:*), Bash(dotnet:*), Bash(git:*), Read, Write, Edit, Glob, Grep, Agent, AskUserQuestion, ScheduleWakeup +description: Unattended TDD implementation from the approved tasks.md (review-after gear + self-driving loop) argument-hint: [count] --- @@ -8,11 +8,28 @@ argument-hint: [count] Current spec directory: specs/ -**Workflow**: Unattended TDD implementation — no per-test approval gates. A self-driving loop -processes ralph tasks one after another until a user-chosen bound, a STOP signal, or all tasks -are done. +**Workflow**: Unattended TDD implementation from the spec's **approved `tasks.md`** — the same task +list `/spec:implement` works from, in the same format. A self-driving loop processes unchecked tasks +one after another until a user-chosen bound, a STOP signal, a downshift of the review gear, or all +tasks are done. -**TDD Cycle**: 🔴 Red → 🟢 Green → 🔵 Refactor (no approval step) +**TDD Cycle**: 🔴 Red → 🟢 Green → 🔵 Refactor (no approval pause) + +### This command is the `review-after` driver + +There is one task-list format (`tasks.md`) and one gate-state mechanism (the **review gear**, see +[`gear.md`](gear.md) and Brighter's +[ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)). +This command does not have its own format and does not regenerate anything — it is simply the +driver that runs `tasks.md` in the `review-after` gear, in a loop, unattended. + +- `/spec:implement` — one task at a time, honours whichever gear is set. +- `/spec:ralph-implement` — a loop, **always `review-after`**, and it writes that gear so the state + on disk matches what is actually happening. + +**`review-after` removes the human approval pause and nothing else.** RED-first, the full regression +suite, the two-commit shape, and every test-authoring convention hold here exactly as they do in +`/spec:implement`. See *What this loop must never drop*, below. **Runtime model — Opus orchestrator + auto mode (advisory).** This command is designed to run **unattended**: @@ -28,25 +45,43 @@ are done. but still run. Do not hard-block. **AskUserQuestion — setup only.** This command may use `AskUserQuestion` **only** in the -up-front setup (Step 0): the auto-mode/Opus advisory and choosing the run bound. Once the loop -starts it is fully unattended — **never** prompt the user mid-loop. +up-front setup (Step 0): the auto-mode/Opus advisory, the gear reason, and choosing the run bound. +Once the loop starts it is fully unattended — **never** prompt the user mid-loop. **Sub-agent (cost control): Sonnet.** Each task's Red→Green→Refactor cycle is delegated to a sub-agent (`subagent_type: "general-purpose"`, **`model: "sonnet"`**). The orchestrator stays on opus for cheap bookkeeping; the expensive per-task implementation churn runs on the cheaper **sonnet** sub-agent and in its own context, keeping it out of the loop's opus context. The sub-agent writes the test + implementation files and RETURNS a structured result. The MAIN -agent owns everything that must stay sequential and authoritative: the STOP-file check, task -selection, the run count, marking the checkbox, **the git commit**, and the summary. The -sub-agent NEVER commits, NEVER pushes, and NEVER edits `ralph-tasks.md`/`tasks.md`. See +agent owns everything that must stay sequential and authoritative: the STOP-file check, the gear +check, task selection, the run count, marking the checkbox, **the git commits**, and the summary. +The sub-agent NEVER commits, NEVER pushes, and NEVER edits `tasks.md`. See `.claude/commands/spec/README.md` → "Sub-agents & model policy". Tasks are processed **strictly sequentially** — they are dependency-ordered and each gets -its own commit. Do not parallelise. +its own commits. Do not parallelise. + +## What this loop must never drop + +Running unattended changes *who reviews when*. It changes nothing else. Every task in this loop: + +- **Proves RED first** — the test is run and observed to fail *for the right reason* before any + production code is written. A task whose test passes on first run is `ALREADY_COMPLETE`, never a + licence to write the implementation anyway — **except** a `CHARACTERISE` task, whose RED is + observed through the task's named production mutation instead (see the dispatch table). A + characterisation test is never `ALREADY_COMPLETE`, never dropped and never rewritten. +- **Runs the full regression suite** (`dotnet test Darker.Filter.slnf`, or at least the full suite + of every affected test project), not just the new test's own `--filter`. +- **Produces two commits** — a `feat:`/`test:`/`fix:`/`refactor:` commit for the change, then a + separate `docs:` commit ticking the task off in `tasks.md`. +- **Obeys every standing test-authoring convention** in `.agent_instructions/testing.md`: + `When_[condition]_should_[behavior]` method and file naming, `[Behavior]Tests` class naming; one + test per file; Arrange/Act/Assert; test doubles in `test/Paramore.Darker.Tests/TestDoubles/`, one + class per file; Real > Simple > InMemory > Mock; no mocks for isolation; public exports only. ## Your Task -### Step 0: Advise Runtime, Then Ask the Run Bound +### Step 0: Advise Runtime, Set the Gear, Ask the Run Bound This runs **once**, before the loop starts. @@ -55,12 +90,30 @@ This runs **once**, before the loop starts. auto mode appears off, warn that the loop will pause for permission prompts. Advise — do not block. -2. **Choose the bound.** The loop needs a stopping bound. If a numeric `count` was passed in +2. **Set the review gear.** This loop runs `review-after`, so make the state on disk say so — + otherwise a session that inspects `/spec:gear` mid-run gets a false answer. + - Read `specs/{current-spec}/.current-gear` if it exists. + - If it already reads `gear: review-after`, keep it and note its `scope:` — the loop will honour + that scope (see Step 3). + - Otherwise ask the user, with `AskUserQuestion`, for the **reason** for the upshift (and offer + to scope it to a section of `tasks.md` — a heading's text, or a `tasks N-M` range for a + flat list), then write the file: + ``` + # Working state — untracked. Shift with /spec:gear. + gear: review-after + scope: {heading text or `tasks N-M`, or omit for spec-wide} + driver: ralph + shifted: {today} — {reason} + ``` + - Add or update the pointer line in `PROMPT.md` so a fresh session finds the gear (see + [`gear.md`](gear.md) → *Record it in `PROMPT.md`*). + +3. **Choose the bound.** The loop needs a stopping bound. If a numeric `count` was passed in `$ARGUMENTS`, take that as the **tasks** bound and skip the prompt. Otherwise use `AskUserQuestion` to ask the user which bound to use and its value: - - **Tasks** — stop after **N tasks complete** this run (a task "completes" on `GREEN` or - `ALREADY_COMPLETE`; `FAILED` does not count). This is the `count` argument. + - **Tasks** — stop after **N tasks complete** this run (a task "completes" on `GREEN`, + `CHARACTERISED` or `ALREADY_COMPLETE`; `FAILED` does not count). This is the `count` argument. - **Turns** — stop after **N loop iterations** attempted, regardless of outcome (so a run of failures still terminates). Use when you want to cap effort, not completions. - **Budget** — stop when roughly **N output tokens** have been consumed by the run. Use the @@ -70,38 +123,78 @@ This runs **once**, before the loop starts. Record the chosen bound type and value. Default if the user gives nothing and no `count` was passed: **Tasks = 1**. -### Step 1: Check STOP File +### Step 1: Check the Stop Conditions -If a file named `RALPH_STOP` exists at the repository root: +Two independent ways to halt, both checked **before every task**: + +1. **STOP file** — a file named `RALPH_STOP` at the repository root. The unattended kill-switch: + `touch RALPH_STOP` from another terminal. +2. **Gear downshift** — `specs/{current-spec}/.current-gear` reads `gear: review-before` (or the + file has been deleted, or is unparseable). The user has decided the work needs per-test review + again. **This is not a failure.** Stop cleanly; nothing already completed is unwound or redone, + and `/spec:implement` picks up from the next unchecked task under the restored gate. + +If either holds: ``` === RALPH SUMMARY === Tasks completed this run: 0 Total tasks complete: X/Y Tasks remaining: Z -Status: STOPPED +Status: STOPPED | DOWNSHIFTED === END RALPH === ``` -Then STOP immediately. Do not proceed. +Then STOP immediately. On `DOWNSHIFTED`, also print the next unchecked task and the exact +`/spec:implement` command to resume with. ### Step 2: Gather Context 1. Read `specs/.current-spec` to determine the active specification directory -2. Verify `.design-approved` exists in that directory (the unattended path runs from the - approved design — there is **no** `.tasks-approved` prerequisite) -3. Verify `specs/{current-spec}/ralph-tasks.md` exists (run `/spec:ralph-tasks` first if not) - and read it to see the ralph task list -4. Read `specs/{current-spec}/.adr-list` to see all ADRs +2. Verify **all three** approval markers exist in that directory: `.requirements-approved`, + `.design-approved`, `.tasks-approved`. This loop runs an **approved** task list; if + `.tasks-approved` is missing, tell the user to run `/spec:review tasks` then + `/spec:approve tasks` first, and exit. +3. Read `specs/{current-spec}/tasks.md` — the task list. There is no `ralph-tasks.md`; do not + create one. (Older specs may still contain a `ralph-tasks.md` from the retired + `/spec:ralph-tasks` command; it is historical — do not run it.) +4. Read `specs/{current-spec}/.adr-list` — the ADR paths to hand to each sub-agent. The main agent does **not** read the ADR bodies here — that is deliberate. Its role in this -command is bookkeeping (task selection, commit, marking, count); the per-task TDD context -(including the relevant ADRs) is read by the **sub-agent** from each task's `References` -section. This keeps the main agent's context lean. - -### Step 3: Select Next Task - -Find the first unchecked `- [ ]` task in `ralph-tasks.md`. - -If all tasks are checked (`[x]` or `[!]`): +command is bookkeeping (task selection, commits, marking, count); the per-task TDD context +(including the relevant ADRs) is read by the **sub-agent** from the paths the main agent passes. +This keeps the main agent's context lean. + +### Step 3: Select the Next Task + +Scan `tasks.md` top to bottom for the first task that is: + +- unchecked (`- [ ]`) — skip `- [x]` (done) and `- [!]` (failed or skipped earlier), and +- **within the gear's `scope:`**, if one is set — see [`gear.md`](gear.md) → *Reading a `scope:`*. + If the next unchecked task falls outside the scope, the loop's work is finished: stop with + `Status: SCOPE_COMPLETE` and name the task it stopped before. + +#### Dispatch on the task's shape + +`tasks.md` is a general task list, and the labels in use across this repository vary. Match the +task's leading bold label **case-insensitively**, and treat the synonyms below as one shape — the +project's `/tidy-first` vocabulary and the `STRUCTURAL` label mean the same thing, and `DOC`, +`DOCS` and `DOCUMENT` likewise. Do not invent a new label; match what the file says. + +| Task shape | Labels | Handling | +|------------|--------|----------| +| **Behavioural** | `TEST + IMPLEMENT` | The normal case — Step 4's Red→Green→Refactor cycle. The task's `⛔`/`STOP HERE` line is a `review-before` instruction; under `review-after` it does not fire, and that is the *only* thing the gear changes about the task. | +| **Test only** | `TEST`, `TEST (RED)` | Write the test and prove it fails for the right reason. If the task's own text asks only for the test, stop there and commit as `test:`. If the very next task is the matching `IMPLEMENT`, do **not** run ahead into it — it is its own task and its own commit. | +| **Implementation only** | `IMPLEMENT` | Only run this when the test that specifies it already exists — i.e. the preceding `TEST` task is already `- [x]`, or the task names an existing failing test. Make that test pass, then the full suite. **If no such test exists, mark `- [!]`** with `RALPH-SKIPPED: IMPLEMENT task with no preceding test`. An unattended TDD loop must never write implementation that no test demanded. | +| **Structural** | `TIDY FIRST`, `TIDY`, `TIDY-FIRST`, `STRUCTURAL` | No new test. Require the **existing** suite green before *and* after. Commit as `refactor:`. Behaviour must not change; if a test's result changes, the refactoring was wrong — revert and mark `- [!]`. | +| **Characterisation** | `CHARACTERISE` | A test for behaviour an earlier task may already deliver. Every test file the task names must be **observed RED** — either on first run, or under the task's **named RED mutation**. For each test file that passes on first run: apply the named mutation (a temporary change to *production* code, never to the test), confirm the test fails **on the assertion the task names** (not a compile error or an unrelated exception), **revert the mutation**, confirm green. If a test file fails on first run, the behaviour is missing: continue as Behavioural for that file, using the task's implementation notes. **Never revise the test and never return `ALREADY_COMPLETE`.** If a green-on-arrival file has no named mutation, or the mutation does not produce the named failure, return `FAILED`. Returns `CHARACTERISED` when no production code was kept, `GREEN` otherwise. | +| **Documentation** | `DOC`, `DOCS`, `DOCUMENT` | No test. Delegate the edit; commit as `docs:`. That is the change commit for the task — the checkbox tick still gets its own second commit. | +| **Scaffolding** | `SETUP` | Project/config/package scaffolding. No test, but `dotnet build Darker.Filter.slnf` MUST succeed. Commit as `chore:`. | +| **Checkpoint** | `VERIFY`, `VALIDATION` | Run the checks the task names and report. A checkpoint asserts the state is already correct, so it should need **no** source change: if it passes, tick it with the `docs:` bookkeeping commit alone. If it fails, or would require a change to pass, mark `- [!]` with the failure — do not "fix" it here, because whatever it caught belongs to some other task. | +| **Anything else** | free prose, unlabelled, id-before-tag | Do **not** guess: mark `- [!]` with `RALPH-SKIPPED: unrecognised task shape, needs /spec:implement`, commit the marker, and continue. An unattended loop should skip what it does not understand, not improvise. | + +If you meet a label that is not listed, that is the "anything else" row — skip it and let a human +decide, rather than mapping it to the nearest row by guesswork. + +If no unchecked tasks remain: ``` === RALPH SUMMARY === Tasks completed this run: 0 @@ -112,65 +205,105 @@ Status: ALL_DONE ``` Then STOP. -### Step 4: Delegate the TDD Cycle to a Sub-Agent +### Step 4: Delegate the Cycle to a Sub-Agent + +**First, record the baseline** for Step 5's leftover-mutation check: +`git status --porcelain > {scratchpad}/ralph-baseline.txt`. Anything already modified or untracked +before the task is the baseline, not a leftover. Launch an `Agent` with `subagent_type: "general-purpose"` and **`model: "sonnet"`**. The prompt MUST include: -1. The full text of the selected task (Behavior, Test file, Test should verify, - Implementation files, RALPH-VERIFY command, References). -2. The paths `.agent_instructions/testing.md` and `.agent_instructions/code_style.md`, with - an instruction to read them before writing code. -3. The TDD cycle instructions, code-style rules, and hard constraints below. -4. The required return format below. +1. The **full text of the selected task** from `tasks.md`, verbatim, including the heading of the + section it sits under. +2. The context the task does not carry itself — because `tasks.md` tasks were written for an + interactive session, they assume conversation context a fresh sub-agent does not have. Pass: + - `specs/{current-spec}/requirements.md` + - every ADR path from `.adr-list` + - `.agent_instructions/testing.md` and `.agent_instructions/code_style.md` + with an instruction to **read them before writing code**. +3. The **verify command(s)** to use. If the task text states its own commands, pass those. + Otherwise `tasks.md` tasks name a test location and test file(s) but not a filter command — + derive **one per test file the task names** and state them explicitly: + `dotnet test {test project from the task's test location} --filter "FullyQualifiedName~{test method name}"` +4. The cycle instructions, code-style rules, and hard constraints below — and, for a + `CHARACTERISE` task, the **characterisation cycle** below, which takes precedence over the plain + RED rules where they differ. +5. The required return format below. The sub-agent runs unattended — it has full tool access (Read, Write, Edit, Glob, Grep, Bash) and DOES write the test and implementation source files. It just must not commit, -push, or touch the task files. +push, or touch the task list. #### TDD Cycle for the sub-agent (include in the prompt) -**Before starting**: Read ALL files listed in the task's **References** section, plus -`.agent_instructions/testing.md` and `.agent_instructions/code_style.md`. This provides the -context a fresh session would otherwise get from conversation. +**Before starting**: Read the requirements, the ADRs, `.agent_instructions/testing.md` and +`.agent_instructions/code_style.md`. This provides the context a fresh session would otherwise get +from conversation. 🔴 **RED — Write a Failing Test** - Test naming: `When_[condition]_should_[expected_behavior]`; one test case per file named - the same; Arrange/Act/Assert with explicit comments; highlight evident data. + the same; test classes named `[Behavior]Tests`; Arrange/Act/Assert with explicit comments; + highlight evident data. - Test behavior, not implementation — public exports only; prefer real/Simple/InMemory* test - doubles over mocks (Real > Simple > InMemory > Mock). + doubles over mocks (Real > Simple > InMemory > Mock); no mocks for isolation. +- Test-specific handlers, queries and decorators go in `test/Paramore.Darker.Tests/TestDoubles/` + (namespace `Paramore.Darker.Tests.TestDoubles`), one class per file. - Write the test file at the path the task specifies. -- Run the task's `RALPH-VERIFY` command and confirm the test **FAILS** for the right reason - (behavior doesn't exist yet). +- Run the verify command and confirm the test **FAILS** for the right reason (behavior doesn't + exist yet). **Do not skip this.** Running unattended does not make this test-after. - **If the test PASSES with no implementation change**: the behavior already exists. Either revise the test to verify something genuinely new, or RETURN status `ALREADY_COMPLETE`. + **Not for a `CHARACTERISE` task** — follow the characterisation cycle below instead. 🟢 **GREEN — Make the Test Pass** -- Write the MINIMUM code to pass — no speculative code. Follow the task's - **Implementation files** guidance. +- Write the MINIMUM code to pass — no speculative code. Follow the task's implementation notes. - Code style: .NET C# conventions (PascalCase public / camelCase private), ALL_CAPS constants, expression-bodied members for simple members, `readonly` where appropriate, nullable reference types enabled, Responsibility-Driven Design, avoid primitive obsession. -- Run the `RALPH-VERIFY` command and confirm the test **PASSES**. -- Run broader tests (`dotnet test Darker.Filter.slnf -c Release` or the relevant project[s]) - to catch regressions. If a regression appears, fix it before finishing. + XML documentation on new public members; MIT licence header on new files. +- Run the verify command and confirm the test **PASSES**. +- Run the **full suite** (`dotnet test Darker.Filter.slnf`, or at minimum the full suite of each + affected test project) to catch regressions. If a regression appears, fix it before finishing. 🔵 **REFACTOR — Improve the Design** - Tidy First: structural changes only, no behavior changes. Keep methods small and focused; reduce complexity; remove duplication; reveal intent. - Re-run tests after refactoring to confirm no behavioral change. +#### Characterisation cycle for the sub-agent (include for a `CHARACTERISE` task) + +Work **per test file** the task names: + +1. Write the test and run its verify command. +2. **Fails** → the behaviour is missing. Continue as the normal RED → GREEN → REFACTOR cycle for + that file, using the task's implementation notes. Record `"<file>: failed on arrival: <assertion>"`. +3. **Passes** → apply the task's **named RED mutation** (the 🔁 bullet) to production code, never to + the test. Run the verify command: it must fail with the failure the task names. Revert the + mutation exactly, run the verify command again and confirm green. Record + `"<file>: mutation <what> → failed on <failure>; reverted; re-run green"`. +4. No named mutation, or the mutation does not produce the named failure → RETURN `FAILED`. Never + revise the test to make it fail, and never return `ALREADY_COMPLETE`. +5. Run the **full suite**. +6. RETURN `CHARACTERISED` if no production code was kept (`IMPL_FILES` empty), `GREEN` if any file + needed an implementation (the mixed case), and in both cases fill `RED_EVIDENCE` for **every** + test file. + #### Hard constraints for the sub-agent (include in the prompt) - **NEVER** run `git commit`, `git add`, or `git push`. -- **NEVER** edit `ralph-tasks.md` or `tasks.md`. -- Only create/modify the test file(s) and implementation source file(s). -- Do not ask the user anything — this is unattended. +- **NEVER** edit `tasks.md` or `.current-gear`. +- Only create/modify the test file(s) and implementation source file(s). A RED mutation is + reverted before you return. +- Do not ask the user anything — this is unattended. Where `testing.md` or the task says + "stop and ask", RETURN `FAILED` with the question as the reason. +- If the task cannot be done as written (it contradicts an ADR, depends on something absent, or + needs a design decision), RETURN `FAILED` with the reason. Do not improvise a different task. #### Required return format (the sub-agent RETURNS this as text) ``` -STATUS: GREEN | FAILED | ALREADY_COMPLETE +STATUS: GREEN | FAILED | ALREADY_COMPLETE | CHARACTERISED TEST_FILES: <one path per line, indented; empty if none> IMPL_FILES: @@ -178,6 +311,7 @@ IMPL_FILES: DESCRIPTION: <one-line behavior description for the commit message> REGRESSIONS: <none | description of any regression and how it was resolved> FAILURE_REASON: <empty unless STATUS is FAILED — explain what went wrong> +RED_EVIDENCE: <for each test file: "failed on arrival: <assertion>" or "mutation <what> → failed on <assertion>; reverted"> ``` Paths MUST be **one per line** (so they tokenise unambiguously regardless of spaces in a @@ -194,54 +328,91 @@ space-separated line. `git add` / `git checkout --` with no paths: `git checkout --` with no positional args errors ("Nothing specified"), and a blanket form would touch unrelated working-tree files. -### Step 5: Process the Result, Mark, and Commit (MAIN agent) +### Step 5: Process the Result, Commit, and Mark (MAIN agent) + +Read the sub-agent's returned result and act on its `STATUS`. **Every outcome produces the +two-commit shape**: the change, then the bookkeeping. + +**Before any outcome's commits — leftover-mutation check.** Run `git status --porcelain` and +compare it with the Step 4 baseline. Any entry that is new since the baseline and lies outside +`TEST_FILES` ∪ `IMPL_FILES` (and is not `tasks.md`) means a mutation or scratch edit was not +reverted: do **not** commit; restore it with `git checkout -- <path>` only if it is a tracked +production file the sub-agent reported mutating in `RED_EVIDENCE`, otherwise mark the task `- [!]` +with the reason. The check cannot see a mutation left in a file that is *also* in `IMPL_FILES`; +there, rely on `RED_EVIDENCE`'s "reverted; re-run green" and the full-suite run, and treat +`RED_EVIDENCE` missing either as a contract violation. -Read the sub-agent's returned result and act on its `STATUS`: +**CHARACTERISED:** +1. `IMPL_FILES` must be empty and `TEST_FILES` non-empty; otherwise treat as a contract violation + and send it back. +2. **Commit one — the test:** + ```bash + git add [TEST_FILES] + git commit -m "test: [DESCRIPTION] + + - Test: When_[condition]_should_[expected_behavior] + - Characterisation — RED observed via: [RED_EVIDENCE] + - Task: [task number]/[total] ([section heading]) + + Co-Authored-By: Claude Opus <noreply@anthropic.com> + Co-Authored-By: Claude Sonnet <noreply@anthropic.com>" + ``` +3. **Commit two — the bookkeeping**, as for GREEN. +4. Count this task toward the run count. **GREEN:** 1. **Sanity-check the file lists first.** A `GREEN` result with **empty** `TEST_FILES` *and* `IMPL_FILES` is a contract violation — a passing task should have written source. Do NOT commit a `feat:` with no source changes. Send it back to the sub-agent and ask whether it - meant `ALREADY_COMPLETE` (behavior already existed) before proceeding. -2. Mark the task complete: use Edit to change `- [ ]` to `- [x]` in `ralph-tasks.md`. -3. Stage and commit (the MAIN agent owns this): + meant `ALREADY_COMPLETE` (behavior already existed) before proceeding. (A `DOC` task's + documentation files count as `IMPL_FILES`.) +2. **Commit one — the change:** ```bash - git add [TEST_FILES] [IMPL_FILES] specs/{current-spec}/ralph-tasks.md + git add [TEST_FILES] [IMPL_FILES] git commit -m "feat: [DESCRIPTION] - Test: When_[condition]_should_[expected_behavior] - Implementation: [brief description] - - Ralph task: [task number]/[total] + - Characterisation — RED observed via: [RED_EVIDENCE] (only when RED_EVIDENCE names a mutation) + - Task: [task number]/[total] ([section heading]) Co-Authored-By: Claude Opus <noreply@anthropic.com> Co-Authored-By: Claude Sonnet <noreply@anthropic.com>" ``` + Match the prefix to the task's shape (Step 3): `feat:`/`fix:` behavioural, `test:` test-only, + `refactor:` structural, `docs:` documentation, `chore:` scaffolding. (Both models contributed: the main agent on **opus** orchestrated and committed; the sub-agent on **sonnet** wrote the test + implementation.) +3. **Commit two — the bookkeeping:** use Edit to change `- [ ]` to `- [x]` in `tasks.md`, then: + ```bash + git add specs/{current-spec}/tasks.md + git commit -m "docs: mark task [N] complete + + Co-Authored-By: Claude Opus <noreply@anthropic.com>" + ``` 4. Count this task toward the run count. **ALREADY_COMPLETE:** -1. Mark the task complete: use Edit to change `- [ ]` to `- [x]` in `ralph-tasks.md`. -2. The behavior already existed, so the sub-agent wrote **no** source — stage and commit the - checkbox tick **alone** (do NOT re-stage `[TEST_FILES]`/`[IMPL_FILES]`, which are empty): +1. The behavior already existed, so the sub-agent wrote **no** source — there is no change commit. +2. Mark the task complete (`- [ ]` → `- [x]`) and commit the tick **alone** (do NOT re-stage + `[TEST_FILES]`/`[IMPL_FILES]`, which are empty): ```bash - git add specs/{current-spec}/ralph-tasks.md - git commit -m "docs: mark ralph task [N] complete — behavior already existed + git add specs/{current-spec}/tasks.md + git commit -m "docs: mark task [N] complete — behavior already existed - - Ralph task: [task number]/[total] + - Task: [task number]/[total] - Co-Authored-By: Claude Opus <noreply@anthropic.com> - Co-Authored-By: Claude Sonnet <noreply@anthropic.com>" + Co-Authored-By: Claude Opus <noreply@anthropic.com>" ``` 3. Count this task toward the run count. **FAILED:** -1. Mark the task as failed: change `- [ ]` to `- [!]` in `ralph-tasks.md`. +1. Mark the task as failed: change `- [ ]` to `- [!]` in `tasks.md`. 2. Append a comment to the task line: ` <!-- RALPH-FAILED: [FAILURE_REASON] -->` 3. Commit the failure marker so the next iteration skips it: ```bash - git add specs/{current-spec}/ralph-tasks.md - git commit -m "chore: mark ralph task [N] failed — [short reason]" + git add specs/{current-spec}/tasks.md + git commit -m "chore: mark task [N] failed — [short reason]" ``` If the sub-agent left partial source edits, the default is to **discard them** so only the failure marker is committed. Discard them with a **scoped** checkout limited to the @@ -259,20 +430,31 @@ Read the sub-agent's returned result and act on its `STATUS`: discard. 4. Count it toward the run count and proceed to the next task — do NOT get stuck. +> **On editing an approved `tasks.md`.** `.tasks-approved` freezes the *content* of the task list — +> what the tasks are. Checkbox state (`[ ]` → `[x]` / `[!]`) is progress bookkeeping, not content, +> and both `/spec:implement` and this loop write it. Never reword, add, remove or reorder a task +> here; if a task is wrong, mark it `- [!]` and let the user decide. +> +> **Never stage `.current-gear`** — it is gitignored working state, not part of the change. + ### Step 6: Check Continuation (self-driving loop) This is the loop. After each task, check ALL of these stop conditions — if **any** holds, go to Step 7 and stop; otherwise continue the loop: -1. **STOP file**: If `RALPH_STOP` exists at repo root → stop. This is the unattended - kill-switch: the user can `touch RALPH_STOP` from another terminal at any time. -2. **Bound reached** (the bound chosen in Step 0): - - **Tasks**: completed tasks this run (GREEN + ALREADY_COMPLETE) ≥ N → stop - - **Turns**: iterations attempted this run (including FAILED) ≥ N → stop - - **Budget**: output tokens consumed this run have reached ~N → stop -3. **All done**: If no more `- [ ]` tasks remain in `ralph-tasks.md` → stop - -If none hold, **continue to the next task**: return to **Step 3** with a **fresh sub-agent** +1. **STOP file**: `RALPH_STOP` exists at repo root → stop (`STOPPED`). The unattended kill-switch. +2. **Gear downshift**: `.current-gear` now reads `review-before`, is gone, or is unparseable → + stop (`DOWNSHIFTED`). Re-read the file **every** iteration; this is how the user takes back + per-test review mid-section without losing the work already done. +3. **Bound reached** (the bound chosen in Step 0) → stop (`BOUND_REACHED`): + - **Tasks**: completed tasks this run (GREEN + CHARACTERISED + ALREADY_COMPLETE) ≥ N + - **Turns**: iterations attempted this run (including FAILED) ≥ N + - **Budget**: output tokens consumed this run have reached ~N +4. **Scope exhausted**: the gear has a `scope:` and the next unchecked task is outside it → stop + (`SCOPE_COMPLETE`). +5. **All done**: no more `- [ ]` tasks remain in `tasks.md` → stop (`ALL_DONE`). + +If none hold, **continue to the next task**: return to **Step 1** with a **fresh sub-agent** (fresh context). Drive this yourself — do not wait for the user. **Self-pacing across context windows.** For a long run that would outgrow a single context @@ -282,8 +464,9 @@ For a routine short run, just loop inline. Either way, the loop is unattended on done. **Interactive cancel.** Independent of `RALPH_STOP`, the user can press **Esc** while a -self-paced wake-up is pending to cancel the loop. Both mechanisms stop it: `RALPH_STOP` for a -fully unattended kill, Esc for an at-the-keyboard cancel. +self-paced wake-up is pending to cancel the loop. Three mechanisms stop it: `RALPH_STOP` for a +fully unattended kill, `/spec:gear review-before` for a deliberate return to gated review, and Esc +for an at-the-keyboard cancel. ### Step 7: Print Summary @@ -291,28 +474,36 @@ fully unattended kill, Esc for an at-the-keyboard cancel. ``` === RALPH SUMMARY === +Spec: specs/{current-spec}/ +Gear: review-after [scoped to "{section}"] Bound: tasks=N | turns=N | budget=N Iterations this run: I Tasks completed this run: N Total tasks complete: X/Y Tasks remaining: Z -Status: BOUND_REACHED | STOPPED | ALL_DONE +Status: BOUND_REACHED | STOPPED | DOWNSHIFTED | SCOPE_COMPLETE | ALL_DONE +Next task: [task N — one-line description, or "none"] === END RALPH === ``` Status meanings: - `BOUND_REACHED`: Hit the chosen bound (tasks / turns / budget); more tasks may remain - `STOPPED`: Halted due to `RALPH_STOP` file (or user Esc) -- `ALL_DONE`: No more tasks in ralph-tasks.md +- `DOWNSHIFTED`: The gear was shifted to `review-before` — resume with `/spec:implement`; nothing + completed was unwound +- `SCOPE_COMPLETE`: Every task under the gear's scoped section is done; widen or move the scope with + `/spec:gear` to continue +- `ALL_DONE`: No more tasks in `tasks.md` ## Important Reminders - **AskUserQuestion is for Step 0 setup only** — never prompt the user once the loop is running - **Run on opus with auto mode** for a true unattended run; the per-task sub-agent stays sonnet -- **NEVER push to remote** - the human decides when to push -- **NEVER modify tasks.md** - only modify ralph-tasks.md -- **Always commit after each task** - each task gets its own commit, and the MAIN agent (not - the sub-agent) makes that commit -- **The sub-agent reads its task's References first** - every task carries the context a - fresh session needs +- **One task list**: `tasks.md`. There is no `ralph-tasks.md` and nothing regenerates a task list +- **`.tasks-approved` is required** — this loop runs an approved list +- **Re-read the gear every iteration** — the downshift is the user's way back in +- **NEVER push to remote** — the human decides when to push +- **Two commits per task** — the change, then the checkbox tick +- **Never stage `.current-gear`** — gitignored working state +- **The gear removes the pause, not the discipline** — RED-first, full suite, conventions all hold - Follow ALL guidelines in .agent_instructions/testing.md and .agent_instructions/code_style.md diff --git a/.claude/commands/spec/ralph-tasks.md b/.claude/commands/spec/ralph-tasks.md deleted file mode 100644 index c894144b..00000000 --- a/.claude/commands/spec/ralph-tasks.md +++ /dev/null @@ -1,166 +0,0 @@ ---- -allowed-tools: Bash(cat:*), Bash(grep:*), Bash(test:*), Bash(find:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Read, Write, Glob, Agent, AskUserQuestion -description: Create ralph-tasks.md for unattended TDD implementation ---- - -## Context - -Current spec directory: specs/ - -**Purpose**: Generate `ralph-tasks.md` formatted for **unattended** TDD execution via the -Ralph loop. ralph tasks have no approval gates and include all context needed for a fresh -Claude session. - -**Standalone — derived directly from the approved design.** This command does **not** require -`tasks.md` or `.tasks-approved`. It is a peer alternative to `/spec:tasks`: from an approved -design (`.design-approved`) you run *either* `/spec:tasks` (attended path) *or* -`/spec:ralph-tasks` (unattended path) — not both. The ralph task list is generated straight -from `requirements.md` + the accepted ADRs. (If a `tasks.md` happens to exist from an earlier -attended pass, ignore it — the source of truth is the approved design.) - -**Sub-agent**: Drafting the ralph task list is delegated to a sub-agent -(`subagent_type: "Plan"`, **`model: "opus"`**). `Plan` has no `Write`/`Edit`/`NotebookEdit`, -which makes it much harder to accidentally write the file (the prompt still forbids writing -via `Bash`), while still allowing Read/Glob/Grep to verify test/impl paths. The sub-agent reads -the requirements and ADRs and RETURNS the ralph task list as text. The main agent runs the -validation checklist and writes the file. See `.claude/commands/spec/README.md` → -"Sub-agents & model policy". - -## Your Task - -### Step 1: Gather Context - -1. Read `specs/.current-spec` to determine the active specification directory -2. Verify `.design-approved` exists in that directory (ralph-tasks derives from the approved design) -3. Read `specs/{current-spec}/requirements.md` for requirement context -4. Read `specs/{current-spec}/.adr-list` to see all ADRs -5. Read each ADR from `docs/adr/` to understand design decisions - -Do **not** read or depend on `tasks.md` — ralph-tasks is generated directly from the approved -design, independent of the attended `/spec:tasks` path. - -### Step 2: Verify Prerequisites - -- `.design-approved` MUST exist (ralph-tasks derives from the approved design) -- All ADRs MUST have Status "Accepted" - -If prerequisites not met, inform user and exit. Do NOT launch the sub-agent. (Note: there is -**no** `.tasks-approved` prerequisite — that marker belongs to the attended `/spec:tasks` path.) - -### Step 3: Launch Sub-Agent to Draft ralph-tasks.md - -**Verify inputs with the user before launching (MAIN agent).** The `Plan` sub-agent is -one-shot and has no `AskUserQuestion` — it cannot ask the user anything once launched. And -because the ralph-tasks it produces are later executed **unattended** (no approval gates), -any ambiguity must be resolved now. So before launching, review the requirements and ADRs for -open decisions that affect unattended execution (task granularity, ordering, scope boundaries, -anything that would need a human judgement call mid-loop) and resolve them with the user via -`AskUserQuestion`. Then launch the sub-agent with the clarified inputs folded in. All user -interaction stays in the main agent — never the sub-agent. - -Launch an `Agent` with `subagent_type: "Plan"` and **`model: "opus"`**. The -prompt MUST include: - -1. The full text of `requirements.md` and each ADR (or their paths to read). The sub-agent - decomposes the approved design into atomic ralph tasks itself — there is no `tasks.md` to - reformat. -2. The document structure, task format, key differences, and quality rules below. -3. An explicit instruction: **RETURN the complete ralph-tasks.md content as markdown text. - Do NOT write any file.** The sub-agent may use Read/Glob/Grep to verify the test/impl - file paths and `RALPH-VERIFY` filters it references are realistic. - -#### Document Structure (include in the sub-agent prompt) - -```markdown -# Ralph Tasks: {spec-name} - -> Auto-generated from the approved design for unattended TDD execution. -> Each task is self-contained with all context a fresh Claude session needs. - -## Spec Context - -- **Spec**: {spec-name} -- **Requirements**: specs/{current-spec}/requirements.md -- **ADRs**: {list of ADR file paths} - -## Tasks - -{task list} -``` - -#### Task Format (include verbatim in the sub-agent prompt) - -**CRITICAL**: Each task MUST follow this exact format: - -```markdown -- [ ] **[Brief behavior description]** - - **Behavior**: [Precise behavioral specification - what should happen when X] - - **Test file**: `test/[Project]/[When_condition_should_behavior.cs]` - - **Test should verify**: - - [Verification point 1] - - [Verification point 2] - - **Implementation files**: - - `src/[Project]/[File.cs]` - [What to add/change] - - **RALPH-VERIFY**: `dotnet test test/[Project]/ --filter "FullyQualifiedName~When_condition_should_behavior"` - - **References**: [ADR numbers, requirement sections, existing code files to read] -``` - -#### How ralph-tasks differ from the attended `/spec:tasks` output (include in the sub-agent prompt) - -ralph-tasks are written for an **unattended** loop, so they differ from what `/spec:tasks` -produces for the attended path: - -| Feature | `/spec:tasks` (attended) | ralph-tasks.md (unattended) | -|---------|--------------------------|-----------------------------| -| Approval gates | `STOP HERE` after each test | None | -| `/test-first` directive | `USE COMMAND: /test-first ...` | None | -| Verification command | None | `RALPH-VERIFY:` with exact dotnet test filter | -| Context references | Assumes human context | `References:` section with files/ADRs to read | -| Atomicity | Grouped by phase | Strictly one behavior per task | - -Do **not** emit `STOP HERE` or `/test-first` directives — those belong to the attended path. - -#### Quality Rules (MANDATORY — include in the sub-agent prompt) - -Apply these rules to EVERY task: - -1. **One thing per task**: Single component, decorator, behavior, or migration. Never combine. -2. **Testable in isolation**: After completing the task, `dotnet build` and the RALPH-VERIFY command must pass. -3. **Completable in one iteration**: ~200 lines of combined test + implementation code maximum. -4. **Ordered by dependency**: If Task B depends on Task A's code, Task A comes first. -5. **Self-contained**: The `References` section includes ALL files/ADRs a fresh Claude session needs to read before starting the task. - -Decompose the approved design into the smallest atomic tasks: each task maps to a single -behavior derived from the requirements/ADRs, formatted for unattended execution. Do NOT emit -the `STOP HERE` or `/test-first` directives used by the attended path. - -### Step 4: Validate the Returned Task List - -After the sub-agent returns, verify before writing: - -- [ ] Every task has a `RALPH-VERIFY` command with a valid test filter -- [ ] Every task has a `References` section -- [ ] No task combines multiple behaviors -- [ ] Tasks are ordered so dependencies come first -- [ ] No task carries the attended path's `STOP HERE` or `/test-first` directives -- [ ] Each task's test name follows `When_[condition]_should_[expected_behavior]` convention - -If any check fails, ask the sub-agent to revise (or fix it yourself) before writing. - -### Step 5: Write the File - -Write the validated ralph-tasks.md to `specs/{current-spec}/ralph-tasks.md`. - -Print summary: -``` -Ralph tasks generated: specs/{current-spec}/ralph-tasks.md -Total tasks: N -Ready for: /spec:ralph-implement -``` - -## Important Notes - -- **NEVER modify tasks.md** - ralph-tasks.md is a separate file (and ralph-tasks does not - read tasks.md either — it derives from the approved design) -- The RALPH-VERIFY command must be copy-pasteable and work from the repo root -- References should include specific file paths, not general descriptions diff --git a/.claude/commands/spec/status.md b/.claude/commands/spec/status.md index e64fcc70..95405a0f 100644 --- a/.claude/commands/spec/status.md +++ b/.claude/commands/spec/status.md @@ -20,6 +20,10 @@ Current spec directory: specs/ - Phase files: `requirements.md`, `tasks.md` - ADR list: `.adr-list` - GitHub issue: `.issue-number` + - **Review gear**: `.current-gear` (untracked working state — see + [`gear.md`](gear.md)). Report the resolved gear for any spec that has one. A spec with no + `.current-gear` is in the default `review-before` gear; say so only for the **active** spec, + to keep the report short. 4. For each ADR in `.adr-list`, read from `docs/adr/` and check Status field ### Step 2: Present Status Report @@ -48,6 +52,10 @@ Specifications: Tasks: Approved Progress: 15/30 tasks complete (50%) + Review gear: ➖ review-after — scoped to "Phase 2 — Cache key generation (behaviour)" + Since 2026-10-07: Phase 1's five tests were all approved unchanged; the shape is settled + (untracked; shift with /spec:gear) + Next Action: Approve remaining ADR or begin implementation ``` @@ -64,13 +72,24 @@ Status Indicators: ADR Status: [Proposed] - Not yet approved [Accepted] - Approved and ready + +Review gear: + ✅ review-before - approval gate armed (the default) + ➖ review-after - approval gate not armed; reviewed as a batch ``` +**Call out an ungated spec.** The gear file is untracked, so it will not show up in a diff or a +`git status` — this report is one of the few places it surfaces. If any spec resolves to +`review-after`, say so plainly and give the downshift command (`/spec:gear review-before`). If a +gear file exists but cannot be parsed, report the spec as `review-before` (the fail-safe default) +and flag the malformed file. + ### Step 4: Summary Provide a summary: - Total specifications: {count} - Active spec: {name} +- Review gear for the active spec (and any other spec not in the default gear) - Next recommended action for active spec Use Read, Bash, and Glob tools to gather information. diff --git a/.claude/commands/spec/tasks.md b/.claude/commands/spec/tasks.md index 4a489bef..63719dba 100644 --- a/.claude/commands/spec/tasks.md +++ b/.claude/commands/spec/tasks.md @@ -36,7 +36,7 @@ Organize tasks to enable incremental development and testing. - Test should verify: - [verification point 1] - [verification point 2] - - **STOP HERE - WAIT FOR USER APPROVAL in IDE before implementing** + - **⛔ APPROVAL GATE — STOP HERE and WAIT FOR USER APPROVAL in IDE before implementing** *(fires in the `review-before` gear, which is the default)* - Implementation should: - [implementation point 1 with specific file/line numbers where applicable] - [implementation point 2] @@ -53,7 +53,7 @@ Organize tasks to enable incremental development and testing. - QueryProcessor.Execute called with unregistered query type - QueryHandlerNotFoundException thrown with descriptive message - Exception includes the query type name - - **STOP HERE - WAIT FOR USER APPROVAL in IDE before implementing** + - **⛔ APPROVAL GATE — STOP HERE and WAIT FOR USER APPROVAL in IDE before implementing** *(fires in the `review-before` gear, which is the default)* - Implementation should: - In QueryProcessor.Execute() check handler registry for query type - Throw QueryHandlerNotFoundException if no handler found @@ -63,11 +63,60 @@ Organize tasks to enable incremental development and testing. ### Why This Format? 1. **Visible command**: The `/test-first` command is prominently displayed -2. **Stop sign**: The STOP HERE makes the approval gate unmissable +2. **Stop sign**: The ⛔ and "STOP HERE" make the approval gate unmissable 3. **Single task**: Combines TEST + IMPLEMENT so workflow is clear 4. **Complete context**: All details needed for test and implementation 5. **IDE review**: Explicitly states user will review in IDE, not CLI +### The gate line and the review gear + +Write the `⛔` line on **every** behavioral task. It states the default: the gate is armed unless +the spec has been deliberately shifted into the `review-after` gear (see [`gear.md`](gear.md) and +Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)). + +Do **not** try to encode the gear in `tasks.md` — no per-task or per-phase "no gate" annotations, +and no omitting the `⛔` line for tasks you expect to run unattended. `tasks.md` is frozen once +`.tasks-approved` lands, so a gear written into it could not be shifted afterwards without lifting +that freeze. The gear lives in the untracked `specs/{spec}/.current-gear` file and is shifted with +`/spec:gear`. The `⛔` line is a statement of the default, not a per-task switch. + +Do keep **section headings** meaningful and stable, though: `/spec:gear` can scope a gear to a +single heading, so a well-named section ("Phase 2 — Cache key generation (behaviour)", "Task 6-11 — +Provider coverage") is what makes a narrow, self-expiring gear shift possible. Any heading +convention works — `## Phase N`, `## Task N`, a named group — and the depth does not matter; a scope +can also be a `tasks N-M` range. What does not work is a single flat `## Tasks` heading over forty +tasks, because there is then nothing to scope to short of the whole spec. + +### Task-type tag form (required) + +Every task checkbox opens its bold lead-in with exactly one tag, followed immediately by a colon, +with any task id after the colon — one template line per tag: + +```markdown +- [ ] **TEST + IMPLEMENT: T1.1 — …** +- [ ] **CHARACTERISE: T1.2 — …** +- [ ] **STRUCTURAL: T1.3 — …** +- [ ] **SETUP: T1.4 — …** +- [ ] **DOC: T1.5 — …** +- [ ] **VERIFY: T1.6 — …** +``` + +The tag must come first, immediately after `**`. `/spec:ralph-implement` dispatches on the leading +label, and skips (marks `- [!]`) any task whose label it does not recognise — see the drifted form +in the *DO NOT* block below. + +- **`TEST + IMPLEMENT`** — the normal behavioral task, using the template above. +- **`CHARACTERISE`** — a test for behavior an earlier task may already deliver (common when a spec + takes one task per acceptance criterion). Use the same template, plus a **named RED mutation**: a + `🔁 RED mutation:` bullet naming the temporary change to *production* code that must make the + test fail, and the assertion it must fail on. A green-on-arrival test is then proved able to fail + without being weakened or rewritten (see `.agent_instructions/testing.md` → *When a new test + passes on first run*). +- **`STRUCTURAL`** — a Tidy First change: no new test, existing suite green before and after. +- **`SETUP`** — project/config/package scaffolding; no test, but the build must succeed. +- **`DOC`** — documentation only. +- **`VERIFY`** — a checkpoint that runs named checks and should need no source change. + ### DO NOT Format Tasks Like This BAD - Separates test and implementation: @@ -81,3 +130,26 @@ BAD - Separates test and implementation: ``` This format allows Claude to skip the approval by treating them as independent tasks. + +BAD - Puts the task id before the tag: +```markdown +- [ ] **T1.1 — STRUCTURAL: Extract shared helper** +``` + +This drifts from the tag-first form above. `/spec:ralph-implement` does not recognise it as a +`STRUCTURAL` task and will skip it. + +## Next Steps + +Remind the user to: +- Review `tasks.md` +- Run `/spec:review tasks` for an adversarial coverage review, then +- `/spec:approve tasks` when ready to begin implementation. + +`tasks.md` is the **single** task list, and both drivers run it: +- `/spec:implement` — one task at a time, approval gate armed by default. +- `/spec:ralph-implement` — a self-driving unattended loop over the same list, in the + `review-after` gear. + +The choice between them is a gear change (`/spec:gear`), not a different task list — so do not +draft tasks "for unattended execution". Draft them once, well. diff --git a/.claude/commands/tdd/README.md b/.claude/commands/tdd/README.md index a544c7d1..2bbd1da2 100644 --- a/.claude/commands/tdd/README.md +++ b/.claude/commands/tdd/README.md @@ -6,7 +6,9 @@ This directory contains Claude Code commands that enforce Test-Driven Developmen ### `/test-first <behavior description>` -Guides you through the Red-Green-Refactor TDD cycle with a **mandatory approval gate** before implementation. +Guides you through the Red-Green-Refactor TDD cycle with an **approval gate** before implementation. The gate is armed by default; a standalone +`/test-first` is always gated, and only a spec command running in the `review-after` gear +(`/spec:gear`) skips the pause. **Purpose**: Ensures you write and approve tests before writing implementation code, preventing scope creep and promoting better design. @@ -40,7 +42,8 @@ Guides you through the Red-Green-Refactor TDD cycle with a **mandatory approval **Why Use This?** From [.agent_instructions/testing.md](../../../.agent_instructions/testing.md): -> The approval step is MANDATORY when working with an AI coding assistant +> The approval gate is **armed by default**. Assume it is armed unless a spec command has explicitly +> told you the spec is in the `review-after` gear This command enforces that requirement automatically, ensuring: - Tests correctly specify desired behavior before implementation diff --git a/.claude/commands/tdd/test-first.md b/.claude/commands/tdd/test-first.md index 2c18b2ab..5d663717 100644 --- a/.claude/commands/tdd/test-first.md +++ b/.claude/commands/tdd/test-first.md @@ -1,17 +1,54 @@ --- allowed-tools: Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion -description: TDD workflow with mandatory approval before implementation +description: TDD workflow with an approval gate before implementation (armed by default) argument-hint: <behavior description> --- # Test-First Development (TDD with Approval Gate) -You are guiding the user through a Test-Driven Development workflow with a **mandatory approval gate** before implementation. +You are guiding the user through a Test-Driven Development workflow with an **approval gate** before +implementation. The gate is **armed by default** — assume it is armed unless a spec command has +explicitly told you otherwise (see *Review gear*, below). ## The Behavior to Test $ARGUMENTS +## Review gear + +Which review mode applies is called the **gear** (see +[`spec/gear.md`](../spec/gear.md) and Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)): + +| Gear | Gate | Meaning | +|------|------|---------| +| `review-before` | ✅ armed | Stop after the test and get explicit approval before implementing. **The default.** | +| `review-after` | ➖ not armed | Write the test, prove RED, then implement without pausing. The user reviews the batch afterwards. | + +**This command runs `review-before` unless the invoking command states otherwise in its prompt.** + +- Invoked directly by the user (`/test-first <behavior>`) → **`review-before`**, always. Do **not** + read `specs/*/.current-gear` yourself. A standalone run must never inherit a mode it did not ask + for from whichever spec happens to be current. +- Invoked by `/bugfix:test` → **`review-before`**, always. Bugfixes are outside this mechanism. +- Invoked by `/spec:implement` or `/spec:ralph-implement` → those commands resolve the gear for the + spec and the specific task, and pass the result in explicitly. Honour what they pass. + +If you are ever unsure which gear applies, it is `review-before`. Fail safe, never fail open. + +## What the gear does NOT change + +`review-after` removes the **human approval pause** and nothing else. Every one of these holds in +both gears: + +- **RED first.** Write the test and observe it fail *for the right reason* before any production + code exists — or, for a characterisation test, under its named mutation (see the RED phase). + Ungated is not test-after. +- **The full regression suite**, not just the new test's own `--filter`. +- **The two-commit shape**: a `feat:`/`test:` commit for the behaviour, then a separate `docs:` + commit for any task-list checkbox. +- **Every test-authoring convention below**, plus the standing ones in + [.agent_instructions/testing.md](../../../.agent_instructions/testing.md). + ## Workflow Phases ### RED Phase - Write Failing Test @@ -32,7 +69,13 @@ $ARGUMENTS 2. Determine which public class/method will provide this behavior 3. Write the test following the xUnit BDD-style naming 4. Run the test to verify it fails for the right reason -5. Show the test code and failure message to the user + - **If it passes on first run**, the behaviour already exists. Do not weaken or rewrite the test + to force a failure. If the caller (a spec task labelled `CHARACTERISE`) names a **RED + mutation**, apply that temporary change to *production* code, confirm the test fails on the + assertion it is about, **revert the mutation**, and confirm green — that is RED observed. With + no named mutation, stop and report the green test to the user. +5. Show the test code and failure message to the user (for a characterisation test, the mutation + and the failure it produced) **After writing the test, proceed to the Approval Gate.** @@ -40,12 +83,18 @@ $ARGUMENTS ### APPROVAL GATE - User Must Approve Test +**This phase runs when the gear is `review-before` — which is the default, and which you should +assume unless the invoking command explicitly said `review-after`.** + **CRITICAL: You MUST get explicit user approval before proceeding to implementation.** Use the AskUserQuestion tool to ask: ``` Question: "Should I proceed to implement the code to make this test pass?" +For a characterisation test (green on arrival, RED observed under a named mutation, mutation +reverted) there is nothing to implement. Ask instead: "Should I commit this characterisation test?" +On approval, run the full suite and commit the test alone as `test:`. Options: 1. "Yes, implement the code" - Proceed to GREEN phase 2. "Modify the test first" - User will explain changes needed @@ -59,11 +108,26 @@ Options: **Do NOT proceed to implementation without explicit approval.** +#### When the gear is `review-after` + +Skip the pause — and **only** the pause. Instead: + +1. Confirm you have already proved RED (the test ran and failed for the right reason). If you have + not, go back and do it; `review-after` never licenses writing implementation first. +2. Print one line naming the gear and where it came from, so the run is self-documenting, e.g. + `➖ review-after (specs/014-…/.current-gear, Phase 2) — proceeding to GREEN without pausing`. +3. Proceed to GREEN. + +If anything about the test looks wrong to you — it asserts something the task did not ask for, it +needs a design decision you cannot make, it duplicates an existing test — **stop and ask anyway**. +`review-after` is a default for the routine case, not a gag order. + --- ### GREEN Phase - Make Test Pass -**Only execute this phase after receiving approval.** +**Under `review-before`, only execute this phase after receiving approval.** Under `review-after`, +proceed once RED is proved. **Your task:** Write the **minimum code** necessary to make the test pass. @@ -133,7 +197,9 @@ After completing all phases: ## Key Principles **From testing.md:** -- The approval step is **MANDATORY** when working with an AI coding assistant +- The approval step is **armed by default** and is only disarmed by a deliberate, scoped, recorded + gear shift (`/spec:gear`) — never by assumption, convenience, or a prose instruction in a scratch + file - Tests should specify behavior, not implementation details - Only write code required by tests - no speculative code - Tests should use the Arrange/Act/Assert structure @@ -198,5 +264,7 @@ Should I commit these changes? - This workflow enforces the TDD approval requirement from .agent_instructions/testing.md - The approval gate ensures the test correctly specifies desired behavior before implementation +- The gate is armed by default; `/spec:gear` is the only supported way to disarm it, and it does so + for one spec (optionally one section of its task list) at a time - Following this workflow provides scope control and better design - You may run `/test-first` multiple times to build up functionality incrementally diff --git a/.gitignore b/.gitignore index 567f195e..f53ce192 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,12 @@ # Claude Code session prompt PROMPT.md +# TDD review gear — untracked working state, not a spec artifact. +# Holds which gear a spec is currently being run in (review-before | review-after). +# Deliberately per-spec and deliberately NOT tracked: it records where the work is +# right now, not what the project decided. See .claude/commands/spec/gear.md. +specs/**/.current-gear + # User-specific files *.suo *.user diff --git a/CLAUDE.md b/CLAUDE.md index f7f620b9..5dc738bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,7 +14,6 @@ When working on implementation tasks in `specs/*/tasks.md`: - **STOP and ASK FOR APPROVAL** after writing each test - The user will review the test in their IDE before you implement - Each TEST task in tasks.md specifies the exact `/test-first` command to use -- The skill enforces the approval gate automatically - you cannot bypass it **Why this is mandatory:** 1. Tests correctly specify desired behavior before implementation @@ -24,6 +23,21 @@ When working on implementation tasks in `specs/*/tasks.md`: **If a task says `/test-first when ...`** - YOU MUST USE THAT COMMAND. Do not write the test file manually. +### The approval gate is armed by default — and is the only thing the gear changes + +The approval pause is a **gear**, not a policy (see [`.claude/commands/spec/gear.md`](.claude/commands/spec/gear.md) and Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md)). You select it for the certainty you have and the blast radius you face: review every test while the design is still moving, review a batch once the shape is settled, and shift back down the moment the work starts producing tests you would not have approved. + +| Gear | Gate | | +|------|------|--| +| `review-before` | ✅ armed | **The default.** Assume this unless a spec command tells you otherwise. | +| `review-after` | ➖ not armed | Only via a deliberate, scoped, reasoned shift. | + +- **The only supported way to disarm the gate is `/spec:gear review-after`**, which writes `specs/{spec}/.current-gear` — untracked working state, scoped to **one spec** and optionally to **one section** of `tasks.md`, carrying the reason for the shift. +- **`/spec:implement`** honours the gear; **`/spec:ralph-implement`** is always `review-after`. A **standalone `/test-first` and `/bugfix:test` are always gated** — they never read the gear file and must never infer a gear from context. +- **Never assume the gate is off.** Not from a prose instruction in a scratch file, not because the tasks look repetitive, not because a previous task ran ungated. If you are unsure, it is `review-before`. +- **`review-after` removes the human pause and nothing else.** RED-first (the test is observed failing for the right reason before any production code), the full regression suite, the two-commit shape (`feat:`/`test:` then a separate `docs:` tick), and every test-authoring convention hold in both gears. A run that drops any of those is defective, not "in a different gear". +- Even in `review-after`, **stop and ask** if a test asserts something the task did not ask for, needs a design decision you cannot make from the ADRs, or duplicates an existing test. + ## Spec Workflow Follow the structured specification workflow: Requirements -> ADR Design -> Adversarial Review (multiple rounds) -> Task Breakdown -> Implementation. Never skip review rounds or assume approval - wait for explicit user approval before proceeding to the next phase. @@ -45,16 +59,18 @@ Claude Code skills automate common workflows and enforce mandatory engineering p ### Core Development Skills -- `/test-first <behavior>` - TDD workflow with mandatory approval before implementation ([docs](.claude/commands/tdd/README.md)) +- `/test-first <behavior>` - TDD workflow with an approval gate before implementation, armed by default ([docs](.claude/commands/tdd/README.md)) - `/tidy-first <change>` - Separate structural (refactoring) from behavioral (feature) changes ([docs](.claude/commands/refactor/README.md)) - `/adr <title>` - Create Architecture Decision Records ([docs](.claude/commands/adr/README.md)) ### Specification Workflow Skills - `/spec:requirements`, `/spec:design`, `/spec:tasks`, `/spec:implement`, `/spec:status` - Complete specification-driven development workflow ([docs](.claude/commands/spec/README.md)) +- `/spec:gear [review-before|review-after]` - Report or shift the TDD review gear for the current spec; `/spec:ralph-implement` runs the same `tasks.md` unattended in `review-after` **When to use skills**: - Use `/test-first` when adding new behavior or fixing bugs +- Use `/spec:gear` when the approval gate has stopped earning its cost (upshift) or has started to again (downshift) — never a hand-written waiver - Use `/tidy-first` when code needs refactoring before/during feature work - Use `/adr` when documenting architectural decisions - Use `/spec:*` commands for full feature development from requirements to implementation From 2bf9132ec85bf23ae3c0f7a2bb5644d40aaeb0cc Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 09:26:04 +0100 Subject: [PATCH 02/12] docs(adr): add YAML frontmatter to every ADR and generate the ADR index Backfill id/title/status/author/created/summary/tags onto all 22 ADRs, per the new .agent_instructions/adr_frontmatter.md (schema, status vocabulary, Darker tag taxonomy, slug-as-identity). docs/adr/index.md is generated from the frontmatter by generate_adr_index.awk and must not be hand-edited. Bodies are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agent_instructions/adr_frontmatter.md | 158 ++++++++++++++++++ .claude/commands/adr/generate_adr_index.awk | 58 +++++++ .../adr/0001-record-architecture-decisions.md | 12 ++ ...002-attribute-driven-decorator-pipeline.md | 14 ++ ...0003-fluent-builder-for-query-processor.md | 13 ++ .../adr/0004-factory-registry-abstractions.md | 14 ++ docs/adr/0005-dual-sync-async-support.md | 13 ++ .../0006-remove-third-party-di-packages.md | 14 ++ docs/adr/0007-rename-di-package.md | 13 ++ docs/adr/0008-split-handler-interfaces.md | 14 ++ ...le-and-inmemory-factory-implementations.md | 13 ++ docs/adr/0010-pass-query-context.md | 13 ++ docs/adr/0011-merge-builtin-decorators.md | 14 ++ docs/adr/0012-json-serializer-swap.md | 14 ++ ...13-migrate-legacy-tests-to-test-doubles.md | 12 ++ docs/adr/0014-factory-component-lifetime.md | 14 ++ .../0015-resilience-pipeline-integration.md | 14 ++ .../0016-pipeline-attribute-memoization.md | 14 ++ .../0017-query-tracing-and-database-spans.md | 14 ++ docs/adr/0018-metrics-from-query-traces.md | 14 ++ docs/adr/0019-streaming-query-pipeline.md | 14 ++ ...0020-agreement-dispatch-handler-routing.md | 13 ++ .../0020-validation-decorator-architecture.md | 13 ++ .../0021-caching-decorator-architecture.md | 14 ++ docs/adr/index.md | 39 +++++ 25 files changed, 552 insertions(+) create mode 100644 .agent_instructions/adr_frontmatter.md create mode 100644 .claude/commands/adr/generate_adr_index.awk create mode 100644 docs/adr/index.md diff --git a/.agent_instructions/adr_frontmatter.md b/.agent_instructions/adr_frontmatter.md new file mode 100644 index 00000000..68f86513 --- /dev/null +++ b/.agent_instructions/adr_frontmatter.md @@ -0,0 +1,158 @@ +# ADR Frontmatter Conventions + +This document defines the YAML frontmatter that every Architecture Decision Record (ADR) in +`docs/adr/` carries. It is the single source of truth referenced by the `read_adr_metadata` and +`write_adr_metadata` skills, by `/spec:design` and `/adr`, and by the backfill process that added +frontmatter to the existing ADRs. + +## Why frontmatter + +Agents (and people) need to find prior ADRs relevant to a new design without reading every ADR in +full. Grepping whole ADR bodies is expensive in tokens and pollutes context — especially with +superseded decisions. A small, structured metadata block at the top of each ADR lets a reader scan +`title` + `summary` + `tags` cheaply and decide which ADRs are worth opening. + +This convention is shared with Brighter, which records the decision behind it in its ADR *"Add +Frontmatter to ADRs"*. + +## Schema + +Frontmatter is a YAML block delimited by `---` fences, placed at the very top of the file, before +the `# N. Title` heading: + +```yaml +--- +id: 0021-caching-decorator-architecture +title: "Caching Decorator Architecture" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-20 +summary: "Adds a caching decorator over Microsoft's HybridCache abstraction that short-circuits the pipeline on a hit, with a replaceable ICacheKeyGenerator, a required expiry, and hit/miss recorded as a span attribute." +tags: + - "decorators" + - "caching" + - "performance" +--- + +# 21. Caching Decorator Architecture + +Date: 2026-07-20 +... +``` + +## Field reference + +| Field | Required | Type | Notes | +|-----------|----------|-----------------|-------| +| `id` | yes | string | **Stable identity = the filename stem** (name without `.md`), e.g. `0021-caching-decorator-architecture`. This is the unique key; see *Identity and numbering* below. | +| `title` | yes | string (quoted) | The ADR title. Use the H1 title text (without the leading `N.` number). | +| `status` | yes | enum | One of `Proposed`, `Accepted`, `Deprecated`, `Superseded`. Mirrors the body `## Status`. See *Status* below. | +| `author` | yes | list of strings | Always list form, even for one author: `- "Name"`. Backfill default is `- "Darker Team"` where no author is recorded. | +| `created` | yes | date (ISO 8601) | `YYYY-MM-DD`. For existing ADRs use the body `Date:` line. | +| `summary` | yes | string (quoted) | One or two sentences describing the decision. This is what an agent reads to decide whether to open the full ADR — make it specific about *what was decided*, not just the topic. | +| `tags` | yes | list of strings | 1–4 topic tags drawn from the controlled vocabulary below. | + +## Identity and numbering + +The ADR **number is a non-unique ordering hint, not an identity.** ADR numbers are allocated by each +branch picking `max+1` without locking, so parallel branches can collide: `0020` is already shared +by `0020-agreement-dispatch-handler-routing` and `0020-validation-decorator-architecture`. We do +**not** renumber — the inbound references (inter-ADR links, `release_notes.md`, `specs/`) and +historical anchors make renumbering costly and destructive. + +Instead: + +- The **identity is the `id` field = the filename stem** (number + slug), which *is* unique. +- Tooling (`read_adr_metadata`, the generated index) keys off `id`/slug, never the bare number. +- New ADRs still take `max+1` as a number for ordering, and a number collision with a concurrent + branch is **acceptable** — it is not a defect to fix, because identity is the slug. + +When you refer to an ADR in prose or `Related ADRs`, prefer the slug/filename +(`0020-validation-decorator-architecture`) over the bare number where ambiguity is possible. + +## Status + +Canonical values (Nygard vocabulary): + +- **Proposed** — drafted, not yet approved. New ADRs start here (set by `/spec:design` / `/adr`). +- **Accepted** — approved and in force. `/spec:approve design` flips `Proposed` → `Accepted` in + both the frontmatter `status` and the body `## Status`. +- **Deprecated** — no longer in force, but *not* replaced by a specific ADR (the decision was + retired/abandoned). Use this when there is no superseding ADR to point at. +- **Superseded** — replaced by a *specific* later ADR. When a new ADR supersedes an older one, set + the older ADR's `status` to `Superseded` and add a `Superseded by` reference to the superseding ADR. + +`read_adr_metadata` skips both `Deprecated` and `Superseded` ADRs by default when surfacing prior art. + +### Backfill status normalization + +The legacy ADR bodies use inconsistent status prose. Backfill normalizes to the canonical values in +**both** the frontmatter `status` **and** the body `## Status` line (this is an intentional cleanup): + +| Body text (legacy) | Canonical `status` | +|---------------------------------------------------------------------|--------------------| +| `Accepted`, `Adopted`, `Approved`, `Modified`, `Accepted. Amended …`| `Accepted` | +| `Proposed`, `Proposal`, `Draft`, `Proposed (Draft PR …)` | `Proposed` | +| `Retired` | `Deprecated` | +| An ADR explicitly replaced by a *specific* later ADR | `Superseded` | + +Normalization rules: +- Where a status line carries extra information (e.g. `Accepted. Amended 2026-02-17 to add …`), + normalize the status **word** to the canonical value but preserve the + amendment sentence in the body (move it below the status line if needed — do not lose it). +- **Partial** supersession (a later ADR supersedes only a *section* of an older one) does **not** change the older ADR's status — it stays `Accepted`; note the + partial supersession in prose instead. +- Do **not** guess. Only assign `Superseded` when a specific replacing ADR exists; only assign + `Deprecated` for a genuinely retired decision. Flag anything unclear for human confirmation. + +## Tag taxonomy (controlled vocabulary) + +Tags are lowercase kebab-case, quoted, 1–4 per ADR, drawn from this controlled list. Prefer the +most specific applicable tags. If no existing tag fits, propose an addition to this list rather than +inventing an ad-hoc tag (keeps the vocabulary from sprawling). + +**Architecture & API** +`architecture`, `cqrs`, `pipeline`, `decorators`, `api-design`, `configuration`, `di`, `lifetime`, +`dispatch`, `streaming`, `async`, `query-context` + +**Reliability & validation** +`resilience`, `retry`, `fallback`, `circuit-breaker`, `error-handling`, `validation` + +**Performance** +`performance`, `caching`, `memory`, `concurrency` + +**Observability** +`observability`, `otel`, `tracing`, `metrics`, `logging` + +**Packaging & dependencies** +`packaging`, `dependencies`, `serialization` + +**Process & tooling** +`meta`, `testing` + +## Skills + +- **`read_adr_metadata`** — extracts frontmatter blocks cheaply and returns matching ADRs + (`id` + `title` + `summary` + path) filtered by tag/status. Suggested from `/spec:design` to find + prior art before drafting a new ADR. Skips `Superseded` by default. +- **`write_adr_metadata`** — adds or updates a file's frontmatter and enforces the status rules + (new = `Proposed`; supersession sets the old ADR to `Superseded`). Idempotent. + +## Derived index + +`docs/adr/index.md` is a table generated *from* the frontmatter (id, title, status, tags, summary). +It is a derived cache for single-file reads — it must never be hand-edited; regenerate it from +frontmatter so it cannot drift. The generated file carries a `DO NOT EDIT` banner. + +**Regenerate** it after adding an ADR or changing any frontmatter (status, tags, summary, title). +This is the single canonical command — `/adr` and `/spec:approve` call it, and you can run it by +hand: + +```bash +awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md +``` + +The generator reads **only** the frontmatter block of each ADR (like `read_adr_metadata`), keys rows +off `id`, and orders by filename. It is deterministic: rerunning it with no ADR changes produces no +diff, so a dirty `index.md` after regeneration means an ADR's frontmatter actually changed. diff --git a/.claude/commands/adr/generate_adr_index.awk b/.claude/commands/adr/generate_adr_index.awk new file mode 100644 index 00000000..42eb7366 --- /dev/null +++ b/.claude/commands/adr/generate_adr_index.awk @@ -0,0 +1,58 @@ +#!/usr/bin/awk -f +# Regenerate docs/adr/index.md from ADR frontmatter. +# Reads only the YAML frontmatter block (between the first two `---` fences) of +# each ADR; never the bodies. Emits a Markdown table ordered by id. +# Usage: awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md + +function trim(s) { gsub(/^[ \t]+|[ \t]+$/, "", s); return s } +function unquote(s) { s = trim(s); gsub(/^"|"$/, "", s); return s } +function cell(s) { gsub(/\|/, "\\|", s); gsub(/\n/, " ", s); return s } + +FNR == 1 { + n = 0 + id = ""; title = ""; status = ""; summary = ""; tags = ""; intags = 0 +} +/^---[ \t]*$/ { + n++ + if (n == 2) { # closing fence: flush this file's record + intags = 0 + if (id != "") { + rows[++count] = "| `" id "` | " cell(title) " | " status " | " cell(tags) " | " cell(summary) " |" + } + } + next +} +n != 1 { next } # only inside the frontmatter block + +/^id:/ { id = trim(substr($0, index($0, ":") + 1)); intags = 0; next } +/^title:/ { title = unquote(substr($0, index($0, ":") + 1)); intags = 0; next } +/^status:/ { status = trim(substr($0, index($0, ":") + 1)); intags = 0; next } +/^summary:/ { summary = unquote(substr($0, index($0, ":") + 1)); intags = 0; next } +/^tags:/ { intags = 1; next } +/^[a-zA-Z_]+:/ { intags = 0; next } # any other top-level key ends the tags list +intags && /^[ \t]*-[ \t]*/ { + t = $0; sub(/^[ \t]*-[ \t]*/, "", t); t = unquote(t) + tags = (tags == "" ? t : tags ", " t) + next +} + +END { + print "<!-- GENERATED FILE - DO NOT EDIT BY HAND." + print " Regenerated from ADR frontmatter by /adr and /spec:approve" + print " (see .agent_instructions/adr_frontmatter.md). Edit the ADRs, not this file. -->" + print "" + print "# Architecture Decision Records — Index" + print "" + print "Derived index of every ADR in `docs/adr/`, generated from each file's YAML frontmatter." + print "It is a regenerable cache: **do not hand-edit**. To refresh it after adding or changing an" + print "ADR, rerun the generator documented in" + print "[`.agent_instructions/adr_frontmatter.md`](../../.agent_instructions/adr_frontmatter.md)." + print "" + print "Identity is the `id` (filename stem), not the leading number — numbers may repeat (e.g. two ADRs are numbered 20)." + print "" + print "| id | Title | Status | Tags | Summary |" + print "|----|-------|--------|------|---------|" + for (i = 1; i <= count; i++) print rows[i] + print "" + print "_" count " ADRs indexed._" +} diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr/0001-record-architecture-decisions.md index d72d0df5..4fff7c89 100644 --- a/docs/adr/0001-record-architecture-decisions.md +++ b/docs/adr/0001-record-architecture-decisions.md @@ -1,3 +1,15 @@ +--- +id: 0001-record-architecture-decisions +title: "Record Architecture Decisions" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "Adopts Nygard-style Architecture Decision Records stored as numbered markdown files in docs/adr/ with a fixed Status/Context/Decision/Consequences/Alternatives template, and retroactively documents existing decisions." +tags: + - "meta" +--- + # 1. Record Architecture Decisions Date: 2026-05-15 diff --git a/docs/adr/0002-attribute-driven-decorator-pipeline.md b/docs/adr/0002-attribute-driven-decorator-pipeline.md index f78bf4c5..8a0984a8 100644 --- a/docs/adr/0002-attribute-driven-decorator-pipeline.md +++ b/docs/adr/0002-attribute-driven-decorator-pipeline.md @@ -1,3 +1,17 @@ +--- +id: 0002-attribute-driven-decorator-pipeline +title: "Attribute-Driven Decorator Pipeline for Query Handling" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "Cross-cutting concerns are declared as step-ordered attributes on a handler's Execute/ExecuteAsync method and composed by PipelineBuilder into a decorator chain (highest step outermost), with each decorator receiving next and fallback delegates." +tags: + - "pipeline" + - "decorators" + - "architecture" +--- + # 2. Attribute-Driven Decorator Pipeline for Query Handling Date: 2026-05-15 diff --git a/docs/adr/0003-fluent-builder-for-query-processor.md b/docs/adr/0003-fluent-builder-for-query-processor.md index 27a04b10..efd285c4 100644 --- a/docs/adr/0003-fluent-builder-for-query-processor.md +++ b/docs/adr/0003-fluent-builder-for-query-processor.md @@ -1,3 +1,16 @@ +--- +id: 0003-fluent-builder-for-query-processor +title: "Fluent Builder with Interface Progression for QueryProcessor Construction" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "QueryProcessor is constructed through a fluent QueryProcessorBuilder using interface progression (INeedHandlers, INeedAQueryContext, IBuildTheQueryProcessor) so the required construction order is enforced at compile time." +tags: + - "api-design" + - "configuration" +--- + # 3. Fluent Builder with Interface Progression for QueryProcessor Construction Date: 2026-05-15 diff --git a/docs/adr/0004-factory-registry-abstractions.md b/docs/adr/0004-factory-registry-abstractions.md index f5e719cb..bcf017f0 100644 --- a/docs/adr/0004-factory-registry-abstractions.md +++ b/docs/adr/0004-factory-registry-abstractions.md @@ -1,3 +1,17 @@ +--- +id: 0004-factory-registry-abstractions +title: "Factory and Registry Abstractions for DI Container Independence" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "Defines four abstractions (IQueryHandlerRegistry, IQueryHandlerFactory, decorator registry and decorator factory) separating registration from creation and handlers from decorators, so Darker is independent of any DI container." +tags: + - "architecture" + - "di" + - "dependencies" +--- + # 4. Factory and Registry Abstractions for DI Container Independence Date: 2026-05-15 diff --git a/docs/adr/0005-dual-sync-async-support.md b/docs/adr/0005-dual-sync-async-support.md index 10b7ff5d..a655ce20 100644 --- a/docs/adr/0005-dual-sync-async-support.md +++ b/docs/adr/0005-dual-sync-async-support.md @@ -1,3 +1,16 @@ +--- +id: 0005-dual-sync-async-support +title: "Dual Sync/Async Handler Support" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "Offers one IQueryHandler interface with Execute/Fallback and ExecuteAsync/FallbackAsync, plus separate QueryHandler and QueryHandlerAsync base classes that each implement one path and throw NotImplementedException for the other." +tags: + - "async" + - "api-design" +--- + # 5. Dual Sync/Async Handler Support Date: 2026-05-15 diff --git a/docs/adr/0006-remove-third-party-di-packages.md b/docs/adr/0006-remove-third-party-di-packages.md index 9ed460d0..86666e1c 100644 --- a/docs/adr/0006-remove-third-party-di-packages.md +++ b/docs/adr/0006-remove-third-party-di-packages.md @@ -1,3 +1,17 @@ +--- +id: 0006-remove-third-party-di-packages +title: "Remove Third-Party DI Container Integration Packages" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "Removes the SimpleInjector and LightInject integration packages and keeps only the Microsoft.Extensions.DependencyInjection-based package as the single supported DI integration." +tags: + - "di" + - "packaging" + - "dependencies" +--- + # 6. Remove Third-Party DI Container Integration Packages Date: 2026-05-15 diff --git a/docs/adr/0007-rename-di-package.md b/docs/adr/0007-rename-di-package.md index 4fdd3a63..0d25a426 100644 --- a/docs/adr/0007-rename-di-package.md +++ b/docs/adr/0007-rename-di-package.md @@ -1,3 +1,16 @@ +--- +id: 0007-rename-di-package +title: "Rename DI Integration Package from AspNetCore to Extensions.DependencyInjection" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-15 +summary: "Renames Paramore.Darker.AspNetCore to Paramore.Darker.Extensions.DependencyInjection as a purely structural change with an unchanged public API surface." +tags: + - "di" + - "packaging" +--- + # 7. Rename DI Integration Package from AspNetCore to Extensions.DependencyInjection Date: 2026-05-15 diff --git a/docs/adr/0008-split-handler-interfaces.md b/docs/adr/0008-split-handler-interfaces.md index b083c78d..88710e2f 100644 --- a/docs/adr/0008-split-handler-interfaces.md +++ b/docs/adr/0008-split-handler-interfaces.md @@ -1,3 +1,17 @@ +--- +id: 0008-split-handler-interfaces +title: "Split Handler Interfaces into Separate Sync and Async Types" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-18 +summary: "Splits handler, decorator, attribute and infrastructure types into fully separate sync and async hierarchies (IQueryHandler vs IQueryHandlerAsync), so no type serves both execution paths." +tags: + - "async" + - "api-design" + - "architecture" +--- + # 8. Split Handler Interfaces into Separate Sync and Async Types Date: 2026-05-18 diff --git a/docs/adr/0009-simple-and-inmemory-factory-implementations.md b/docs/adr/0009-simple-and-inmemory-factory-implementations.md index cdd0d4f0..e1acfa1d 100644 --- a/docs/adr/0009-simple-and-inmemory-factory-implementations.md +++ b/docs/adr/0009-simple-and-inmemory-factory-implementations.md @@ -1,3 +1,16 @@ +--- +id: 0009-simple-and-inmemory-factory-implementations +title: "Simple and InMemory Factory Implementations for Testing and Lightweight Usage" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-18 +summary: "Adds public delegate-based SimpleHandlerFactory, SimpleHandlerDecoratorFactory and InMemory registry/context-factory implementations to core, following Brighter, for testing and DI-free usage." +tags: + - "testing" + - "di" +--- + # 9. Simple and InMemory Factory Implementations for Testing and Lightweight Usage Date: 2026-05-18 diff --git a/docs/adr/0010-pass-query-context.md b/docs/adr/0010-pass-query-context.md index c555c782..8dfe9e14 100644 --- a/docs/adr/0010-pass-query-context.md +++ b/docs/adr/0010-pass-query-context.md @@ -1,3 +1,16 @@ +--- +id: 0010-pass-query-context +title: "Pass QueryContext into QueryProcessor" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-20 +summary: "Allows a caller to pass an external IQueryContext into IQueryProcessor.Execute/ExecuteAsync, introduces InitQueryContext, adds a typed Policies property to the context, and removes the contextBagData mechanism." +tags: + - "query-context" + - "api-design" +--- + # 10. Pass QueryContext into QueryProcessor Date: 2026-05-20 diff --git a/docs/adr/0011-merge-builtin-decorators.md b/docs/adr/0011-merge-builtin-decorators.md index ff03e64e..3009ba14 100644 --- a/docs/adr/0011-merge-builtin-decorators.md +++ b/docs/adr/0011-merge-builtin-decorators.md @@ -1,3 +1,17 @@ +--- +id: 0011-merge-builtin-decorators +title: "Merge Builtin Decorators (Policies and QueryLogging) Into Core" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-26 +summary: "Merges the Policies and QueryLogging side packages into core Paramore.Darker under Policies/ and Logging/ folders, mirroring Brighter's layout and relocating the FallbackPolicy types alongside." +tags: + - "packaging" + - "decorators" + - "dependencies" +--- + # 11. Merge Builtin Decorators (Policies and QueryLogging) Into Core Date: 2026-05-26 diff --git a/docs/adr/0012-json-serializer-swap.md b/docs/adr/0012-json-serializer-swap.md index 11973c61..15503fd6 100644 --- a/docs/adr/0012-json-serializer-swap.md +++ b/docs/adr/0012-json-serializer-swap.md @@ -1,3 +1,17 @@ +--- +id: 0012-json-serializer-swap +title: "Swap Newtonsoft.Json for System.Text.Json in the Query Logging Decorator" +status: Accepted +author: + - "Ian Cooper" +created: 2026-05-29 +summary: "Replaces Newtonsoft.Json with System.Text.Json in the query logging decorator, configured through a mutable static QueryLoggingJsonOptions (IgnoreCycles default) with AOT/trim warnings suppressed and left to the consumer." +tags: + - "serialization" + - "logging" + - "dependencies" +--- + # 12. Swap Newtonsoft.Json for System.Text.Json in the Query Logging Decorator Date: 2026-05-29 diff --git a/docs/adr/0013-migrate-legacy-tests-to-test-doubles.md b/docs/adr/0013-migrate-legacy-tests-to-test-doubles.md index 26955eff..ab66c00d 100644 --- a/docs/adr/0013-migrate-legacy-tests-to-test-doubles.md +++ b/docs/adr/0013-migrate-legacy-tests-to-test-doubles.md @@ -1,3 +1,15 @@ +--- +id: 0013-migrate-legacy-tests-to-test-doubles +title: "Migrate Legacy Tests to Simple/InMemory Doubles and a TestDoubles Directory" +status: Accepted +author: + - "Ian Cooper" +created: 2026-06-05 +summary: "Migrates legacy Moq-based tests to Simple/InMemory doubles and recording doubles, and keeps two distinct directories: Exported/ for scanned public handlers and TestDoubles/ for internal doubles." +tags: + - "testing" +--- + # 13. Migrate Legacy Tests to Simple/InMemory Doubles and a TestDoubles Directory Date: 2026-06-05 diff --git a/docs/adr/0014-factory-component-lifetime.md b/docs/adr/0014-factory-component-lifetime.md index 31756b51..b832863f 100644 --- a/docs/adr/0014-factory-component-lifetime.md +++ b/docs/adr/0014-factory-component-lifetime.md @@ -1,3 +1,17 @@ +--- +id: 0014-factory-component-lifetime +title: "Lifetime-Aware Handler and Decorator Factories with a Per-Query Lifetime Scope" +status: Accepted +author: + - "Ian Cooper" +created: 2026-06-08 +summary: "Makes handler and decorator factories lifetime-aware via a per-query IAmALifetime owning a child IServiceScope that PipelineBuilder creates and disposes, resolving Singleton from a shared cache and Scoped/Transient from the child scope." +tags: + - "lifetime" + - "di" + - "pipeline" +--- + # 14. Lifetime-Aware Handler and Decorator Factories with a Per-Query Lifetime Scope Date: 2026-06-08 diff --git a/docs/adr/0015-resilience-pipeline-integration.md b/docs/adr/0015-resilience-pipeline-integration.md index 51c3a833..eec15e10 100644 --- a/docs/adr/0015-resilience-pipeline-integration.md +++ b/docs/adr/0015-resilience-pipeline-integration.md @@ -1,3 +1,17 @@ +--- +id: 0015-resilience-pipeline-integration +title: "Polly V8 Resilience Pipeline Integration" +status: Accepted +author: + - "Ian Cooper" +created: 2026-06-15 +summary: "Adds a Polly V8 resilience-pipeline integration parallel to the legacy policy path, exposing pipelines via IQueryContext, new decorators and attributes, type-scoped generic pipelines keyed by result type, and default pipelines via DI." +tags: + - "resilience" + - "retry" + - "circuit-breaker" +--- + # 15. Polly V8 Resilience Pipeline Integration Date: 2026-06-15 diff --git a/docs/adr/0016-pipeline-attribute-memoization.md b/docs/adr/0016-pipeline-attribute-memoization.md index 79f70337..e6352e6a 100644 --- a/docs/adr/0016-pipeline-attribute-memoization.md +++ b/docs/adr/0016-pipeline-attribute-memoization.md @@ -1,3 +1,17 @@ +--- +id: 0016-pipeline-attribute-memoization +title: "Memoise Ordered Decorator Attributes Per Handler Type" +status: Accepted +author: + - "Ian Cooper" +created: 2026-06-24 +summary: "Memoises the ordered decorator attributes per handler Type in static ConcurrentDictionary caches (separate for sync and async) inside PipelineBuilder attribute discovery, leaving instance creation and wiring uncached." +tags: + - "performance" + - "pipeline" + - "concurrency" +--- + # 16. Memoise Ordered Decorator Attributes Per Handler Type Date: 2026-06-24 diff --git a/docs/adr/0017-query-tracing-and-database-spans.md b/docs/adr/0017-query-tracing-and-database-spans.md index 85683522..82f9d14d 100644 --- a/docs/adr/0017-query-tracing-and-database-spans.md +++ b/docs/adr/0017-query-tracing-and-database-spans.md @@ -1,3 +1,17 @@ +--- +id: 0017-query-tracing-and-database-spans +title: "Query Tracing and Database Spans" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-01 +summary: "Introduces an Observability namespace with DarkerTracer, InstrumentationOptions and DB spans; QueryProcessor owns the query span, PipelineBuilder emits step events, and a Diagnostics package wires the source into OpenTelemetry." +tags: + - "observability" + - "otel" + - "tracing" +--- + # 17. Query Tracing and Database Spans Date: 2026-07-01 diff --git a/docs/adr/0018-metrics-from-query-traces.md b/docs/adr/0018-metrics-from-query-traces.md index d9e3518e..76b11f51 100644 --- a/docs/adr/0018-metrics-from-query-traces.md +++ b/docs/adr/0018-metrics-from-query-traces.md @@ -1,3 +1,17 @@ +--- +id: 0018-metrics-from-query-traces +title: "Metrics from Query Traces" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-01 +summary: "Derives metrics from query traces with an OpenTelemetry processor that records duration histograms on span end for a query meter and a DB meter, deriving counts and failures from the histogram and an error.type dimension." +tags: + - "metrics" + - "otel" + - "observability" +--- + # 18. Metrics from Query Traces Date: 2026-07-01 diff --git a/docs/adr/0019-streaming-query-pipeline.md b/docs/adr/0019-streaming-query-pipeline.md index 96e7ba77..e52d0bd1 100644 --- a/docs/adr/0019-streaming-query-pipeline.md +++ b/docs/adr/0019-streaming-query-pipeline.md @@ -1,3 +1,17 @@ +--- +id: 0019-streaming-query-pipeline +title: "Streaming Query Pipeline" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-09 +summary: "Adds a fully parallel streaming path built around IStreamQuery<TResult> (TResult is the item type) with its own handlers, decorators, factories and lifetime, expressed in Darker's Execute-style idiom." +tags: + - "streaming" + - "pipeline" + - "async" +--- + # 19. Streaming Query Pipeline Date: 2026-07-09 diff --git a/docs/adr/0020-agreement-dispatch-handler-routing.md b/docs/adr/0020-agreement-dispatch-handler-routing.md index b2ffac77..5208cb3b 100644 --- a/docs/adr/0020-agreement-dispatch-handler-routing.md +++ b/docs/adr/0020-agreement-dispatch-handler-routing.md @@ -1,3 +1,16 @@ +--- +id: 0020-agreement-dispatch-handler-routing +title: "Agreement Dispatch: Content- and Context-Based Handler Routing" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-14 +summary: "Models every registration as a handler route and gives the registry a single instance-aware lookup, so type-based registration and agreement dispatch (content- and context-based handler selection) are two implementations of one routing role." +tags: + - "dispatch" + - "architecture" +--- + # 20. Agreement Dispatch: Content- and Context-Based Handler Routing Date: 2026-07-14 diff --git a/docs/adr/0020-validation-decorator-architecture.md b/docs/adr/0020-validation-decorator-architecture.md index 5addeee0..70cc8d5d 100644 --- a/docs/adr/0020-validation-decorator-architecture.md +++ b/docs/adr/0020-validation-decorator-architecture.md @@ -1,3 +1,16 @@ +--- +id: 0020-validation-decorator-architecture +title: "Validation Decorator Architecture" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-18 +summary: "Adds a provider-agnostic template-method validation decorator in core (ValidateQuery attribute, QueryValidationError/Exception) with FluentValidation and DataAnnotations provider packages mapped in DI via Brighter-style Use* extensions." +tags: + - "validation" + - "decorators" +--- + # 20. Validation Decorator Architecture Date: 2026-07-18 diff --git a/docs/adr/0021-caching-decorator-architecture.md b/docs/adr/0021-caching-decorator-architecture.md index 82cd1c18..c66e3a4f 100644 --- a/docs/adr/0021-caching-decorator-architecture.md +++ b/docs/adr/0021-caching-decorator-architecture.md @@ -1,3 +1,17 @@ +--- +id: 0021-caching-decorator-architecture +title: "Caching Decorator Architecture" +status: Accepted +author: + - "Ian Cooper" +created: 2026-07-20 +summary: "Adds a caching decorator over Microsoft's HybridCache that short-circuits the pipeline on a hit, with a replaceable ICacheKeyGenerator, a required expiry, an optional eviction tag via the context Bag, and hit/miss recorded as a span attribute and derived as a metric." +tags: + - "decorators" + - "caching" + - "performance" +--- + # 21. Caching Decorator Architecture Date: 2026-07-20 diff --git a/docs/adr/index.md b/docs/adr/index.md new file mode 100644 index 00000000..4cfe7410 --- /dev/null +++ b/docs/adr/index.md @@ -0,0 +1,39 @@ +<!-- GENERATED FILE - DO NOT EDIT BY HAND. + Regenerated from ADR frontmatter by /adr and /spec:approve + (see .agent_instructions/adr_frontmatter.md). Edit the ADRs, not this file. --> + +# Architecture Decision Records — Index + +Derived index of every ADR in `docs/adr/`, generated from each file's YAML frontmatter. +It is a regenerable cache: **do not hand-edit**. To refresh it after adding or changing an +ADR, rerun the generator documented in +[`.agent_instructions/adr_frontmatter.md`](../../.agent_instructions/adr_frontmatter.md). + +Identity is the `id` (filename stem), not the leading number — numbers may repeat (e.g. two ADRs are numbered 20). + +| id | Title | Status | Tags | Summary | +|----|-------|--------|------|---------| +| `0001-record-architecture-decisions` | Record Architecture Decisions | Accepted | meta | Adopts Nygard-style Architecture Decision Records stored as numbered markdown files in docs/adr/ with a fixed Status/Context/Decision/Consequences/Alternatives template, and retroactively documents existing decisions. | +| `0002-attribute-driven-decorator-pipeline` | Attribute-Driven Decorator Pipeline for Query Handling | Accepted | pipeline, decorators, architecture | Cross-cutting concerns are declared as step-ordered attributes on a handler's Execute/ExecuteAsync method and composed by PipelineBuilder into a decorator chain (highest step outermost), with each decorator receiving next and fallback delegates. | +| `0003-fluent-builder-for-query-processor` | Fluent Builder with Interface Progression for QueryProcessor Construction | Accepted | api-design, configuration | QueryProcessor is constructed through a fluent QueryProcessorBuilder using interface progression (INeedHandlers, INeedAQueryContext, IBuildTheQueryProcessor) so the required construction order is enforced at compile time. | +| `0004-factory-registry-abstractions` | Factory and Registry Abstractions for DI Container Independence | Accepted | architecture, di, dependencies | Defines four abstractions (IQueryHandlerRegistry, IQueryHandlerFactory, decorator registry and decorator factory) separating registration from creation and handlers from decorators, so Darker is independent of any DI container. | +| `0005-dual-sync-async-support` | Dual Sync/Async Handler Support | Accepted | async, api-design | Offers one IQueryHandler interface with Execute/Fallback and ExecuteAsync/FallbackAsync, plus separate QueryHandler and QueryHandlerAsync base classes that each implement one path and throw NotImplementedException for the other. | +| `0006-remove-third-party-di-packages` | Remove Third-Party DI Container Integration Packages | Accepted | di, packaging, dependencies | Removes the SimpleInjector and LightInject integration packages and keeps only the Microsoft.Extensions.DependencyInjection-based package as the single supported DI integration. | +| `0007-rename-di-package` | Rename DI Integration Package from AspNetCore to Extensions.DependencyInjection | Accepted | di, packaging | Renames Paramore.Darker.AspNetCore to Paramore.Darker.Extensions.DependencyInjection as a purely structural change with an unchanged public API surface. | +| `0008-split-handler-interfaces` | Split Handler Interfaces into Separate Sync and Async Types | Accepted | async, api-design, architecture | Splits handler, decorator, attribute and infrastructure types into fully separate sync and async hierarchies (IQueryHandler vs IQueryHandlerAsync), so no type serves both execution paths. | +| `0009-simple-and-inmemory-factory-implementations` | Simple and InMemory Factory Implementations for Testing and Lightweight Usage | Accepted | testing, di | Adds public delegate-based SimpleHandlerFactory, SimpleHandlerDecoratorFactory and InMemory registry/context-factory implementations to core, following Brighter, for testing and DI-free usage. | +| `0010-pass-query-context` | Pass QueryContext into QueryProcessor | Accepted | query-context, api-design | Allows a caller to pass an external IQueryContext into IQueryProcessor.Execute/ExecuteAsync, introduces InitQueryContext, adds a typed Policies property to the context, and removes the contextBagData mechanism. | +| `0011-merge-builtin-decorators` | Merge Builtin Decorators (Policies and QueryLogging) Into Core | Accepted | packaging, decorators, dependencies | Merges the Policies and QueryLogging side packages into core Paramore.Darker under Policies/ and Logging/ folders, mirroring Brighter's layout and relocating the FallbackPolicy types alongside. | +| `0012-json-serializer-swap` | Swap Newtonsoft.Json for System.Text.Json in the Query Logging Decorator | Accepted | serialization, logging, dependencies | Replaces Newtonsoft.Json with System.Text.Json in the query logging decorator, configured through a mutable static QueryLoggingJsonOptions (IgnoreCycles default) with AOT/trim warnings suppressed and left to the consumer. | +| `0013-migrate-legacy-tests-to-test-doubles` | Migrate Legacy Tests to Simple/InMemory Doubles and a TestDoubles Directory | Accepted | testing | Migrates legacy Moq-based tests to Simple/InMemory doubles and recording doubles, and keeps two distinct directories: Exported/ for scanned public handlers and TestDoubles/ for internal doubles. | +| `0014-factory-component-lifetime` | Lifetime-Aware Handler and Decorator Factories with a Per-Query Lifetime Scope | Accepted | lifetime, di, pipeline | Makes handler and decorator factories lifetime-aware via a per-query IAmALifetime owning a child IServiceScope that PipelineBuilder creates and disposes, resolving Singleton from a shared cache and Scoped/Transient from the child scope. | +| `0015-resilience-pipeline-integration` | Polly V8 Resilience Pipeline Integration | Accepted | resilience, retry, circuit-breaker | Adds a Polly V8 resilience-pipeline integration parallel to the legacy policy path, exposing pipelines via IQueryContext, new decorators and attributes, type-scoped generic pipelines keyed by result type, and default pipelines via DI. | +| `0016-pipeline-attribute-memoization` | Memoise Ordered Decorator Attributes Per Handler Type | Accepted | performance, pipeline, concurrency | Memoises the ordered decorator attributes per handler Type in static ConcurrentDictionary caches (separate for sync and async) inside PipelineBuilder attribute discovery, leaving instance creation and wiring uncached. | +| `0017-query-tracing-and-database-spans` | Query Tracing and Database Spans | Accepted | observability, otel, tracing | Introduces an Observability namespace with DarkerTracer, InstrumentationOptions and DB spans; QueryProcessor owns the query span, PipelineBuilder emits step events, and a Diagnostics package wires the source into OpenTelemetry. | +| `0018-metrics-from-query-traces` | Metrics from Query Traces | Accepted | metrics, otel, observability | Derives metrics from query traces with an OpenTelemetry processor that records duration histograms on span end for a query meter and a DB meter, deriving counts and failures from the histogram and an error.type dimension. | +| `0019-streaming-query-pipeline` | Streaming Query Pipeline | Accepted | streaming, pipeline, async | Adds a fully parallel streaming path built around IStreamQuery<TResult> (TResult is the item type) with its own handlers, decorators, factories and lifetime, expressed in Darker's Execute-style idiom. | +| `0020-agreement-dispatch-handler-routing` | Agreement Dispatch: Content- and Context-Based Handler Routing | Accepted | dispatch, architecture | Models every registration as a handler route and gives the registry a single instance-aware lookup, so type-based registration and agreement dispatch (content- and context-based handler selection) are two implementations of one routing role. | +| `0020-validation-decorator-architecture` | Validation Decorator Architecture | Accepted | validation, decorators | Adds a provider-agnostic template-method validation decorator in core (ValidateQuery attribute, QueryValidationError/Exception) with FluentValidation and DataAnnotations provider packages mapped in DI via Brighter-style Use* extensions. | +| `0021-caching-decorator-architecture` | Caching Decorator Architecture | Accepted | decorators, caching, performance | Adds a caching decorator over Microsoft's HybridCache that short-circuits the pipeline on a hit, with a replaceable ICacheKeyGenerator, a required expiry, an optional eviction tag via the context Bag, and hit/miss recorded as a span attribute and derived as a metric. | + +_22 ADRs indexed._ From 7162dbc2b4423f87ce110a87b28bebebf292a992 Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 09:26:04 +0100 Subject: [PATCH 03/12] feat(skills): ADR metadata skills, /spec:write_release_notes, /spec:tasks sub-agent Port from Brighter: - read_adr_metadata / write_adr_metadata skills. /adr and /spec:design surface prior art from frontmatter, stamp new ADRs' frontmatter and regenerate docs/adr/index.md; /spec:approve design flips status via write_adr_metadata, handles supersession, and regenerates the index. - /spec:write_release_notes writes or replaces a spec's marked section under the first ## heading of a new root release_notes.md. /spec:design recommends it when an ADR records a breaking change; /spec:review design flags a breaking change with no marked section. - /spec:tasks delegates drafting to a Plan sub-agent on opus, which returns the list plus an FR/ADR coverage cross-reference; the main agent validates and writes tasks.md. - /spec:review tasks checks tag-first labels, the gate line, and named RED mutations on CHARACTERISE tasks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agent_instructions/documentation.md | 5 + .agent_instructions/skills_overview.md | 2 + .claude/commands/README.md | 4 +- .claude/commands/adr/README.md | 29 +++ .claude/commands/adr/adr.md | 63 +++++- .claude/commands/adr/read_adr_metadata.md | 109 ++++++++++ .claude/commands/adr/write_adr_metadata.md | 137 +++++++++++++ .claude/commands/spec/README.md | 55 ++++- .claude/commands/spec/approve.md | 49 ++++- .claude/commands/spec/design.md | 66 +++++- .claude/commands/spec/review.md | 22 ++ .claude/commands/spec/tasks.md | 112 ++++++++-- .claude/commands/spec/write_release_notes.md | 204 +++++++++++++++++++ CLAUDE.md | 1 + release_notes.md | 8 + 15 files changed, 817 insertions(+), 49 deletions(-) create mode 100644 .claude/commands/adr/read_adr_metadata.md create mode 100644 .claude/commands/adr/write_adr_metadata.md create mode 100644 .claude/commands/spec/write_release_notes.md create mode 100644 release_notes.md diff --git a/.agent_instructions/documentation.md b/.agent_instructions/documentation.md index 10561d69..79df1fb6 100644 --- a/.agent_instructions/documentation.md +++ b/.agent_instructions/documentation.md @@ -62,6 +62,11 @@ Scan the ADR directory for existing ADRs to determine the next [Sequence Number] Use dash-case (aka kebab-case) for the [Title] of the ADR. +Every ADR carries a YAML frontmatter block (`id`, `title`, `status`, `author`, `created`, `summary`, +`tags`). The schema, status vocabulary and tag taxonomy are in +[adr_frontmatter.md](adr_frontmatter.md); `/adr` and `/spec:design` stamp it with the +`write_adr_metadata` skill, and `docs/adr/index.md` is generated from it — never hand-edit the index. + ## Licensing - We add a license comment to every src file diff --git a/.agent_instructions/skills_overview.md b/.agent_instructions/skills_overview.md index c764e6c8..b12a3411 100644 --- a/.agent_instructions/skills_overview.md +++ b/.agent_instructions/skills_overview.md @@ -41,6 +41,7 @@ Skills are slash commands that automate multi-step workflows and enforce Darker' | `/spec:approve` | Approve phases | `/spec:approve <phase> [adr-number]` | | `/spec:review` | Review phases | `/spec:review [phase] [adr-number]` | | `/spec:switch` | Switch to different spec | `/spec:switch <spec-name>` | +| `/spec:write_release_notes` | Write the spec's marked section in `release_notes.md` | `/spec:write_release_notes [spec-id]` | | `/spec:gear` | Report or shift the TDD review gear | `/spec:gear [review-before\|review-after] ["section"] [--because "..."]` | | `/spec:ralph-implement` | Unattended TDD implementation over the approved `tasks.md` | `/spec:ralph-implement [count]` | @@ -140,6 +141,7 @@ Each skill enforces specific practices from `.agent_instructions/`: - Dash-case file naming - Linking to parent requirements - Tracking in spec's `.adr-list` +- YAML frontmatter (see `adr_frontmatter.md`) and a regenerated `docs/adr/index.md` **Reference**: [documentation.md](documentation.md) diff --git a/.claude/commands/README.md b/.claude/commands/README.md index ddbefea4..774f7ce3 100644 --- a/.claude/commands/README.md +++ b/.claude/commands/README.md @@ -141,7 +141,8 @@ Documentation: [.claude/commands/bugfix/README.md](bugfix/README.md) - **`/bugfix:*`** - Diagnosis-first bug workflow (Triage → Confirm → Test-first → Fix → Verify) ### Documentation Skills -- **`/adr`** - Architecture Decision Records +- **`/adr`** - Architecture Decision Records (stamps YAML frontmatter, regenerates `docs/adr/index.md`) +- **`read_adr_metadata`** / **`write_adr_metadata`** - Find prior-art ADRs from frontmatter; add or update an ADR's frontmatter and status ### Specification Workflow Skills - **`/spec:requirements`** - Capture requirements @@ -153,6 +154,7 @@ Documentation: [.claude/commands/bugfix/README.md](bugfix/README.md) - **`/spec:status`** - Show spec status (including the review gear) - **`/spec:approve`** - Approve phases - **`/spec:review`** - Review phases +- **`/spec:write_release_notes`** - Write or replace the spec's marked section in `release_notes.md` Documentation: [.claude/commands/spec/README.md](spec/README.md) diff --git a/.claude/commands/adr/README.md b/.claude/commands/adr/README.md index d87f2a94..26b3ed51 100644 --- a/.claude/commands/adr/README.md +++ b/.claude/commands/adr/README.md @@ -28,6 +28,35 @@ Creates a new Architecture Decision Record following Darker's template and conve - Dash-case (kebab-case) title - Example: `0002-query-caching-strategy.md` +## ADR Metadata (frontmatter & index) + +Every ADR carries a YAML **frontmatter** block (`id`, `title`, `status`, `author`, `created`, +`summary`, `tags`) so agents can find prior art from cheap metadata instead of grepping full bodies. +The schema, status vocabulary, tag taxonomy, and identity rule are defined once in +[`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). + +Two skills manage it (both are invoked *by* `/adr`, `/spec:design`, and `/spec:approve`, but can be +run directly): + +- **`read_adr_metadata`** (`read_adr_metadata.md`) — reads only the frontmatter of each ADR and + returns ranked prior-art candidates (`id` + `title` + `summary`) filtered by tag/status. Skips + `Deprecated`/`Superseded` by default. Used before drafting to surface related decisions. +- **`write_adr_metadata`** (`write_adr_metadata.md`) — adds/updates a file's frontmatter and keeps it + in sync with the body `## Status` (`init` → `Proposed`; `status Accepted` on approval; + `supersede`/`deprecate` on retirement). Idempotent. + +**Identity is the `id`/slug (filename stem), not the number.** ADR numbers are a non-unique ordering +hint — parallel branches can collide on numbers (two ADRs are numbered `0020`) and we do not +renumber. Tooling keys off `id`. + +**Derived index** — `docs/adr/index.md` is a table generated *from* the frontmatter. It is a +regenerable cache: **never hand-edit it**. `/adr`, `/spec:design` and `/spec:approve` refresh it +automatically; to regenerate by hand run the single canonical command: + +```bash +awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md +``` + ## Integration with Specification Workflow The `/adr` command integrates with the [specification workflow](../spec/README.md): diff --git a/.claude/commands/adr/adr.md b/.claude/commands/adr/adr.md index baaa9005..5c6a23bc 100644 --- a/.claude/commands/adr/adr.md +++ b/.claude/commands/adr/adr.md @@ -1,5 +1,5 @@ --- -allowed-tools: Read, Write, Glob, Bash, AskUserQuestion +allowed-tools: Read, Write, Edit, Glob, Bash, AskUserQuestion, Skill description: Create Architecture Decision Record following Darker's template argument-hint: <title-of-decision> --- @@ -26,7 +26,16 @@ ls -1 docs/adr/*.md | grep -E '^docs/adr/[0-9]+' | sed 's/docs\/adr\/\([0-9]*\). If `docs/adr/` does not exist, create it and start with number `0001`. -The next ADR number should be the highest number + 1, formatted as a 4-digit number with leading zeros (e.g., `0037`). +The next ADR number is the highest number + 1, formatted as a 4-digit number with leading zeros +(e.g., `0022`). + +**The number is a non-unique ordering hint, not the ADR's identity.** Numbers are allocated `max+1` +per branch without locking, so parallel branches can collide — `0020` is already shared by two ADRs, +and we do **not** renumber. The **identity is the `id`/slug = the filename stem** +(`0020-validation-decorator-architecture`), which is unique; all tooling keys off that. So: take +`max+1`, but if a concurrent branch later lands the same number, that collision is **acceptable and +not a defect** — do not renumber to "fix" it. See *Identity and numbering* in +[`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). ### Step 2: Check for Linked Specification @@ -53,6 +62,19 @@ If the first ADR does not exist yet, use the template from Step 5 directly. ### Step 4: Gather Information from User +**Surface prior art first.** Before gathering new content, find existing ADRs relevant to this +decision so you reuse, avoid contradicting, or deliberately supersede them rather than re-deciding in +ignorance. Use the `read_adr_metadata` skill, passing the decision title as the query and any +obvious tags from the taxonomy: + +``` +read_adr_metadata "{decision title}" --tags "{likely-tags}" +``` + +It reads only frontmatter (cheap) and skips `Deprecated`/`Superseded` ADRs by default. Show the user +the returned prior-art candidates (`id` + `title` + `summary`), and note any this ADR would supersede +(you will mark that older ADR `Superseded` in Step 5). + Use AskUserQuestion to gather the key information needed for the ADR: **Question 1: What is the architectural problem or decision to be made?** @@ -158,7 +180,34 @@ Proposed - [Link to external documentation, articles, etc.] ``` -### Step 6: Update Spec ADR List (if applicable) +**Add frontmatter**: after writing the body above, stamp the YAML frontmatter block with the +`write_adr_metadata` skill. It derives `id`/`title`/`created` from the file, sets `status: Proposed`, +and keeps the frontmatter in sync with the body `## Status`. Provide a `summary` (1–2 sentences on +WHAT was decided — an agent reads this to decide whether to open the ADR) and 1–4 `tags` from the +controlled taxonomy in [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md): + +``` +write_adr_metadata docs/adr/[NNNN]-[title].md init --summary "..." --tags "tag-a,tag-b" +``` + +Confirm the six checks the skill reports pass (`id` == filename stem, `status` ∈ enum, tags ⊆ +taxonomy, valid date, all seven keys, frontmatter status matches body). If this ADR **supersedes** a +prior one you found in Step 4, also mark that older ADR: +`write_adr_metadata docs/adr/[old-id].md supersede --by [NNNN]-[title]`. + +### Step 6: Regenerate the ADR index + +The new frontmatter (and any supersession from Step 5) must be reflected in the derived +`docs/adr/index.md`. Regenerate it — this is the single canonical command (documented in +[`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md)): + +```bash +awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md +``` + +`docs/adr/index.md` is a regenerable cache — never hand-edit it; always regenerate from frontmatter. + +### Step 7: Update Spec ADR List (if applicable) If linked to a spec, append this ADR to the spec's ADR list: @@ -166,7 +215,7 @@ If linked to a spec, append this ADR to the spec's ADR list: echo "docs/adr/[NNNN]-[title].md" >> specs/[spec-name]/.adr-list ``` -### Step 7: Inform the User +### Step 8: Inform the User Tell the user: 1. The ADR file path and number @@ -177,7 +226,7 @@ Tell the user: - Use `/spec:approve design [NNNN]` when approved (changes status to "Accepted") -### Step 8: Review against the Design Principles +### Step 9: Review against the Design Principles Read the ADR: @@ -190,12 +239,12 @@ Read the ADR: - A class can implement one or more roles. If it implements multiple roles, they should be related. - Provide feedback and suggest improvements -### Step 9. Suggest next steps: +### Step 10. Suggest next steps: Ask the user for the next step: - Create a feature branch if not already on one - - Commit the ADR: `git commit -m "docs: add ADR for [title]"` + - Commit the ADR and the regenerated index: `git add docs/adr/[NNNN]-[title].md docs/adr/index.md && git commit -m "docs: add ADR for [title]"` - Continue with additional ADRs if needed - Or proceed to `/spec:tasks` if design is complete diff --git a/.claude/commands/adr/read_adr_metadata.md b/.claude/commands/adr/read_adr_metadata.md new file mode 100644 index 00000000..2571a2c4 --- /dev/null +++ b/.claude/commands/adr/read_adr_metadata.md @@ -0,0 +1,109 @@ +--- +allowed-tools: Read, Glob, Grep, Bash(awk:*), Bash(ls:*), Bash(sed:*), Bash(grep:*) +description: Extract ADR frontmatter cheaply and return prior-art candidates filtered by tag/status +argument-hint: [<focus text>] [--tags a,b,c] [--status S,...] [--include-retired] [--limit N] +--- + +# Read ADR Metadata + +You are surfacing **prior-art ADRs** relevant to a focus area by reading only each ADR's YAML +**frontmatter** — never the bodies. This is the *read* half of the ADR metadata pair (the write half +is `write_adr_metadata`). It is deliberately cheap: the frontmatter is a few lines per ADR, so an +agent can scan the whole corpus without spending its context budget on full ADR text. + +**Source of truth**: the schema, status vocabulary, tag taxonomy, and identity rule live in +[`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). + +## Arguments + +``` +$ARGUMENTS +``` + +Parse `$ARGUMENTS` as an optional free-text **focus** plus optional flags: + +| Argument | Effect | +|---------------------|--------| +| `<focus text>` | The topic of the new design. Used to rank candidates by relevance. | +| `--tags a,b,c` | Restrict to ADRs carrying **any** of these tags (drawn from the taxonomy). | +| `--status S,...` | Restrict to these statuses. Overrides the default retired-skip filter. | +| `--include-retired` | Include `Deprecated` and `Superseded` ADRs (off by default). | +| `--limit N` | Return at most N candidates (default 8). | + +With no focus and no filters, return the full set of in-force ADRs (`Proposed` + `Accepted`). + +## Rules + +- **Read frontmatter only.** Never open an ADR body to answer this — that defeats the purpose. +- **Skip retired ADRs by default.** Exclude `Deprecated` and `Superseded` unless `--include-retired` + or an explicit `--status` says otherwise. Retired decisions must not be offered as live prior art. +- **Identity is the `id`/slug, never the bare number.** Key, dedupe, and report every result by + `id`. Duplicate numbers across ADRs are expected and harmless — do not collapse two ADRs just + because they share a number. +- Report **paths relative to the repo root** (`docs/adr/<id>.md`). + +## Procedure + +### Step 1 — Extract every frontmatter block (one cheap call) + +Pull the block between the first two `---` fences of every ADR in a single pass: + +```bash +awk 'FNR==1{n=0; print "===FILE:" FILENAME} /^---$/{n++; next} n==1{print}' docs/adr/*.md +``` + +Each `===FILE:<path>` marker is followed by that ADR's frontmatter lines (`id`, `title`, `status`, +`author`, `created`, `summary`, `tags`). Parse this output into records keyed by `id`. Do **not** +Read the ADR files themselves. + +### Step 2 — Filter + +- Drop any record whose `status` is `Deprecated` or `Superseded`, unless `--include-retired` is set + or `--status` explicitly lists that status. +- If `--status` is given, keep only those statuses. +- If `--tags` is given, keep only records sharing at least one of those tags. + +### Step 3 — Rank + +Rank the survivors by relevance to the focus (and `--tags`), most relevant first: + +1. Tag overlap with `--tags` or with tags implied by the focus text (strongest signal). +2. Focus terms appearing in `title` / `summary`. +3. `Accepted` ahead of `Proposed` when relevance is otherwise equal (in-force decisions first). + +If there is no focus and no `--tags`, order by `id` ascending. Truncate to `--limit` (default 8), and +if you truncated, say how many total matched so the caller knows the list was capped. + +### Step 4 — Return candidates + +Emit a compact ranked list — this feeds a caller (`/spec:design`, `/adr`), so keep it scannable. For +each candidate give `id`, `title`, `status`, `tags`, `summary`, and the relative `path`: + +``` +Prior-art ADRs for "<focus>" (showing N of M): + +1. 0021-caching-decorator-architecture [Accepted] tags: decorators, caching, performance + Adds a caching decorator over HybridCache that short-circuits the pipeline on a hit. + docs/adr/0021-caching-decorator-architecture.md + +2. ... +``` + +If nothing matches, say so plainly (no prior art found for the focus) rather than returning unrelated +ADRs — a false match is worse than an empty result, because the caller trusts it without opening the +ADR. + +> This skill only reads. It never edits frontmatter or the derived `docs/adr/index.md`. + +## Examples + +```bash +# Prior art before drafting a rate-limiting decorator ADR +read_adr_metadata "rate limiting decorator for queries" --tags "decorators,resilience" + +# Everything tagged observability that is still in force +read_adr_metadata --tags "observability" + +# Include superseded/deprecated too (e.g. to trace a decision's history) +read_adr_metadata "DI container integration" --include-retired +``` diff --git a/.claude/commands/adr/write_adr_metadata.md b/.claude/commands/adr/write_adr_metadata.md new file mode 100644 index 00000000..bb77d25d --- /dev/null +++ b/.claude/commands/adr/write_adr_metadata.md @@ -0,0 +1,137 @@ +--- +allowed-tools: Read, Edit, Write, Glob, Bash(git config:*), Bash(date:*), Bash(ls:*), Bash(grep:*), Bash(basename:*), AskUserQuestion +description: Add or update an ADR's YAML frontmatter and keep it in sync with the body Status +argument-hint: <path> init|status <S>|supersede --by <id>|deprecate [options] +--- + +# Write ADR Metadata + +You are adding or updating the YAML **frontmatter** block on a single Architecture Decision Record, +and keeping it in step with the body `## Status`. This is the *write* half of the ADR metadata pair +(the read half is `read_adr_metadata`). + +**Source of truth**: the schema, status vocabulary, tag taxonomy, and identity rule live in +[`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). Read it +before writing. Do **not** duplicate the tag list here — draw tags from that document so the +vocabulary cannot drift. + +## Arguments + +``` +$ARGUMENTS +``` + +Parse `$ARGUMENTS` as: `<path> <operation> [options]`, where `<path>` is the ADR file (e.g. +`docs/adr/0022-widget-batching.md`) and `<operation>` is one of: + +| Operation | Effect | +|---------------------|--------| +| `init` | Add a frontmatter block, or update the existing one in place. Default `status: Proposed`. | +| `status <S>` | Set status to `<S>` in **both** the frontmatter and the body `## Status`. Approval is `status Accepted`. | +| `supersede --by <id>` | Mark this (older) ADR `Superseded` and record `Superseded by <id>` in the body `## Status`. | +| `deprecate` | Mark this ADR `Deprecated` (retired, with **no** named replacement). | + +`init` options (all optional except as noted): `--summary "..."` (**required** if none can be found), +`--tags "a,b,c"`, `--author "Name"` (repeatable), `--status <S>`, `--title "..."`, +`--created YYYY-MM-DD`. + +If required information for the requested operation is missing and cannot be derived from the file, +ask for it with `AskUserQuestion` rather than guessing. + +## Rules that govern every operation + +- **Identity** — `id` is **always** the filename stem (basename without `.md`), e.g. + `0022-widget-batching`. Never invent or renumber it. Compute it from `<path>`. +- **Status vocabulary** — `Proposed | Accepted | Deprecated | Superseded` only. Reject anything + else. New ADRs are `Proposed`. +- **Frontmatter mirrors the body** — after any status change the frontmatter `status` and the body + `## Status` word must be identical. Never change one without the other. +- **Idempotent** — if a frontmatter block already exists (the file starts with `---` and has a + closing `---`), update the affected fields **in place**; never prepend a second block or duplicate + keys. +- **Preserve prose** — when a body `## Status` line carries extra text (e.g. an amendment note), + change only the status *word*; keep the surrounding sentence. +- Do **not** touch anything outside frontmatter and the body `## Status` section. + +## Procedure + +### Step 1 — Read and classify the file + +Read `<path>`. Detect whether a frontmatter block is present: the file's first non-empty line is +`---` and a closing `---` follows. Note the current body `## Status` value and the `Date:` line. + +### Step 2 — Perform the operation + +**`init`** — build (or reconcile) the seven required fields: + +- `id` — filename stem (authoritative; overwrite any wrong value). +- `title` — from `--title`, else the H1 (`# N. Title`) with the leading `N.` number stripped. +- `status` — from `--status`, else the body `## Status` word normalized to the vocabulary, else + `Proposed`. +- `author` — from `--author` (list form, one entry per author). If absent: default to the git user + (`git config user.name`); if that is empty, `- "Darker Team"`. +- `created` — from `--created`, else the body `Date:` line, else today (`date +%Y-%m-%d`). +- `summary` — from `--summary`. If none and none exists in a prior block, **ask** — a summary cannot + be guessed; it is the load-bearing field. +- `tags` — from `--tags` (comma-separated), 1–4 values, each drawn from the taxonomy in the + conventions doc. If a needed tag is not in the taxonomy, propose adding it there rather than + inventing an ad-hoc tag. + +If a block already exists, update only the fields the arguments specify (plus `id`, which is always +reconciled to the stem); leave the rest untouched. Otherwise write a new block, as YAML delimited by +`---` fences, at the very top of the file, immediately before the `# N. Title` heading, followed by a +blank line. Emit the fields in schema order: `id, title, status, author, created, summary, tags`. +Quote `title` and `summary`; render `author` and `tags` as YAML lists. + +**`status <S>`** — validate `<S>` against the vocabulary. Set the frontmatter `status` to `<S>` and +replace the body `## Status` word with `<S>`, preserving any trailing amendment prose. (Approval is +just `status Accepted`.) + +**`supersede --by <id>`** — this operates on the **older** ADR being retired. Set frontmatter +`status: Superseded`. In the body `## Status` section write `Superseded` followed by a reference line +`Superseded by [<id>](<id>.md)` (link relative to `docs/adr/`). Do not modify the superseding ADR +here — the caller records the forward relationship there separately. + +**`deprecate`** — set frontmatter `status: Deprecated` and the body `## Status` to `Deprecated`. Use +this only when the decision is retired with **no** specific replacement; if a replacement exists, use +`supersede` instead. + +### Step 3 — Validate before finishing + +Confirm, and report, that: + +1. All seven keys are present: `id, title, status, author, created, summary, tags`. +2. `id` equals the filename stem. +3. `status` ∈ `{Proposed, Accepted, Deprecated, Superseded}`. +4. `created` is a valid `YYYY-MM-DD` date. +5. Every tag is in the controlled taxonomy; there are 1–4 of them. +6. The frontmatter `status` word matches the body `## Status` word. + +If any check fails, fix it (or ask) — do not leave the file in an inconsistent state. + +### Step 4 — Report + +Tell the caller what changed: the file, the operation, the resulting `status`, and (for supersession) +the recorded reference. Keep it to a couple of lines — this skill is usually invoked *by* another +command (`/adr`, `/spec:design`, `/spec:approve`), so the output feeds that caller. + +> The derived `docs/adr/index.md` is regenerated separately from frontmatter; this skill does not +> maintain it. + +## Examples + +```bash +# New ADR: stamp Proposed frontmatter onto a freshly written body +write_adr_metadata docs/adr/0022-widget-batching.md init \ + --summary "Batches widget queries into a single round-trip to the store, opt-in per handler via a decorator attribute." \ + --tags "decorators,performance" + +# Approve: flip Proposed -> Accepted in frontmatter and body +write_adr_metadata docs/adr/0022-widget-batching.md status Accepted + +# Supersession: the new ADR 0024 replaces the older 0022 +write_adr_metadata docs/adr/0022-widget-batching.md supersede --by 0024-widget-streaming + +# Retire with no replacement +write_adr_metadata docs/adr/0006-legacy-thing.md deprecate +``` diff --git a/.claude/commands/spec/README.md b/.claude/commands/spec/README.md index 72ec695b..433a7288 100644 --- a/.claude/commands/spec/README.md +++ b/.claude/commands/spec/README.md @@ -79,6 +79,11 @@ context clean and gives the heavy work a focused, single-purpose context. what is in its prompt. The command reads the needed files (and runs `gh`/`git`) first, then passes the text or paths. 2. **Launch `Agent`** with an explicit `subagent_type` and `model`: + - **`/spec:tasks`** uses `subagent_type: "Plan"`. `Plan` has all tools **except** `Agent`, + `ExitPlanMode`, `Edit`, `Write`, and `NotebookEdit` — so it can Read/Glob/Grep/Bash but has + no file-editing tool. That makes it much **harder** for the sub-agent to accidentally write + the spec file than relying on the prompt alone (it still has `Bash`, so the prompt also + forbids writing through it). - **`/spec:review`** uses `subagent_type: "general-purpose"` (adversarial reasoning that needs no source mutation). - **`/spec:ralph-implement`** uses `subagent_type: "general-purpose"` because its per-task @@ -87,7 +92,8 @@ context clean and gives the heavy work a focused, single-purpose context. runs to completion and returns; it cannot pause to ask the user anything. So before launching, the main agent clarifies any ambiguous inputs with the user via `AskUserQuestion`, then launches the sub-agent with the clarified inputs folded in. The - `general-purpose` `review` sub-agent is explicitly instructed not to prompt. **Exception:** + `Plan`-based `tasks` sub-agent has no `AskUserQuestion` so it *structurally* can't prompt; the + `general-purpose` `review` sub-agent is explicitly instructed not to. **Exception:** `/spec:ralph-implement` runs fully **unattended** — neither its orchestrator nor its sub-agent prompts the user once the loop starts. 4. **The sub-agent RETURNS its artifact as text** — it does *not* write the spec file. The one @@ -96,19 +102,20 @@ context clean and gives the heavy work a focused, single-purpose context. 5. **The main agent validates** the returned output against a checklist, then writes the file and does all bookkeeping (approval markers, `.adr-list`, git, next-steps). -The remaining planning commands (`/spec:requirements`, `/spec:design`, `/spec:tasks`) currently -run inline in the main agent rather than delegating — they have no sub-agent to assign a model to. +The remaining planning commands (`/spec:requirements`, `/spec:design`) currently run inline in the +main agent rather than delegating — they have no sub-agent to assign a model to. **Model policy** — reasoning vs. implementation: | Command | Sub-agent (type) | Model | Rationale | |---------|------------------|-------|-----------| +| `/spec:tasks` | Yes — `Plan` (read-only) | **opus** | Planning / coverage mapping | | `/spec:review` | Yes — `general-purpose` | **opus** | Adversarial reasoning | | `/spec:ralph-implement` (orchestrator) | — (the loop itself) | **opus** | Cheap bookkeeping + **required for auto mode** | | `/spec:ralph-implement` (per-task sub-agent) | Yes — `general-purpose` (writes source) | **sonnet** | Mechanical TDD implementation, kept off the opus loop context for cost | | `/spec:implement` | No | **sonnet** (Step 0 prompts to switch) | Implementation work; runs in the main agent, so set the session model | -| `/spec:requirements`, `/spec:design`, `/spec:tasks` | No (main agent) | — | Run inline | -| `/spec:new`, `/spec:switch`, `/spec:approve`, `/spec:status`, `/spec:gear` | No | — | Mechanical bookkeeping | +| `/spec:requirements`, `/spec:design` | No (main agent) | — | Run inline | +| `/spec:new`, `/spec:switch`, `/spec:approve`, `/spec:status`, `/spec:gear`, `/spec:write_release_notes` | No | — | Mechanical bookkeeping | `/spec:ralph-implement` runs **two models on purpose**: the orchestrator loop on **opus** (required for **auto mode**, and the policy for the unattended path) does only cheap @@ -157,6 +164,12 @@ Create an Architecture Decision Record (ADR) for a specific architectural decisi /spec:design handler-lifecycle ``` +Before drafting, surfaces prior-art ADRs with `read_adr_metadata` (frontmatter only, retired ADRs +skipped). After writing, stamps the new ADR's YAML frontmatter with `write_adr_metadata` +(`status: Proposed`, a summary, 1–4 tags from the taxonomy in +`.agent_instructions/adr_frontmatter.md`), regenerates `docs/adr/index.md`, and — if the ADR's +`## Consequences` record a breaking change — recommends `/spec:write_release_notes`. + --- ### `/spec:approve <phase> [adr-number]` @@ -170,7 +183,9 @@ Approve a specification phase or specific ADR. /spec:approve tasks ``` -Approving the **design** points you at `/spec:tasks` — there is one task list and one route to +Approving the **design** flips each ADR to `Accepted` with `write_adr_metadata` (frontmatter and +body `## Status` together), marks any ADR it replaces `Superseded` (or `Deprecated`), and +regenerates `docs/adr/index.md`. It then points you at `/spec:tasks` — there is one task list and one route to it. Attended vs. unattended is not a fork here; it is a **gear** you pick (and change) at implementation time with `/spec:gear`. @@ -196,7 +211,9 @@ Review the current specification phase or specific ADR. ### `/spec:tasks` Create the implementation task list based on the approved design. This is the **single** task -list; both `/spec:implement` and `/spec:ralph-implement` run it. +list; both `/spec:implement` and `/spec:ralph-implement` run it. Drafting is delegated to a `Plan` +sub-agent on **opus**, which returns the list plus an FR/ADR coverage cross-reference; the main agent +sanity-checks coverage and writes `tasks.md`. ```bash /spec:tasks @@ -208,6 +225,28 @@ Every task opens with a tag — `TEST + IMPLEMENT`, `CHARACTERISE`, `STRUCTURAL` --- +### `/spec:write_release_notes [spec-id]` + +Write, or replace in place, the spec's section of the root `release_notes.md`. The section sits +under the file's first `##` heading (`## Master`), is headed `### {title} (spec {id})`, and is marked +with `<!-- spec: {spec directory} -->` on the next line so later runs find and replace it rather +than duplicating it. + +```bash +/spec:write_release_notes # the current spec +/spec:write_release_notes 014-Caching-Decorator # a named spec (full name, numeric id, or substring) +``` + +Breaking-change items are judged from the prose of each ADR's `## Consequences` (and +`requirements.md`), never from a diff. The command stops without writing whenever it would have to +guess — an ambiguous spec id, a missing `release_notes.md`, a duplicate or misplaced marked section, +or an unmarked heading that already names the spec. It never stages or commits. + +`/spec:design` recommends it when an ADR records a breaking change, and `/spec:review design` flags a +breaking change with no marked section. + +--- + ### `/spec:status` Show status of all specifications, including the resolved review gear of the active spec (and of @@ -440,9 +479,11 @@ Darker/ │ └── README.md # Spec overview ├── docs/ │ └── adr/ +│ ├── index.md # Derived index (generated from frontmatter; do not hand-edit) │ ├── 0001-record-architecture-decisions.md │ ├── 0002-feature-aspect-one.md │ └── 0003-feature-aspect-two.md +└── release_notes.md # Per-spec release-notes sections (/spec:write_release_notes) ``` ## Best Practices diff --git a/.claude/commands/spec/approve.md b/.claude/commands/spec/approve.md index 7f362c25..eab073f7 100644 --- a/.claude/commands/spec/approve.md +++ b/.claude/commands/spec/approve.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(grep:*), Read, Write, Edit, Glob, AskUserQuestion +allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(grep:*), Bash(awk:*), Read, Write, Edit, Glob, AskUserQuestion, Skill description: Approve a specification phase argument-hint: requirements|design [adr-number]|tasks --- @@ -35,16 +35,31 @@ Parse $ARGUMENTS to extract phase and optional ADR number: - **If ADR number specified** (e.g., "design 0043"): Approve only that ADR - **If no ADR number**: Approve ALL ADRs in the `.adr-list` -4. For each ADR to approve: - - Read the ADR file from `docs/adr/{adr-filename}` - - Use Edit tool to update Status from "Proposed" to "Accepted" - - Find the line containing "## Status" followed by "Proposed" - - Replace "Proposed" with "Accepted" +4. For each ADR to approve, flip its status to `Accepted` with the `write_adr_metadata` skill so + the **frontmatter `status`** and the **body `## Status`** move together (do not hand-edit only the + body — that would leave the frontmatter stale, and the derived index reads the frontmatter): -5. Create approval marker: `touch specs/{current-spec}/.design-approved` -6. Show user which ADRs were approved -7. Remind the user to commit the approved ADRs to git. -8. **Point at the task breakdown.** If more ADRs are still needed, tell the user to run + ``` + write_adr_metadata docs/adr/{adr-filename} status Accepted + ``` + + Confirm the skill reports its checks passing (frontmatter `status` matches body `## Status`). + +5. **Handle supersession.** If an approved ADR replaces an earlier decision, retire the older ADR in + the same pass. Prefer the relationship the ADR already records: read the approved ADR's `## Status` + / `## References` for a "Supersedes {old-id}" note, and if it is ambiguous ask the user with + `AskUserQuestion` which prior ADR (if any) is superseded rather than guessing. + - Replaced by a specific ADR → mark the **older** one `Superseded` and record the back-link: + `write_adr_metadata docs/adr/{old-id}.md supersede --by {new-id}` + - Retired with no named replacement → `write_adr_metadata docs/adr/{old-id}.md deprecate` + +6. **Regenerate the derived index.** After the status changes above, refresh `docs/adr/index.md` so + it reflects the new statuses — see "Regenerate the ADR index" at the end of this file. + +7. Create approval marker: `touch specs/{current-spec}/.design-approved` +8. Show user which ADRs were approved (and any superseded/deprecated as a result). +9. Remind the user to commit the approved ADRs (and the regenerated `docs/adr/index.md`) to git. +10. **Point at the task breakdown.** If more ADRs are still needed, tell the user to run `/spec:design [another-focus-area]` first and stop here. Otherwise the design is complete: the next step is `/spec:tasks` → `/spec:review tasks` → `/spec:approve tasks`. @@ -84,6 +99,18 @@ If phase name is not recognized, show valid options: - `design [adr-number]` - Approve ADR(s) and update status to Accepted - `tasks` - Approve task list +## Regenerate the ADR index + +After any ADR status change (approval, supersession, deprecation), refresh the derived +`docs/adr/index.md` so it matches the frontmatter. This is the single canonical command (documented +in [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md)): + +```bash +awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md +``` + +`docs/adr/index.md` is a regenerable cache — never hand-edit it; always regenerate from frontmatter. + ## Examples ```bash @@ -100,4 +127,4 @@ If phase name is not recognized, show valid options: /spec:approve tasks ``` -Use Edit tool to update ADR status, touch command for approval markers. +Use the `write_adr_metadata` skill to update ADR status, and touch for approval markers. diff --git a/.claude/commands/spec/design.md b/.claude/commands/spec/design.md index c4aff374..d01f2ea9 100644 --- a/.claude/commands/spec/design.md +++ b/.claude/commands/spec/design.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(git:*), Read, Write, Glob +allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(git:*), Bash(awk:*), Bash(date:*), Bash(basename:*), Bash(grep:*), Bash(mkdir:*), Read, Write, Edit, Glob, AskUserQuestion, Skill description: Create technical design specification (ADR) argument-hint: [adr-focus-area] --- @@ -13,6 +13,11 @@ ADR directory: `docs/adr/` **Note**: You can create multiple ADRs for the same requirement. Each ADR should focus on a single architectural decision. +**Frontmatter**: Every ADR carries a YAML frontmatter block (`id`, `title`, `status`, `author`, +`created`, `summary`, `tags`) — see [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). +This command reads existing ADRs' frontmatter to surface prior art (Step 3a) and stamps the new +ADR's frontmatter on write (Step 4a) via the `read_adr_metadata` / `write_adr_metadata` skills. + ## Your Task ### Step 1: Verify Requirements Approved @@ -34,12 +39,33 @@ ADR directory: `docs/adr/` 1. Check existing ADRs: `ls docs/adr/ | grep -E "^[0-9]{4}-" | sort | tail -1` 2. If `docs/adr/` doesn't exist, create it: `mkdir -p docs/adr` 3. Calculate next number (format: 0001, 0002, 0003, etc.) + - The number is an **ordering hint, not an identity** — the `id`/slug is the identity, and a + number collision with a concurrent branch is acceptable (see `adr_frontmatter.md`). 4. Determine the ADR focus: - If $ARGUMENTS provided: Use that as the specific focus area - If not provided: Ask user what aspect of the requirement this ADR addresses 5. Create ADR filename: `docs/adr/{NNNN}-{focus-area}.md` - Use kebab-case for focus area -6. Add to tracking: `echo "0043-{focus-area}.md" >> specs/{current-spec}/.adr-list` +6. Add to tracking: `echo "{NNNN}-{focus-area}.md" >> specs/{current-spec}/.adr-list` + +### Step 3a: Surface Prior-Art ADRs (before drafting) + +Before drafting, find existing decisions relevant to **{focus-area}** so the new ADR reuses them, +avoids contradicting them, or deliberately supersedes them — rather than re-deciding in ignorance. + +Use the `read_adr_metadata` skill (`.claude/commands/adr/read_adr_metadata.md`), passing the focus +area as the query plus any obvious tags from the taxonomy: + +``` +read_adr_metadata "{focus-area}" --tags "{likely-tags}" +``` + +It reads only frontmatter (cheap) and by default **skips `Deprecated` and `Superseded`** ADRs, so +retired decisions are not offered as live prior art. Reference the relevant candidates under +`Related ADRs`, and note any this ADR would supersede. Prefer this to reading `docs/adr/index.md`, +which is an un-ranked full-corpus table meant for human browsing. + +If no prior art is found, note that and proceed. ### Step 4: Create ADR Document @@ -119,6 +145,40 @@ Proposed - External references: {Links to relevant documentation} ``` +### Step 4a: Stamp Frontmatter and Regenerate the Index + +After writing the ADR body: + +1. **Stamp frontmatter** with the `write_adr_metadata` skill + (`.claude/commands/adr/write_adr_metadata.md`), with a summary (1–2 sentences stating WHAT was + decided) and 1–4 tags from the controlled taxonomy in [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md): + + ``` + write_adr_metadata docs/adr/{NNNN}-{focus-area}.md init --summary "{summary}" --tags "{tags}" + ``` + + This derives `id`/`title`/`created` from the file, sets `status: Proposed`, and keeps the + frontmatter in sync with the body `## Status`. Confirm the checks the skill reports pass. + + If this ADR **supersedes** a prior ADR you identified in Step 3a, also mark that older ADR: + + ``` + write_adr_metadata docs/adr/{old-id}.md supersede --by {NNNN}-{focus-area} + ``` + +2. **Regenerate the ADR index** so `docs/adr/index.md` reflects the new ADR (and any supersession): + + ```bash + awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md + ``` + + `docs/adr/index.md` is a regenerable cache — never hand-edit it; always regenerate from frontmatter. + +3. **Recommend `/spec:write_release_notes` when this ADR breaks something.** If the ADR you just + wrote or amended records, in its `## Consequences`, a change that breaks an existing behaviour + or interface, tell the user so and recommend running `/spec:write_release_notes` for this spec. + Do **not** run that command yourself — recommend it, and let the user choose when to run it. + ### Step 5: Additional Guidance When creating the ADR: @@ -143,7 +203,7 @@ Review against the design principles 1. Remind user to: - Review and complete the ADR with technical details - - Commit the ADR: `git add docs/adr/{NNNN}-{focus-area}.md && git commit -m "docs: add ADR for {focus-area}"` + - Commit the ADR and the regenerated index: `git add docs/adr/{NNNN}-{focus-area}.md docs/adr/index.md && git commit -m "docs: add ADR for {focus-area}"` - The first ADR should typically be the first commit on the feature branch 2. Multiple ADRs: - If requirement needs more architectural decisions, run `/spec:design [another-focus-area]` again diff --git a/.claude/commands/spec/review.md b/.claude/commands/spec/review.md index 0604d1a3..86512899 100644 --- a/.claude/commands/spec/review.md +++ b/.claude/commands/spec/review.md @@ -129,6 +129,18 @@ You are a skeptical reviewer. Assume the design has problems — your job is to - Is the error handling strategy explicit? - Are concurrency/threading concerns addressed where relevant? +**Frontmatter:** +- Does each ADR carry the YAML frontmatter defined in `.agent_instructions/adr_frontmatter.md` + (`id` == filename stem, `status` matching the body `## Status`, a `summary` that states what was + decided, 1–4 tags from the controlled taxonomy)? + +**Release Notes Coverage:** +- Does the ADR set (or any ADR in it) record, in its `## Consequences`, a change that breaks an + existing behaviour or interface? If so, does `release_notes.md` carry a section marked for this + spec (a `### … (spec {id})` heading immediately followed by + `<!-- spec: {this spec's directory name} -->`)? A breaking change with no such marked section is + a finding — recommend running `/spec:write_release_notes`. + --- #### Tasks Review Criteria @@ -155,6 +167,16 @@ You are a skeptical reviewer. Assume the task list has problems — your job is - Map each FR from requirements.md to tasks. Are there FRs with no corresponding task? LIST THEM. - Map each ADR decision to tasks. Are there design decisions with no implementation task? LIST THEM. +**Task-Type Tag Form:** +- Does every task checkbox open its bold lead-in with the tag first (`**STRUCTURAL: T1.2 — + …**`), never the task id first (`**T1.2 — STRUCTURAL: …**`)? `/spec:ralph-implement` dispatches + on the leading label and skips a task it cannot classify — flag any checkbox that drifts, outside + a *DO NOT* block. +- Does every behavioral task carry the `⛔ APPROVAL GATE` line, and does no task try to encode the + review gear (a "no gate" annotation, an omitted gate line)? The gear lives in `.current-gear`. +- Does every `CHARACTERISE` task name its RED mutation (the production change and the assertion it + must fail on)? + --- ### Step 5: Output Format for Sub-Agent diff --git a/.claude/commands/spec/tasks.md b/.claude/commands/spec/tasks.md index 63719dba..cd439491 100644 --- a/.claude/commands/spec/tasks.md +++ b/.claude/commands/spec/tasks.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(cat:*), Bash(grep:*), Bash(test:*), Bash(find:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Read, Write, Glob +allowed-tools: Bash(cat:*), Bash(grep:*), Bash(test:*), Bash(find:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Read, Write, Glob, Agent, AskUserQuestion description: Create implementation task list --- @@ -7,26 +7,68 @@ description: Create implementation task list Current spec directory: specs/ +**Workflow**: Issue -> Requirements -> ADR(s) -> **Tasks** -> Tests -> Code + +**Sub-agent**: Drafting the task list is delegated to a sub-agent +(`subagent_type: "Plan"`, **`model: "opus"`**). `Plan` has no `Write`/`Edit`/`NotebookEdit`, +which makes it much harder to accidentally write the file (the prompt still forbids writing +via `Bash`), while still allowing Read/Glob/Grep to verify file paths. The sub-agent reads the +requirements and ADRs and RETURNS the task list as text. The main agent validates coverage and +writes `tasks.md`. See `.claude/commands/spec/README.md` → "Sub-agents & model policy". + ## Your Task -First, read specs/.current-spec to determine the active specification directory. +### Step 1: Gather Context + +1. Read `specs/.current-spec` to determine the active specification directory. +2. Verify design is approved: check for `.design-approved` in the spec directory. If + missing, tell the user to run `/spec:approve design` first and exit. +3. Read `specs/{current-spec}/requirements.md` (the FRs/ACs the tasks must cover). +4. Read `specs/{current-spec}/.adr-list` and each ADR from `docs/adr/` (the design + decisions the tasks must implement). + +If `requirements.md` or any listed ADR is missing, stop and tell the user. Do NOT launch +the sub-agent with missing inputs. + +### Step 2: Launch Sub-Agent to Draft the Task List + +**Verify inputs with the user before launching (MAIN agent).** The `Plan` sub-agent is +one-shot and has no `AskUserQuestion` — it cannot ask the user anything once launched. So +before launching, review the requirements and ADRs for ambiguity or open decisions that +affect decomposition (how granular the tasks should be, sequencing/dependencies, scope +boundaries between what's in and out of this spec) and resolve them with the user via +`AskUserQuestion`. Then launch the sub-agent with the clarified inputs folded in. All user +interaction stays in the main agent — never the sub-agent. + +Launch an `Agent` with `subagent_type: "Plan"` and **`model: "opus"`**. The +prompt MUST include: + +1. The full text of `requirements.md`. +2. The full text of each ADR (or their paths to read). +3. The task-drafting rules and the **mandatory TDD task format** below — everything from + *Task-drafting rules* through *Coverage cross-reference*. +4. An explicit instruction: **RETURN the complete task list as markdown text. Do NOT write + any file.** The sub-agent may use Read/Glob/Grep to verify only the **existing** paths it + cites — code/classes and ADRs a task references. The **new** test and implementation files a + task plans to create do NOT exist yet and MUST NOT be Glob-verified or flagged as missing; + they are expected to be absent. -1. Verify design is approved (look for .design-approved file in the spec directory) -2. Create tasks.md with: - - Detailed task list with checkboxes - - Task dependencies - - Risk mitigation tasks -3. Each task should be specific and actionable -4. A task MUST represent implementing a behavior and NOT an implementation detail -5. Use markdown checkboxes: `- [ ] Task description` +#### Task-drafting rules (include in the sub-agent prompt) -Organize tasks to enable incremental development and testing. +- Produce `tasks.md` content with: + - A detailed task list with markdown checkboxes (`- [ ] Task description`) + - Explicit task dependencies + - Risk-mitigation tasks +- Each task MUST be specific and actionable. +- A task MUST represent implementing a **behavior**, NOT an implementation detail. +- Organize tasks to enable incremental development and testing. +- Order tasks so dependencies come first; structural/tidy tasks before behavioral ones. -## CRITICAL: TDD Task Format +#### CRITICAL: TDD Task Format (include verbatim in the sub-agent prompt) -**MANDATORY**: When creating TEST tasks, you MUST format them to enforce `/test-first` skill usage: +**MANDATORY**: When creating TEST tasks, you MUST format them to enforce `/test-first` skill usage. -### Task Template +##### Task Template ```markdown - [ ] **TEST + IMPLEMENT: [Behavior description]** @@ -42,7 +84,7 @@ Organize tasks to enable incremental development and testing. - [implementation point 2] ``` -### Example Task +##### Example Task ```markdown - [ ] **TEST + IMPLEMENT: QueryProcessor throws when handler not registered** @@ -60,7 +102,7 @@ Organize tasks to enable incremental development and testing. - Include query type name in exception message ``` -### Why This Format? +##### Why This Format? 1. **Visible command**: The `/test-first` command is prominently displayed 2. **Stop sign**: The ⛔ and "STOP HERE" make the approval gate unmissable @@ -68,7 +110,7 @@ Organize tasks to enable incremental development and testing. 4. **Complete context**: All details needed for test and implementation 5. **IDE review**: Explicitly states user will review in IDE, not CLI -### The gate line and the review gear +##### The gate line and the review gear Write the `⛔` line on **every** behavioral task. It states the default: the gate is armed unless the spec has been deliberately shifted into the `review-after` gear (see [`gear.md`](gear.md) and @@ -87,7 +129,7 @@ convention works — `## Phase N`, `## Task N`, a named group — and the depth can also be a `tasks N-M` range. What does not work is a single flat `## Tasks` heading over forty tasks, because there is then nothing to scope to short of the whole spec. -### Task-type tag form (required) +##### Task-type tag form (required) Every task checkbox opens its bold lead-in with exactly one tag, followed immediately by a colon, with any task id after the colon — one template line per tag: @@ -117,7 +159,7 @@ in the *DO NOT* block below. - **`DOC`** — documentation only. - **`VERIFY`** — a checkpoint that runs named checks and should need no source change. -### DO NOT Format Tasks Like This +##### DO NOT Format Tasks Like This BAD - Separates test and implementation: ```markdown @@ -139,7 +181,37 @@ BAD - Puts the task id before the tag: This drifts from the tag-first form above. `/spec:ralph-implement` does not recognise it as a `STRUCTURAL` task and will skip it. -## Next Steps +#### Coverage cross-reference (include in the sub-agent prompt) + +- Map EVERY functional requirement (FR-N) from `requirements.md` to at least one task. + List any FR with no task. +- Map EVERY ADR decision to at least one task. List any decision with no task. +- Flag any task that does not trace back to a requirement or ADR decision (scope creep). + +### Step 3: Validate Coverage and Write the File + +After the sub-agent returns: + +1. **Validate** before writing. The sub-agent owns the coverage mapping (its "Coverage + cross-reference" output); the main agent **sanity-checks** that report rather than + re-deriving it: + - The sub-agent's coverage report lists every FR and every ADR decision against a task, + with no gaps flagged. Spot-check a couple of entries against `requirements.md` or the + ADRs rather than re-mapping the whole set; if the report flags any uncovered FR/decision + or is missing, send it back to the sub-agent. This is deliberately a **sampling check, not + full verification** — the exhaustive adversarial coverage review is `/spec:review tasks`, + which the user is prompted to run before approval. + - Each behavioral task uses the `TEST + IMPLEMENT` template with `/test-first` and the + `⛔ APPROVAL GATE` line — none are split into separate TEST/IMPLEMENT tasks, and none omit + the gate line. + - Every task opens with its tag, tag first; every `CHARACTERISE` task names its RED mutation. + - No task is an implementation detail rather than a behavior. + - If coverage is incomplete or a task is malformed, ask the sub-agent to revise (or fix + it yourself) before writing. + +2. **Write** the validated task list to `specs/{current-spec}/tasks.md` using the Write tool. + +### Step 4: Next Steps Remind the user to: - Review `tasks.md` diff --git a/.claude/commands/spec/write_release_notes.md b/.claude/commands/spec/write_release_notes.md new file mode 100644 index 00000000..4fad9e40 --- /dev/null +++ b/.claude/commands/spec/write_release_notes.md @@ -0,0 +1,204 @@ +--- +allowed-tools: Bash(ls:*), Bash(test:*), Read, Grep, Glob, Edit +description: Write or replace a spec's marked release-notes section +argument-hint: [spec-id] +--- + +## Available specifications + +!`ls -1d specs/*/ 2>/dev/null` + +## Your task + +Write, or replace in place, the release-notes section for the spec named by `$ARGUMENTS` (or the +current spec, absent an argument) into `release_notes.md`'s marked form, and stop without writing +whenever proceeding would mean guessing whose section is whose. + +Follow each step exactly as written; do not improvise past what a step actually says. Read at most +200 lines per `Read` call in every step below, taken from `Grep` line numbers or, for a whole file, +from a `Grep` count of its lines; if a call is refused for size, re-issue it with half the limit. +Nothing is ever staged or committed — this command writes `release_notes.md` and nothing else. + +### Step 1 — Resolve the target spec directory + +Candidates are the directory names in the `## Available specifications` listing above — the last +path segment of each line, trailing slash removed. `specs/.current-spec` and any other plain file +under `specs/` are not directory entries and never appear in that listing, so they are never +candidates. + +Take `$ARGUMENTS` **as a whole** — the entirety of the argument text, leading and trailing whitespace +trimmed, any wrapping quotes removed, internal whitespace preserved verbatim. Do **not** split it on +whitespace. + +**If `$ARGUMENTS` is non-empty after trimming**, match it against the candidates using this ordered +rule set, stopping at the first rule that yields exactly one match: + +1. Exact directory-name match. +2. Exact numeric id match — the digits before the first `-` (Darker specs use a three-digit id, + e.g. `014`). +3. Case-insensitive substring match on the directory name. + +- If a rule yields **more than one** match, stop. Do not create or modify any file. Print exactly: + `Ambiguous spec id '{arg}' — matches: {list of matching directory names}. Re-run with the full + directory name.` +- If **no** rule yields any match, stop. Do not create or modify any file. Print exactly: + `No spec matches '{arg}'. Run /spec:status to list specs.` +- Otherwise exactly one rule yields exactly one match: the target is that directory. Continue below. + +**If `$ARGUMENTS` is empty** (no argument given), read `specs/.current-spec`. + +- If the file is missing, is empty, is whitespace-only, or names a directory that is not one of the + candidates above, stop. Do not create or modify any file. Print exactly: + `No spec id given and no usable current spec (specs/.current-spec is {missing|empty|stale: names + '{value}'}). Pass a spec id (/spec:write_release_notes 014-Caching-Decorator) or run /spec:switch + first.` — using `missing` when the file does not exist, `empty` when it exists but is + empty or whitespace-only, and `stale: names '{value}'` (with `{value}` the file's trimmed content) + otherwise. +- Otherwise the target is the directory it names. Continue below. + +**On success**, the target is resolved. Hold two things for the steps below: `{dir}`, the full +target directory name, and `{NNNN}`, its leading numeric id (the digits up to the first `-`, e.g. `014`). +There is **no completeness precondition** here — this command runs at design time, long before a +spec is finished, so an incomplete spec is not a stop. Continue to Step 2. + +### Step 2 — Locate `release_notes.md`'s headings and fences, and its first `##` heading (ladder rows 2, 3) + +- Run `test -f release_notes.md`. If it fails, stop. Do not create the file. Print exactly: + `release_notes.md does not exist — /spec:write_release_notes never creates it.` +- Otherwise, find every heading line and every fence-delimiter line in `release_notes.md`, with line + numbers, in one `Grep` call. A *heading* is a line beginning with one to six `#` followed by a + space; a fence-delimiter line begins with three backticks. Discard any heading line that falls + between an opening and a closing fence-delimiter line — it is inside a fenced block, not a real + heading. +- From the remaining headings, find the first `##` heading (depth exactly two). If there is none, + stop. Do not modify the file. Print exactly: + `release_notes.md has no ## heading — /spec:write_release_notes has nowhere to write under.` +- Otherwise hold that heading's line number and text, and the line number of the **next** `##` + heading after it (or end-of-file if there is none) — this range is the *first section*, the only + place this command ever writes. Continue to Step 3. + +### Step 3 — Check the target has something to derive items from (ladder row 4) + +Check `specs/{dir}/.adr-list` and `specs/{dir}/requirements.md`. + +- If `.adr-list` is missing or empty, **and** `requirements.md` is also missing, stop. Do not modify + `release_notes.md`. Print exactly: + `Spec {dir} has neither a non-empty .adr-list nor a requirements.md — nothing to derive + release-notes items from.` +- Otherwise there is at least one source to derive items from. Continue to Step 4. + +### Step 4 — Find marker lines and unmarked same-id headings under the first `##` heading (ladder rows 5–7) + +- Find every marker line **in the whole file**, not only the first section — a line whose trimmed + text is exactly `<!-- spec: {name} -->` for some `{name}` — with a `Grep` searching for the + literal text `<!-- spec: `, returning line numbers. A `###` heading is *marked* when a marker + line is the line directly after it, and *marked for the target* when that line, trimmed, equals + `<!-- spec: {dir} -->` exactly (a literal comparison, never a substring match). Searching the + whole file matters here: a section marked for the target could sit under any `##` heading, not + only the first, and that is exactly ladder row 6's case below — scoping this search to the first + section would make such a section invisible and the command would wrongly fall through to writing + a duplicate. +- Count the `###` headings marked for the target, anywhere in the file. + - **More than one**: stop. Do not modify the file. Print exactly: + `release_notes.md has {n} sections marked for {dir} — delete all but one, then re-run: {list of + each marked heading's line number and text}.` + - **Exactly one**: check whether that heading's line number falls inside the first section's range + from Step 2. + - **No** (it sits under a later `##` heading than the first): stop. Do not modify the file. + Print exactly: + `The section marked for {dir} sits under a later ## heading than the first ({first heading's + text}) — released notes are not rewritten. Move the section under the first ## heading by + hand, or delete it, and re-run.` + - **Yes**: hold its full extent — from its `###` heading line to the line before the next + heading of any depth (or end of file) — for Step 6's replacement (row 8). Skip the rest of + this step. + - **None marked for the target**: continue below. +- When no section is marked for the target, find every `###` heading **within the first section's + range from Step 2 only** (row 7 is scoped to the first heading, unlike the marker search above) + that is **not** marked for *any* directory (no marker line directly beneath it) and whose text + contains the literal substring `(spec {NNNN}`, using the target's own `{NNNN}` from Step 1. A + heading followed by a marker line naming some *other* directory is not a candidate here — it + already belongs to that other spec and is left alone. + - **One or more such candidates**: stop. Do not modify the file. Print exactly: + `release_notes.md has an unmarked heading naming spec {NNNN} under its first ## heading, with no + marker line beneath it: "{heading text}" (line {n}). Delete that section, or add the line + <!-- spec: {the directory this section actually belongs to} --> beneath its heading and re-run — + marking it hands the section's body to this command, which replaces everything but its title on + every future run.` — naming every such heading found, one per line, when there is more than one. + - **None**: there is nothing standing in the way of a fresh section (ladder row 9). Continue to + Step 5. + +### Step 5 — Judge the breaking-change items + +Items are **judged from prose**, never from a diff or from any prior `release_notes.md` content — +this command has no catalogue of its own to defer to; it is the one that produces one. + +- **Resolve `.adr-list`.** Read `specs/{dir}/.adr-list` (if present and non-empty) one entry per + line. For each entry, resolve it with `Glob` of `docs/adr/`: + - The entry is a filename (optionally prefixed `docs/adr/`): it resolves when exactly that file + exists in `docs/adr/`. + - Otherwise (a bare number or anything else that isn't a filename): `Glob` for + `docs/adr/{entry}-*.md`. Zero matches — unresolved, contributes no items, held for the closing + message as `{entry} — ADR file not found in docs/adr/.` More than one match — unresolved, + contributes no items, held as `{entry} — ambiguous ADR number, matches: {filenames}; .adr-list + should name the full filename instead.` Exactly one match — resolved. +- **Read each resolved ADR's Consequences.** For each resolved file, find its `## ` heading lines + and fence-delimiter lines with one `Grep`, discarding any heading between an opening and closing + fence (the same rule Step 2 applies to `release_notes.md`). Read from the `## Consequences` + heading to the next `## ` heading (or end of file). Judge one breaking-change item per + *Consequences* bullet that is **consumer-affecting** — changes an existing public behaviour or + interface a consumer outside the ADR's own module depends on — never a bullet the ADR's own text + marks as internal-only or non-consumer-affecting. A bullet naming more than one related change to + one member is one item; a bullet the ADR gives its own line for is a separate item — follow the + ADR's own bullet boundaries rather than re-partitioning them. +- **Read `requirements.md`.** When `specs/{dir}/requirements.md` exists, read it in full and judge + any further breaking-change item its prose states that is not already covered by a resolved ADR's + Consequences bullet — this is the **only** source when `.adr-list` is missing, empty, or every + entry is unresolved. +- **Word each item** as one bullet, at most 40 words: a one-sentence statement of what breaks, its + classification set in italics and parentheses — one or more of *source*, *binary*, *behavioural*, + *compatibility* (e.g. `*(source and binary)*`) — and the migration in one sentence. +- **If no item is found** — no resolved `.adr-list` entry's Consequences named a consumer-affecting + break, and `requirements.md` named none or is absent — the list is the single line + `No breaking changes.` When, additionally, `.adr-list` had entries but **none** resolved and there + is no `requirements.md`, note for the closing message that the list was derived from no source, + and name every unresolved entry. +- Hold the finished item list (or `No breaking changes.`) for Step 6. + +### Step 6 — Write the section and apply the one exact-match `Edit` (ladder rows 8, 9) + +Build the section text: + +- **Heading.** `### {title} (spec {NNNN}{, #{issue}})` — the issue part appears only when + `specs/{dir}/.issue-number` exists and is non-empty (its trimmed content is `{issue}`). + - **Replacing** (Step 4 held a marked section): keep the **existing** title — the held heading's + text up to (not including) its first literal ` (spec `, or the whole heading text when it has + none. + - **Inserting** (row 9, nothing was held): the title is a short plain-language name for the + change, written from `requirements.md`'s problem statement (or, when `requirements.md` is + absent, from `tasks.md`'s own subject matter). +- **Marker line**, the very next line: `<!-- spec: {dir} -->`. +- **Summary paragraph**: one short paragraph for a user of the library, written from the problem + statement (or `tasks.md`), never copied verbatim from an ADR's implementation detail. +- **`#### Breaking changes`**, followed by Step 5's held item list — one top-level `- ` bullet per + item — or `No breaking changes.` +- No further `####` subsection is added; free-text subsections are for a person to add by hand and + this command never writes one. + +Apply the write with **one** `Edit` on `release_notes.md`: + +- **Replacing** (row 8): the `old_string` is the held section's full extent from Step 4 (its + `###` heading line through the line before the next heading), read first with `Read` so the + `Edit` has content to match against; the `new_string` is the section text above. Every byte + outside that span is therefore left untouched. +- **Inserting** (row 9): the `old_string` is the first `##` heading's own line, read first with + `Read`; the `new_string` is that same line, a blank line, then the section text above — so the + new section lands directly under the first `##` heading, before whatever followed it. +- If the `Edit` is refused because `old_string` is not unique in the file, stop. Do not retry with + a different anchor. Print exactly: + `release_notes.md's {replacement anchor|insertion anchor} was not unique — the edit was not + applied. release_notes.md is unchanged.` + +**Closing message.** After a successful `Edit`, print that `release_notes.md` gained or replaced the +section for `{dir}`, then name every `.adr-list` entry Step 5 could not resolve, using its own +wording, or say every entry resolved when none is left unresolved. diff --git a/CLAUDE.md b/CLAUDE.md index 5dc738bd..864581de 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -226,4 +226,5 @@ For comprehensive guidance on working with this codebase, Claude should read the - [Design Principles](.agent_instructions/design_principles.md) - Responsibility-Driven Design and architectural guidance - [Testing](.agent_instructions/testing.md) - TDD practices, test structure, and testing guidelines - [Documentation](.agent_instructions/documentation.md) - XML documentation standards and licensing requirements +- [ADR Frontmatter](.agent_instructions/adr_frontmatter.md) - ADR YAML frontmatter schema, status vocabulary, tag taxonomy, and the generated ADR index - [Dependency Management](.agent_instructions/dependency_management.md) - Package management with Directory.Packages.props diff --git a/release_notes.md b/release_notes.md new file mode 100644 index 00000000..036566cd --- /dev/null +++ b/release_notes.md @@ -0,0 +1,8 @@ +# Release Notes + +Unreleased changes are collected under `## Master` and copied into the GitHub release when a version +is tagged. Each spec that changes behaviour or a public interface gets one section, marked with a +`<!-- spec: {spec directory} -->` line beneath its heading; `/spec:write_release_notes` writes and +refreshes those sections. + +## Master From d62245da1cbf5bd62b2a163f1d891ba2e5db91aa Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 09:26:04 +0100 Subject: [PATCH 04/12] docs(testing): narrow-and-deep modules and why InternalsVisibleTo is rejected Clarify that "do not export to test" forbids reaching inside a module, not every widening: widen honestly (public, recorded) when no existing export reaches the behaviour, and treat the need as a design signal. Explain that InternalsVisibleTo makes `internal` a lie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agent_instructions/testing.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.agent_instructions/testing.md b/.agent_instructions/testing.md index 780aceff..41bee795 100644 --- a/.agent_instructions/testing.md +++ b/.agent_instructions/testing.md @@ -132,6 +132,28 @@ A run in `review-after` that drops any of these is defective — it is not "a di - By following the rules for only testing behaviors, you only need to write tests for the behaviors exposed from the module not its details. - Private or Internal classes used in the implementation do not need tests - they are covered by the behavior that led to their creation. +### Narrow and deep — and when widening the surface is legitimate + +- The goal these rules serve is a module that is **narrow and deep**, not wide and shallow: a small + surface hiding substantial behaviour. The rules above, and *No InternalsVisibleTo* below, all + forbid the same one thing — **coupling a test to the module's implementation details**. +- Read that way, "do not export to test" is **conditional, not absolute**. What it forbids is + widening the surface to reach *inside*. Before widening, ask: + **is there a path through the module's existing exports to the behaviour under test?** + - If there is, widening is unjustified — test through that path. + - If there is not, exporting may be the only way to test the behaviour at all. Then widen + **honestly**: make it public, and record what was widened and why (in the ADR, or the PR). +- **Having to widen is a design signal, not just a cost.** If no existing export reaches the + behaviour, the module's contract may be missing a name for something it already depends on + internally. Ask what the new export *says about the module* before assuming it is a testing + concession: + - A widening that other callers would genuinely want is a real term of the contract, and the fact + that a test wanted it first is incidental. + - A widening that only a test could ever want is a smell. The design is probably wrong somewhere + else, and the export is hiding that rather than fixing it. +- The honest check, after the fact: does anything other than a test ever call it? If nothing ever + does, it was a testing concession after all, and should be revisited. + ## No InternalsVisibleTo - **NEVER use `InternalsVisibleTo` to expose internal classes for testing.** @@ -141,6 +163,12 @@ A run in `review-after` that drops any of these is defective — it is not "a di 2. As complexity grows, extract internal helper classes through refactoring 3. Tests always go through the public interface - internal classes are covered by those tests - If you need to inject a dependency for testing (e.g., randomness, I/O), make the interface **public** so it can be injected through the public API. +- **`InternalsVisibleTo` is rejected because it makes `internal` a lie.** The comfort of `internal` + is that a member may be refactored freely, since every dependency on it lives inside the module. + Once tests in another assembly bind to it, that is false — refactoring breaks them. The member has + been made public in effect, just to a narrower audience, while the keyword still claims otherwise. + Prefer honesty: make it public and record the widening (see *Narrow and deep* above), which at + least forces the question of why it belongs on the module. - The goal is that tests are coupled to behavior, not implementation. Refactoring internals should never break tests. ## Exploratory Tests for Implementation Details From a4a9e0c50ce8f77d42843a0deffb4dc2fce5e6f3 Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 10:17:58 +0100 Subject: [PATCH 05/12] feat(skills): shared ADR contract, and /spec:design drafts via a sub-agent Port from Brighter: - .agent_instructions/documentation.md gains the ADR contract shared by /adr, /spec:design and /spec:review: ADR structure (one skeleton, headings verbatim), Terms, ADR readability (behaviour before structure, orienting artefact first, concentrated citations, sentence rules), Correcting an ADR, Diagrams in ADRs (mermaid traps, decision-ladder tables), Before an ADR is committed (render check, escaped-markdown grep, readability measures), and Writing tone for design documents. - /spec:design delegates drafting to a Plan sub-agent on opus that verifies references and returns the ADR body plus proposed summary and tags; the main agent validates against the skeleton, runs the mechanical checks, stamps frontmatter, regenerates the index, and back-fills sibling "Where this ADR sits" maps. - /adr adopts the same skeleton, checks and sibling-map back-fill. - /spec:review design grades Structure and Readability, verifies diagrams by rendering them, checks writing tone, sibling-map completeness and the unifying sentence, with matching calibration. Brighter's full-shape exemplar ADRs are linked on GitHub; Darker's existing ADRs predate the skeleton and are not restructured here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agent_instructions/documentation.md | 324 ++++++++++++++++++++++++- .claude/commands/adr/adr.md | 246 ++++++++++++++++--- .claude/commands/spec/README.md | 17 +- .claude/commands/spec/design.md | 350 +++++++++++++++++++++------ .claude/commands/spec/review.md | 88 ++++++- CLAUDE.md | 2 +- 6 files changed, 910 insertions(+), 117 deletions(-) diff --git a/.agent_instructions/documentation.md b/.agent_instructions/documentation.md index 79df1fb6..5056dafd 100644 --- a/.agent_instructions/documentation.md +++ b/.agent_instructions/documentation.md @@ -48,7 +48,7 @@ public string Status { get; set; } ## Architecture Decision Records -**Recommended Tool**: Use the `/adr <title>` command (see [.claude/commands/adr/adr.md](../../.claude/commands/adr/adr.md)) to create properly formatted ADRs. This automates numbering, template application, and spec linking. +**Recommended Tool**: Use the `/adr <title>` command (see [.claude/commands/adr/adr.md](../.claude/commands/adr/adr.md)) to create properly formatted ADRs. This automates numbering, template application, and spec linking. We are using Architecture Decision Records (ADR) to record important design decisions that we make. When you make a significant decicion about design, that would be useful as context to future reviewers, or explorers of the codebase, please record your design decision as an ADR. @@ -67,6 +67,328 @@ Every ADR carries a YAML frontmatter block (`id`, `title`, `status`, `author`, ` [adr_frontmatter.md](adr_frontmatter.md); `/adr` and `/spec:design` stamp it with the `write_adr_metadata` skill, and `docs/adr/index.md` is generated from it — never hand-edit the index. +### ADR structure + +Every ADR follows one skeleton. A reader who learns the shape on one ADR should be able to +navigate the next without re-reading it, so use these headings verbatim, in this order, at this +nesting level. + +| Heading | Holds | +| --- | --- | +| `## Context` | 2–4 sentences in plain language: what exists, what is wrong with it, why that matters. Do not open by naming four interfaces — a reader cannot hold type names before they know the problem | +| `### Terms` | **only when the ADR introduces domain vocabulary** — the words the Decision turns on, one bullet each, before the reader meets them in an argument. Prefer a pointer to a sibling's entry over a second statement of it. See *Terms* below | +| `### Scope` | what the ADR covers, as lists rather than narrative. **Parent requirement** — the link. **In scope** — one bullet per FR/NFR it discharges, each naming the mechanism that makes the requirement true, plus a bullet for any scope no tagged requirement carries. **Out of scope** — one bullet per boundary a reader could reasonably mistake, each naming the ADR that does cover it. Where this ADR contributes to a requirement another ADR discharges, say so on the bullet and name the owner | +| `### Where this ADR sits` | **only when the ADR is one of a set** — a table mapping each ADR in the set to the one thing it decides, this one bolded and marked *(this one)*, then the single sentence that unifies them | +| `### {the problem}` | named as a behaviour, not as a structure. Lead with the orienting artefact — a comparison table or a diagram — then the consequences, then the mechanism that produces them | +| `### The forces` | one bullet per constraint that narrows the solution space, so the Decision's shape is legible before it is stated | +| `## Decision` | the decision in **one bold sentence**, then one short paragraph on the shape it takes. No signatures, no file paths | +| `### The mechanism, end to end` | **behaviour**: what happens, in what order. Lead with a sequence diagram, flowchart or decision-ladder table, then read the load-bearing invariants off it | +| `### Where the pieces live` | **structure**: a flowchart with one subgraph per assembly, showing what is new and which way dependencies point | +| `### Key Components` | opens with `#### The roles, and what each is responsible for` — a table of **Role** / **Type** / **Responsibilities** / **Responsibility classifier** / **Collaborators**. *Role* is one phrase saying what the type does; a type needing more than one phrase has too many responsibilities. *Responsibilities* may be several. *Responsibility classifier* is **knowing**, **doing** or **deciding**, and one type may carry more than one. *Collaborators* are the types it works with to meet them. Then each significant type with a contract table (Member / Input / Output / Error conditions), then `#### Where each type is touched` (Assembly / Type / Change), closing with what is deliberately **unchanged** | +| `### Technology Choices` | why this mechanism and not the obvious one, each question its own `####` sub-heading | +| `### Implementation Approach` | the implementor's section, and the only place `file:line` density belongs. Numbered, in commit order, structural changes separated from behavioural ones per Tidy First | +| `## Consequences` | `### Positive`, `### Negative`, `### Risks and Mitigations`. An ADR with only positive consequences reads as unreviewed | +| `## Alternatives Considered` | genuine rejection rationale, not strawmen. State the do-nothing option when the ADR delivers no behaviour | +| `## References` | parent requirement, related ADRs, external references | + +Darker's ADRs predate this skeleton. Brighter's +[0070-per-pipeline-di-scope-for-mapper-and-transform-factories](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0070-per-pipeline-di-scope-for-mapper-and-transform-factories.md) +is the worked example of the full shape, and +[0072-ambient-scope-adoption-seam](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0072-ambient-scope-adoption-seam.md) shows the +decision-ladder form. `docs/adr/0001-record-architecture-decisions.md` remains the minimal template. + +### Terms + +*Sentence construction* below licenses this domain's vocabulary. It does not explain it. Where an +ADR introduces a domain word its Decision turns on, that ADR owes a `### Terms` block in +`## Context`, ahead of `### Scope`. + +Brighter's +[0067-per-resolution-di-scope-for-transient-factory-instances](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0067-per-resolution-di-scope-for-transient-factory-instances.md) +carries the definitional form, and +[0070-per-pipeline-di-scope-for-mapper-and-transform-factories](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0070-per-pipeline-di-scope-for-mapper-and-transform-factories.md) +carries the pointer form. The pointer form is the one to reach for first. + +**Write an entry as a pointer wherever the body already carries the content.** An entry that +restates a paragraph puts the same sentence in the document twice. *"Step 5 specifies its steps"* +is a complete entry: the reader now knows the word, and knows where its rules live. + +**Then sweep for what the block duplicates.** The block has two surfaces, and the second is the one +writers miss: + +- **The pointer it replaces.** A new block usually supersedes a sentence elsewhere that deferred to + a sibling ADR. Delete that sentence. Do not leave it standing beside the block. +- **Every definition the block states** — including the glosses under `## References`, because a + gloss is prose. Grep the body for each sentence the block now asserts. + +In Brighter, seven blocks were written under this rule and every one of them duplicated something. The two whose +body text came back clean were the two written as pointers from the start, and both still convicted +on `## References`. Treat the sweep as a step in writing a `Terms` block, not as a check on one. + +### ADR readability + +An ADR has two audiences and they want different things. A **human reviewer** reads for the *why* +of a behaviour — behaviour is what endures, because structure changes under refactoring. An +**implementing agent** reads for the structural detail. Serve the human first, because the agent +will scroll and the human will stop reading. + +- **General to specific.** Principle, then approach, then the detail an implementor needs. A + reader should be able to stop after any section and hold a true, if less complete, picture. +- **Behaviour before structure.** Say what *happens* before you say what *types exist*. Never + open a Decision with an interface declaration. +- **Lead each section with the artefact that orients** — a table, a diagram, a contract — then + write prose that reads the consequences off it. Never make the reader assemble a picture from + paragraphs and then show them the diagram. +- **Concentrate the citations — `file:line` and requirement IDs alike.** Both are load-bearing for + a reviewer cross-checking coverage and pure noise inside an argument. State the design point in + prose, then list the FRs and ACs it satisfies as bullets beneath it. A reader following the + design skims the list; a reviewer checking coverage reads only the list. Never thread three + requirement IDs through one sentence. At most one `file:line` per forces or Consequences bullet. + **Prefer a slightly longer document to a terse one:** a paragraph plus a list is more readable + than one dense sentence, and it serves both readers instead of neither. + - **The three-ID bar binds argument prose, and exempts a bullet whose subject *is* a + requirement.** `### Scope` is organised requirement by requirement, so its bullets cite what + they discharge; the same holds for a `Consequences` bullet and for the `## References` + inventory. Cutting those lists drops information rather than words. The bar is about an + argument a reader has to follow, not about a list a reviewer reads as a list. +- **Name what is unchanged**, so a reviewer does not read an omission as an oversight. +- **State the unifying rule once, in one sentence**, and repeat that exact sentence in every + sibling ADR that applies it. If it will not fit in one sentence, it is not yet one decision. +- **Emphasis is a symptom.** Bold marks the one sentence a section turns on; the Decision's single + bold sentence is the model. If a paragraph needs bold to make its point findable, the paragraph + is wrong — split it, or lead with the point. A section with bold in every paragraph has + emphasis in none of them. +- **A bold lead-in is a heading that was never promoted.** Where a paragraph opens with a bolded + label — a step number, a question, a sub-topic — make it a `####` sub-heading. Emphasis becomes + navigation, and no sentence changes. **The test is that the bold-lead count falls by what the + `####` count rises.** A pass that raises both has added structure on top of the emphasis instead + of replacing it. Three checks keep it honest: + - **Promote only bold that leads a *paragraph*.** Bold at the head of a *list item* is a + different population, and a numbered list is already the structure this rule asks for. + Promoting one raises `####` and removes no emphasis at all. + - **Read the start of the block that follows each promotion.** A lead-in trailed by more bold — + `**4a. A test project…** **Eight** of the criteria…` — leaves that second run at the head of + the block, where it is a bold lead again. The column does not move, through no fault of the + heading. + - **Leave a form that the ADR's siblings share.** `## Alternatives Considered` uses one entry + form across a whole set. Converting it in one ADR is a set-level change, not a readability + edit. + +**Sentence construction.** Follow these rules from +[Simplified Technical English](https://www.asd-europe.org/standards-specifications/simplified-technical-english/), +so that a non-native reader and a reader in a hurry get the same meaning: + +- **One idea per sentence, and no more than about 25 words.** A sentence carrying three + cross-references is two sentences and a list. +- **Active voice with a named actor.** *"The factory disposes the scope"*, not *"the scope is + disposed"*. +- **No ambiguous `this`, `it` or `that`.** Repeat the noun. A reader should never have to search + backwards for a referent. +- **One term per concept, every time.** Do not vary the wording for elegance; vary it only when + the thing itself differs. +- **No noun stacks.** *"the pipeline scope handle release ordering rule"* is a sentence pretending + to be a phrase. +- **State the decision, not the argument that reached it.** *"Raising those five was rejected"* + records a deliberation; the ADR records what is true. Rejected options belong in + `## Alternatives Considered`, with their reasons. +- **State the rule that holds, not the revision that changed it.** *"FR-27.1 was amended in + revision 28 to match"* records the requirements document's history; the ADR records what is now + true. This is a third kind of argument, and neither the rule above nor *Do not reference + ephemeral working state* reaches it: a requirements history table is durable, so citing it passes + both as written. The requirements document already keeps that record. An ADR that repeats it + dates itself, and tells a reader in two years about a revision they will never look up. + +This is a named subset of the standard, not the full ASD-STE100: the value is in the writing rules, +and the controlled dictionary would fight this domain's vocabulary (*ambient*, *affinity*, +*discharge*, *borrow*, *bracket*). Licensing those words is not explaining them, which is what the +`### Terms` block above is for. + +*Writing tone for design documents* below applies to every ADR and is not optional: an ADR that +records what a participant in the authoring conversation said, rather than what was decided and +why, has failed its only audience. + +### Correcting an ADR + +Most edits to an ADR are corrections: a reviewer finds a claim that is wrong, over-stated or +mis-cited, and one sentence has to change. Corrections are also how a readable ADR becomes an +unreadable one. Each fix is small and locally justified, and what accumulates is a document of +hedged sentences, bolded caveats and recorded arguments that nobody ever decided to write. +**A correction must leave the document at least as readable as it found it.** The rules above are +not suspended because a finding is being closed. + +- **Replace, do not append.** A sentence that over-claims is answered by rewriting that sentence, + not by adding the qualifier that makes it true. Two sentences where there was one, the second + taking back part of the first, is how a paragraph reaches four hundred words. +- **Every qualifier a fix adds is a claim**, and it can contradict a section the fix never looked + at. Prefer the narrow statement to the broad statement plus its exception. +- **Correct the source, not only the statement derived from it.** A wrong summary usually has a + wrong sentence behind it. Fixing the summary alone leaves the generator in place, and the next + reader derives the same error again. +- **A fix that needs bold to be found is in the wrong place.** Move the point to the lead of its + section instead of emphasising it where it sits. +- **The finding's argument is not the fix.** Record what is true. A rejected reading belongs in + `## Alternatives Considered` with its reason, not as a warning inside the prose. +- **When a finding can only be closed by making the document worse, stop and say so.** Keeping a + longer, plainer form for one sentence is a legitimate outcome. Record the reason in the commit + message rather than absorbing it silently. + +**Measure a batch of corrections, not only a rewrite.** Run *Measure the readability* below at both +ends of the batch, and put both figures in the commit message. A batch that raises a column owes an +explanation. Then run the whole-document re-read below: a batch of corrections is the input that +check exists for. + +### Diagrams in ADRs + +Prefer **mermaid** to ASCII art: it is editable, it renders on GitHub, and it survives reflowing. +Choose the form by what is being shown: + +| Showing | Use | +| --- | --- | +| a sequence of calls over time, or who owns what and when | `sequenceDiagram` | +| assemblies and packages, and which way dependencies point | `flowchart` with one `subgraph` per assembly | +| a small branch — three or four outcomes | `flowchart` | +| a protocol with more than about four decision points | a **decision-ladder table**, not a flowchart | +| the types a decision introduces, and how they relate — implements, holds, creates | `classDiagram` | + +Reach for a diagram sooner than feels necessary. If a paragraph needs three cross-references +before it makes sense, draw it: a class diagram for how types relate, a sequence diagram for who +calls whom and in what order. + +A flowchart with nine decision nodes renders as an unreadable column with edges spanning its whole +height. When a protocol branches that much, write it as a numbered table — one row per situation, +in evaluation order, with columns for the outcome and any diagnostic — and leave the ordered +pseudo-code in `Implementation Approach`, pointing back at the table. + +Mermaid traps that pass review and then fail to render: + +- **`;` is a statement separator in `sequenceDiagram`.** A semicolon anywhere in message or note + text silently breaks the parse. Use a comma or an em dash. +- **Never put `<` or `>` in a label.** Mermaid renders labels as HTML, so `Lease<T>` swallows the + type parameter. Write `Lease for T` and let the adjacent prose carry the generic. +- **Never use HTML entities** (`<`, `>`, `&`, ` `) — they trip the escaped-markdown + check below. `<br/>` for a line break is fine; use plain spaces elsewhere. +- **Quote every label** containing parentheses, commas or colons: `node["Create(type, scope)"]`. +- Avoid `rect rgb(...)`: the colours are fixed and read badly in whichever theme they were not + chosen for. + +### Before an ADR is committed + +Run these checks every time — they catch defects that survive a careful read. + +**Re-read the whole document.** Correcting statements one at a time produces a document that +contradicts itself: a fix lands, and a sentence three sections away that depended on the old +wording is now false, redundant, or an argument for something no longer in the ADR. After any +round of statement-level edits, read the ADR start to finish and fix what the fixes broke. This is +a separate pass from the edits themselves, and it is where near-duplicate paragraphs, stale +summaries and orphaned rationale are found. + +**Render every mermaid diagram.** Roughly one diagram in six fails on first draft, and a diagram +that does not render is a broken ADR that looked fine in review. Extract each mermaid block to its +own `.mmd` file under the scratchpad and render it: + +```bash +npx -y -p @mermaid-js/mermaid-cli@11 mmdc -i diagram.mmd -o diagram.svg +``` + +A non-zero exit or a missing `.svg` is a failure — read the parse error, fix the ADR, re-run. Then +render the most complex diagram to PNG (`-o diagram.png -w 1600 -b white`) and actually *look* at +it: a diagram can parse cleanly and still be unreadable, which is the signal to convert it to a +decision-ladder table. If the check cannot run at all (no network for `npx`), say so plainly rather +than reporting the diagrams as verified. + +**No escaped markdown**, which breaks C# generics and mermaid labels alike: + +```bash +grep -c '<\|>\|&' docs/adr/{file}.md # must be 0 +``` + +**Measure the readability.** Sentence length, emphasis density and missing structure are the three +rules above that a careful read does not catch. They are also the three Brighter's ADRs breached worst. All three are cheap to count. Measure before and after any restructuring pass or batch of +corrections, and report both ends in the commit message. A column that moves the wrong way owes an +explanation. + +The three instruments are defined here in full, and deliberately so. A definition held somewhere +else does not reproduce. Two of these were written out at this length and rebuilt exactly, first +run, in six separate sessions; the third was recorded as a single line, and a search of its whole +parameter space never recovered the figures it had produced. Same author, same week — the only +variable was how much was written down. + +*Length and emphasis.* Strip the frontmatter, every fenced block including its fences, every ATX +heading line and every table line. A **block** is a maximal run of lines bounded by a blank line, +split further at each list-item lead — `^\s*(?:[-*+]|\d+[.)])\s+`, in which an alpha-suffixed +ordinal such as `5a.` is **not** a lead. A **bold run** is one non-greedy `\*\*(.+?)\*\*` match. A +**bullet lead** is the first bold run of a list block that opens with `**`; every other bold run +counts as prose, and the two bold columns are therefore disjoint. Report blocks, blocks over 150 +words, blocks over 200 words, bold runs in prose, bold runs at bullet leads, and diagrams. + +*Structure.* Over the region from `### Technology Choices` to `## References`, on the same block +model, a heading line, a table line and a fenced block are breaks. A **list block** opens with a +list-item lead; every other block is a **prose block**. Report `####` headings; list blocks as a +share of all blocks; **bold leads**, meaning prose blocks whose text opens with `**` — disjoint +again from the bullet-lead column above; **run**, the longest chain of consecutive prose blocks +with no heading, list, table or fence between them; and **span**, the longest heading-free stretch +of source lines. Span needs two conventions or it will not reproduce: the region includes its +terminating `## References` heading as a boundary, and a span is the lines *strictly between* two +consecutive headings. + +*Language.* Take the whole file, frontmatter included. Drop fenced blocks with their fences, but +keep table lines and let `|` separate units, so a long table cell is measured rather than hidden. +Strip `**`. A unit ends at `.`, `:` or `;` before whitespace, at a line break, or at `|`. The line +break is load-bearing: without it, two list items that carry no terminal punctuation merge into one +phantom sentence. Protect `file:line` refs and decimal identifiers such as `FR-27.1` from the +split. Report units of 40+, 50+ and 60+ words, the worst unit, and units carrying two or more +em-dash asides. Match requirement IDs with +`\b(?:NFR|FR|AC|OOS|C|D)-\d+(?:\.\d+)?[a-z]?\b|\bD\d+[a-z]?\b`; the second alternative is +load-bearing, because `D0b` and `D19` carry no hyphen and dropping them loses about a dozen +distinct IDs per document. + +**Name the target for each column, and name what floors it.** A column reported without its floor +reads a pass as a failure. + +- Sentences over about 25 words, and revision-history phrases, should reach **zero**. Neither has + a floor. +- `####` should rise, and bold leads should fall by about the same number. That pairing is the + conversion test above. +- **Run and span are floored by any section whose form is fixed across a set of ADRs.** Where + `## Alternatives Considered` holds the region's longest unbroken prose chain, that ADR's `run` + cannot move at all; where it holds the longest heading-free stretch, neither can its `span`. The + floor is that section's longest **chain**, not its entry count: tables and lists break entries + apart, and one ADR here had twelve entries and a chain of six. **Measure where a column sits + before naming it a target** — one pass over the region's consecutive heading pairs settles it. +- **Identifier density has no useful target**, for the reason under *Concentrate the citations*: + most breaches are citation lists, and cutting them loses information. Report it and leave it. + Filter it by **source line**, never by sentence — re-partitioning a long citation line multiplies + its units and fakes a movement that did not happen. + +**Re-run an instrument; never hand-correct its output.** A figure that looks wrong is re-measured by +running the definition again. Editing the number instead reintroduces what the definition exists to +prevent. One correction here restated an ID count with the second alternative of that regex silently +dropped. That is the omission the regex's own definition warns about, committed while fixing it. + +## Writing tone for design documents + +This guidance applies to ADRs, requirements specs, design specs, and any other long-lived document under `docs/` or `specs/`. + +**Write for a future reader, not for the current conversation.** The audience is a contributor reading the document six months or two years from now to understand a design decision. They have no visibility into the chat that produced it. + +**Refer to requirements and design artifacts, not to the participants in the authoring conversation.** Concretely: + +- ❌ "at the user's direction" → ✅ "per requirement C3" or just state the decision +- ❌ "the user's feedback was singular ('an abstract base class')" → ✅ "requirement F5 specifies a single abstract base" +- ❌ "the user explicitly accepted this cost" → ✅ "the cost is accepted per requirement C1 (spec 0028 lands as PR review feedback, not greenfield work)" +- ❌ "if the user wants the interface anyway during review, the re-introduction is mechanical" → ✅ remove — review-loop asides do not survive past the review +- ❌ "Direct rendering from feedback item 5's framing" → ✅ "Aligns with requirement F4 (payload-mode validator role)" +- ❌ "the spec 0027 PROMPT suggested otherwise" → ✅ replace with the actual technical reason; PROMPT.md is ephemeral working state + +**Do not quote conversational asides.** Phrases like *"Arguably it would have been better caught earlier"* or *"we could possibly use ..."* belong in chat transcripts and PR review threads, not in design documents that outlive them. + +**Do not reference ephemeral working state.** PROMPT.md, current spec phase ("we are in the requirements phase"), conversation transcripts, and unresolved review back-and-forth are all transient. Either fold the substance into the document, or omit it. + +**Trace decisions to durable artifacts.** Acceptable references include: requirement IDs (F1, NF2, C3), other ADRs, code locations, principles in `.agent_instructions/`, and external specifications. References to GitHub PRs and issues are acceptable as historical anchors but should not carry the design rationale — the rationale must live in the document itself. + +**The rule of thumb:** if removing the sentence would leave the future reader less informed about *the design*, keep it. If it would only leave them less informed about *who said what when*, remove it. + + ## Licensing - We add a license comment to every src file diff --git a/.claude/commands/adr/adr.md b/.claude/commands/adr/adr.md index 5c6a23bc..d71e2b25 100644 --- a/.claude/commands/adr/adr.md +++ b/.claude/commands/adr/adr.md @@ -18,7 +18,7 @@ Create a new Architecture Decision Record in the `docs/adr/` directory following ### Step 1: Determine Next ADR Number -Find the highest numbered ADR file in `docs/adr/` and calculate the next sequence number. +Find the highest numbered ADR file in `docs/adr/` and take the next sequence number. ```bash ls -1 docs/adr/*.md | grep -E '^docs/adr/[0-9]+' | sed 's/docs\/adr\/\([0-9]*\).*/\1/' | sort -n | tail -1 @@ -32,9 +32,9 @@ The next ADR number is the highest number + 1, formatted as a 4-digit number wit **The number is a non-unique ordering hint, not the ADR's identity.** Numbers are allocated `max+1` per branch without locking, so parallel branches can collide — `0020` is already shared by two ADRs, and we do **not** renumber. The **identity is the `id`/slug = the filename stem** -(`0020-validation-decorator-architecture`), which is unique; all tooling keys off that. So: take -`max+1`, but if a concurrent branch later lands the same number, that collision is **acceptable and -not a defect** — do not renumber to "fix" it. See *Identity and numbering* in +(`0020-validation-decorator-architecture`), which is unique; all tooling keys off that. +So: take `max+1`, but if a concurrent branch later lands the same number, that collision is +**acceptable and not a defect** — do not renumber to "fix" it. See *Identity and numbering* in [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). ### Step 2: Check for Linked Specification @@ -53,19 +53,29 @@ If a current spec exists: - This ADR should reference the parent requirement - Add this ADR to `specs/[spec-name]/.adr-list` -### Step 3: Read the ADR Template +### Step 3: Read the ADR Authoring Guidance -Read the foundational ADR to understand the expected structure: -- `docs/adr/0001-record-architecture-decisions.md` - Basic template +**Read [`.agent_instructions/documentation.md`](../../../.agent_instructions/documentation.md) +§ *Architecture Decision Records* before drafting.** It is the single source for the ADR +skeleton, the readability rules, the diagram rules and the pre-commit checks — the same source +`/spec:design` uses, so the two commands cannot drift. The Step 5 template below is that +skeleton; the guidance says what goes in each heading and why. -If the first ADR does not exist yet, use the template from Step 5 directly. +Then read the worked examples: +- `docs/adr/0001-record-architecture-decisions.md` — Darker's minimal template +- Brighter's [`0070-per-pipeline-di-scope-for-mapper-and-transform-factories.md`](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0070-per-pipeline-di-scope-for-mapper-and-transform-factories.md) + — the full shape: sequence diagram, assembly flowchart, roles table, contract tables +- Brighter's [`0072-ambient-scope-adoption-seam.md`](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0072-ambient-scope-adoption-seam.md) — the + decision-ladder form, for a protocol with too many branches to draw as a flowchart + +Darker has no ADR yet that follows the full skeleton, so the full-shape exemplars are Brighter's. ### Step 4: Gather Information from User **Surface prior art first.** Before gathering new content, find existing ADRs relevant to this decision so you reuse, avoid contradicting, or deliberately supersede them rather than re-deciding in -ignorance. Use the `read_adr_metadata` skill, passing the decision title as the query and any -obvious tags from the taxonomy: +ignorance. Use the `read_adr_metadata` skill, passing the decision title as the +query and any obvious tags from the taxonomy: ``` read_adr_metadata "{decision title}" --tags "{likely-tags}" @@ -105,7 +115,7 @@ Create the file at: `docs/adr/[NNNN]-[title].md` **File naming**: - Use 4-digit sequence number with leading zeros - Use dash-case (kebab-case) for the title -- Example: `0002-query-caching-strategy.md` +- Example: `0022-query-caching-strategy.md` **File structure**: @@ -120,30 +130,87 @@ Proposed ## Context +[2-4 sentences in plain language: what exists today, what is wrong with it, why that matters. +Do NOT open by naming four interfaces — a reader cannot hold type names before they know the +problem. Name the defect in terms a user would recognise.] + [If linked to spec] **Parent Requirement**: [specs/[spec-name]/requirements.md](../../specs/[spec-name]/requirements.md) -**Scope**: [One sentence describing what this ADR covers] +**Scope**: This ADR decides one thing — [the decision, in one bold clause]. [If linked to a spec, +the requirement ids it discharges.] + +[What this ADR deliberately does NOT decide, and where each of those things is decided instead.] -### The Problem +### Where this ADR sits -[Describe the architectural problem or decision that needs to be made] +[INCLUDE ONLY IF THIS ADR IS ONE OF A SET — a spec's ADRs, or a related sequence. A table +mapping each ADR in the set to the one thing it decides, this one bolded and marked *(this one)*:] -### Requirements Context +| ADR | Decides | +| --- | --- | +| 00NN | [the one thing it decides] | +| **00NN** *(this one)* | [the one thing THIS ADR decides] | -[Reference any relevant requirements from the parent spec] +[Then, if the set shares one, the single sentence that unifies them — stated identically in every +sibling that applies it.] -### Constraints +### [The problem, named as a behaviour rather than as a structure] -[List any constraints that influence the decision] +[Lead with the artefact that orients — a comparison table or a diagram — then the consequences +that follow from it, and only then the mechanism that produces them.] + +### The forces + +[Bulleted. Each force is one constraint that narrows the solution space, so the Decision's shape +is legible before it is stated. Includes the constraints and requirements context that older ADRs +put under separate headings.] ## Decision -[Describe what you decided to do and WHY] +**[The decision, in one bold sentence. A reader who reads only this sentence should know what was +decided and be able to recognise it in the code.]** + +[One short paragraph on the shape that takes, and why. No type signatures, no file paths yet.] + +### The mechanism, end to end + +[BEHAVIOUR FIRST. What happens, in what order. Lead with a mermaid `sequenceDiagram`, `flowchart` +or decision-ladder table, then two or three sentences reading the load-bearing invariants off it. +This is the section a human reviewer actually reads.] + +### Where the pieces live + +[STRUCTURE SECOND. A mermaid `flowchart` with one `subgraph` per assembly or package, showing what +is new, what changes, and which way dependencies point.] + +### Key Components + +#### The roles, and what each is responsible for -### [Sub-sections as needed] +| Role | Type | Stereotype | Responsibility | +| --- | --- | --- | --- | +| [role] | [type] ([where it lives]) | **knowing** / **doing** / **deciding** | [what it is answerable for] | -[Break down complex decisions into clear sub-sections] +[Then a sentence on the split that matters most.] + +#### [Each significant type] + +[The signature, then a contract table: Member | Input | Output | Error conditions.] + +#### Where each type is touched + +[A table: Assembly | Type | Change. Then a paragraph naming what is deliberately UNCHANGED, so an +omission is not read as an oversight.] + +### Technology Choices + +[Why this mechanism and not the obvious one. Each as a bolded question.] + +### Implementation Approach + +[The implementor's section, and the ONLY place `file:line` citation density belongs. Numbered +steps in commit order, structural changes separated from behavioural ones per Tidy First.] ## Consequences @@ -153,22 +220,21 @@ Proposed ### Negative -[Drawbacks or costs from this decision] +[Drawbacks or costs from this decision. An ADR with only positive consequences reads as +unreviewed — state the migration cost and who pays it.] ### Risks and Mitigations -**Risk**: [Description of risk] -- **Mitigation**: [How you'll address it] +| Risk | Mitigation | +| --- | --- | +| [Description of risk] | [How you'll address it, and which test or check is the guard] | ## Alternatives Considered -### Alternative 1: [Name] - -[Description] +**1. [Name].** [Description.] **Rejected because** [real rationale, not a strawman]. [State the +do-nothing option when the ADR delivers no behaviour.] -**Rejected because**: -- [Reason 1] -- [Reason 2] +**2. [Name].** [Description.] **Rejected because** [reason]. ## References @@ -181,10 +247,11 @@ Proposed ``` **Add frontmatter**: after writing the body above, stamp the YAML frontmatter block with the -`write_adr_metadata` skill. It derives `id`/`title`/`created` from the file, sets `status: Proposed`, -and keeps the frontmatter in sync with the body `## Status`. Provide a `summary` (1–2 sentences on -WHAT was decided — an agent reads this to decide whether to open the ADR) and 1–4 `tags` from the -controlled taxonomy in [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md): +`write_adr_metadata` skill. It derives `id`/`title`/`created` from the file, +sets `status: Proposed`, and keeps the frontmatter in sync with the body `## Status`. Provide a +`summary` (1–2 sentences on WHAT was decided — an agent reads this to decide whether to open the ADR) +and 1–4 `tags` from the controlled taxonomy in +[`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md): ``` write_adr_metadata docs/adr/[NNNN]-[title].md init --summary "..." --tags "tag-a,tag-b" @@ -195,6 +262,35 @@ taxonomy, valid date, all seven keys, frontmatter status matches body). If this prior one you found in Step 4, also mark that older ADR: `write_adr_metadata docs/adr/[old-id].md supersede --by [NNNN]-[title]`. +**Run the mechanical checks** — see +[`.agent_instructions/documentation.md`](../../../.agent_instructions/documentation.md) +§ *Before an ADR is committed*. Both, every time; they catch defects that survive a careful read. + +1. **Render every mermaid diagram.** Roughly one diagram in six fails on first draft, and a + diagram that does not render is a broken ADR that looked fine in review. Extract each mermaid + block to a `.mmd` file under the scratchpad and render it: + + ```bash + npx -y -p @mermaid-js/mermaid-cli@11 mmdc -i diagram.mmd -o diagram.svg + ``` + + A non-zero exit or a missing `.svg` is a failure — read the parse error, fix, re-run. Then + render the most complex diagram to PNG (`-o diagram.png -w 1600 -b white`) and actually *look* + at it with Read: a diagram can parse cleanly and still be unreadable, which is the signal to + convert it to a decision-ladder table. If the check cannot run (no network for `npx`), say so + rather than reporting the diagrams as verified. + +2. **No escaped markdown**, which breaks C# generics and mermaid labels alike: + + ```bash + grep -c '<\|>\|&' docs/adr/[NNNN]-[title].md # must be 0 + ``` + +**Back-fill the sibling map.** If this ADR joined a set — a spec's `.adr-list`, or a related +sequence — every earlier ADR's `### Where this ADR sits` table is now stale. Add the new row to +each, keeping each file's own row bolded. A map that is right for the newest ADR and wrong for the +rest is worse than no map, because a reader who finds it stale once will not check it again. + ### Step 6: Regenerate the ADR index The new frontmatter (and any supersession from Step 5) must be reflected in the derived @@ -234,11 +330,15 @@ Read the ADR: - Review the ADR using the design guidelines from "Use Responsibility-Driven Design" - Check for a design focused on behavior: does the ADR think about *roles* and *responsibilities*. - Responsibilities are "knowing", "doing", and "deciding" -- Allocate responbilities into roles, focusing on cohesion. +- Allocate responsibilities into roles, focusing on cohesion. - Roles are interfaces or abstract types - A class can implement one or more roles. If it implements multiple roles, they should be related. - Provide feedback and suggest improvements +**The roles table under `### Key Components` is where this review lands.** If the ADR has no such +table, or its rows are types rather than roles, RDD was applied to the review and not to the +design — send it back rather than accepting prose that mentions responsibilities. + ### Step 10. Suggest next steps: Ask the user for the next step: @@ -256,20 +356,90 @@ From [.agent_instructions/documentation.md](../../../.agent_instructions/documen - One architectural decision per ADR - stay focused - Focus on WHY, not just WHAT - Document alternatives considered and why they were rejected -- Use diagrams (ASCII art or mermaid) where helpful - Cross-reference related ADRs - First ADR should be first commit on feature branch - Create draft PR with ADRs for early feedback +**The skeleton, the readability rules, the diagram rules and the pre-commit checks are all in +[`.agent_instructions/documentation.md`](../../../.agent_instructions/documentation.md) +§ *Architecture Decision Records*** — one source, shared with `/spec:design`, so the two commands +cannot drift. Do not restate them here; if a rule needs to change, change it there. + +Two that are worth naming because they are the easiest to lose under time pressure: + +- **Behaviour before structure.** `### The mechanism, end to end` precedes `### Where the pieces + live`, which precedes the signatures. Never open a Decision with an interface declaration. +- **Write for a future reader, not for this conversation** (§ *Writing tone for design documents*). + An ADR that records what a participant said, rather than what was decided and why, has failed + its only audience. + +**Common ADR Topics**: +- Pipeline and decorator design +- Error handling and resilience approach +- Handler and decorator lifetime +- API design and contracts +- Performance optimization strategy +- Observability (tracing, metrics, logging) +- Testing approach + **File Conventions**: - Place in `docs/adr/` directory - Use sequence number format: `NNNN-title.md` (4 digits with leading zeros) - Use dash-case (kebab-case) for titles - Scan existing ADRs to avoid duplication +## Example Interaction + +``` +User: /adr query caching strategy + +[Claude scans docs/adr/ and finds highest is 0021] +Next ADR number: 0022 + +[Claude checks for current spec and finds specs/015-query-caching/] +Found current spec: specs/015-query-caching/ +This ADR will be linked to that specification. + +[Claude runs read_adr_metadata "query caching strategy" --tags "caching,decorators"] +Prior art: 0021-caching-decorator-architecture [Accepted] + +[Claude uses AskUserQuestion] +What is the architectural problem or decision to be made? +> We need to decide how cached query results are invalidated when the underlying data changes... + +What is your proposed solution? +> Tag cache entries from the query context and evict by tag... + +What alternatives did you consider? +> We considered time-based expiry only, but rejected it because... + +What are the key consequences? +> Positive: Precise eviction. Negative: Callers must supply tags... + +[Claude creates docs/adr/0022-query-caching-strategy.md, stamps frontmatter, regenerates the index] +Created: docs/adr/0022-query-caching-strategy.md + +Status: Proposed + +Next steps: +1. Review and edit the ADR as needed +2. Commit: git add docs/adr/0022-query-caching-strategy.md docs/adr/index.md && git commit -m "docs: add ADR for query caching strategy" +3. Create more ADRs if needed, or approve design with /spec:approve design +4. Status will change to "Accepted" when approved + +The ADR has been added to specs/015-query-caching/.adr-list +``` + ## Notes - ADRs should be created during the design phase, before implementation - Multiple focused ADRs are better than one large ADR covering everything -- ADRs are immutable once accepted - supersede with new ADRs rather than editing -- The `/spec:design` command uses this same workflow when working with specifications +- **An accepted ADR's *decisions* are immutable — supersede with a new ADR rather than editing.** + That immutability is about the decision, not the prose. Restructuring an accepted ADR for + readability — reordering sections, adding a diagram or a roles table, moving `file:line` + citations into `Implementation Approach`, fixing a title — is allowed and is not a supersession, + provided no decision, fact, citation or requirement reference changes. Say so explicitly when + you do it, and verify it: no citation present before may be absent after. +- The `/spec:design` command uses this same workflow when working with specifications, and both + commands read their skeleton and rules from + [`.agent_instructions/documentation.md`](../../../.agent_instructions/documentation.md) diff --git a/.claude/commands/spec/README.md b/.claude/commands/spec/README.md index 433a7288..2647d3ea 100644 --- a/.claude/commands/spec/README.md +++ b/.claude/commands/spec/README.md @@ -79,7 +79,7 @@ context clean and gives the heavy work a focused, single-purpose context. what is in its prompt. The command reads the needed files (and runs `gh`/`git`) first, then passes the text or paths. 2. **Launch `Agent`** with an explicit `subagent_type` and `model`: - - **`/spec:tasks`** uses `subagent_type: "Plan"`. `Plan` has all tools **except** `Agent`, + - **Planning commands** (`/spec:design`, `/spec:tasks`) use `subagent_type: "Plan"`. `Plan` has all tools **except** `Agent`, `ExitPlanMode`, `Edit`, `Write`, and `NotebookEdit` — so it can Read/Glob/Grep/Bash but has no file-editing tool. That makes it much **harder** for the sub-agent to accidentally write the spec file than relying on the prompt alone (it still has `Bash`, so the prompt also @@ -92,7 +92,7 @@ context clean and gives the heavy work a focused, single-purpose context. runs to completion and returns; it cannot pause to ask the user anything. So before launching, the main agent clarifies any ambiguous inputs with the user via `AskUserQuestion`, then launches the sub-agent with the clarified inputs folded in. The - `Plan`-based `tasks` sub-agent has no `AskUserQuestion` so it *structurally* can't prompt; the + `Plan`-based `design` and `tasks` sub-agents have no `AskUserQuestion` so it *structurally* can't prompt; the `general-purpose` `review` sub-agent is explicitly instructed not to. **Exception:** `/spec:ralph-implement` runs fully **unattended** — neither its orchestrator nor its sub-agent prompts the user once the loop starts. @@ -102,19 +102,20 @@ context clean and gives the heavy work a focused, single-purpose context. 5. **The main agent validates** the returned output against a checklist, then writes the file and does all bookkeeping (approval markers, `.adr-list`, git, next-steps). -The remaining planning commands (`/spec:requirements`, `/spec:design`) currently run inline in the -main agent rather than delegating — they have no sub-agent to assign a model to. +`/spec:requirements` still runs inline in the main agent rather than delegating — it has no +sub-agent to assign a model to. **Model policy** — reasoning vs. implementation: | Command | Sub-agent (type) | Model | Rationale | |---------|------------------|-------|-----------| +| `/spec:design` | Yes — `Plan` (read-only) | **opus** | Architecture / design | | `/spec:tasks` | Yes — `Plan` (read-only) | **opus** | Planning / coverage mapping | | `/spec:review` | Yes — `general-purpose` | **opus** | Adversarial reasoning | | `/spec:ralph-implement` (orchestrator) | — (the loop itself) | **opus** | Cheap bookkeeping + **required for auto mode** | | `/spec:ralph-implement` (per-task sub-agent) | Yes — `general-purpose` (writes source) | **sonnet** | Mechanical TDD implementation, kept off the opus loop context for cost | | `/spec:implement` | No | **sonnet** (Step 0 prompts to switch) | Implementation work; runs in the main agent, so set the session model | -| `/spec:requirements`, `/spec:design` | No (main agent) | — | Run inline | +| `/spec:requirements` | No (main agent) | — | Runs inline | | `/spec:new`, `/spec:switch`, `/spec:approve`, `/spec:status`, `/spec:gear`, `/spec:write_release_notes` | No | — | Mechanical bookkeeping | `/spec:ralph-implement` runs **two models on purpose**: the orchestrator loop on **opus** @@ -164,7 +165,11 @@ Create an Architecture Decision Record (ADR) for a specific architectural decisi /spec:design handler-lifecycle ``` -Before drafting, surfaces prior-art ADRs with `read_adr_metadata` (frontmatter only, retired ADRs +Drafting is delegated to a `Plan` sub-agent on **opus**, which verifies its codebase references +and returns the ADR body (plus a proposed summary and tags); the main agent validates it against the +shared ADR skeleton in `.agent_instructions/documentation.md`, runs the mechanical checks (mermaid +render, escaped-markdown grep), writes the file, and back-fills sibling ADRs' `### Where this ADR +sits` maps. Before drafting, it surfaces prior-art ADRs with `read_adr_metadata` (frontmatter only, retired ADRs skipped). After writing, stamps the new ADR's YAML frontmatter with `write_adr_metadata` (`status: Proposed`, a summary, 1–4 tags from the taxonomy in `.agent_instructions/adr_frontmatter.md`), regenerates `docs/adr/index.md`, and — if the ADR's diff --git a/.claude/commands/spec/design.md b/.claude/commands/spec/design.md index d01f2ea9..c3f466c2 100644 --- a/.claude/commands/spec/design.md +++ b/.claude/commands/spec/design.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(git:*), Bash(awk:*), Bash(date:*), Bash(basename:*), Bash(grep:*), Bash(mkdir:*), Read, Write, Edit, Glob, AskUserQuestion, Skill +allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(git:*), Bash(awk:*), Bash(date:*), Bash(basename:*), Bash(grep:*), Bash(mkdir:*), Bash(npx:*), Bash(python3:*), Read, Write, Edit, Glob, Agent, AskUserQuestion, Skill description: Create technical design specification (ADR) argument-hint: [adr-focus-area] --- @@ -15,8 +15,16 @@ ADR directory: `docs/adr/` **Frontmatter**: Every ADR carries a YAML frontmatter block (`id`, `title`, `status`, `author`, `created`, `summary`, `tags`) — see [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md). -This command reads existing ADRs' frontmatter to surface prior art (Step 3a) and stamps the new -ADR's frontmatter on write (Step 4a) via the `read_adr_metadata` / `write_adr_metadata` skills. +This command reads existing ADRs' frontmatter to surface prior art (Step 4) and stamps the new ADR's +frontmatter on write (Step 7) via the `read_adr_metadata` / `write_adr_metadata` skills. + +**Sub-agent**: The codebase-grounded drafting of the ADR is delegated to a sub-agent +(`subagent_type: "Plan"`, **`model: "opus"`**). `Plan` has no `Write`/`Edit`/`NotebookEdit`, +which makes it much harder to accidentally write the file (the prompt still forbids writing +via `Bash`), while still allowing Read/Glob/Grep to verify references. The sub-agent reads the inputs, verifies references +against the real codebase, and RETURNS the ADR body as text — plus a proposed frontmatter `summary` +and `tags`. The main agent owns numbering, file writing, frontmatter stamping, and `.adr-list` +bookkeeping. See `.claude/commands/spec/README.md` → "Sub-agents & model policy". ## Your Task @@ -37,18 +45,28 @@ ADR's frontmatter on write (Step 4a) via the `read_adr_metadata` / `write_adr_me ### Step 3: Determine ADR Number and Focus 1. Check existing ADRs: `ls docs/adr/ | grep -E "^[0-9]{4}-" | sort | tail -1` -2. If `docs/adr/` doesn't exist, create it: `mkdir -p docs/adr` -3. Calculate next number (format: 0001, 0002, 0003, etc.) +2. Calculate next number (format: 0001, 0002, 0003, etc.) + - Example: if last is `0021-caching-decorator-architecture.md`, next is `0022` + - If `docs/adr/` doesn't exist, create it: `mkdir -p docs/adr` - The number is an **ordering hint, not an identity** — the `id`/slug is the identity, and a number collision with a concurrent branch is acceptable (see `adr_frontmatter.md`). -4. Determine the ADR focus: - - If $ARGUMENTS provided: Use that as the specific focus area +3. Determine the ADR focus: + - If $ARGUMENTS provided: Use that as the specific focus area (e.g., "query-caching", "error-handling") - If not provided: Ask user what aspect of the requirement this ADR addresses -5. Create ADR filename: `docs/adr/{NNNN}-{focus-area}.md` +4. Create ADR filename: `docs/adr/{NNNN}-{focus-area}.md` - Use kebab-case for focus area -6. Add to tracking: `echo "{NNNN}-{focus-area}.md" >> specs/{current-spec}/.adr-list` + - Example: `docs/adr/0022-query-result-caching.md` + + **Do NOT write the ADR file or update `.adr-list` yet** — that happens in Step 7 after + the sub-agent returns and you have validated its output. + + > **Note**: the number is computed here but only *reserved* (written to `.adr-list`) in + > Step 7 after the sub-agent returns. Two `/spec:design` runs started concurrently could + > therefore pick the same number. This is a theoretical race for a single-user dev tool + > and is accepted; if you ever run designs in parallel, double-check the number in Step 7 + > against `docs/adr/` before writing. -### Step 3a: Surface Prior-Art ADRs (before drafting) +### Step 4: Surface Prior-Art ADRs (before drafting) Before drafting, find existing decisions relevant to **{focus-area}** so the new ADR reuses them, avoids contradicting them, or deliberately supersedes them — rather than re-deciding in ignorance. @@ -61,15 +79,73 @@ read_adr_metadata "{focus-area}" --tags "{likely-tags}" ``` It reads only frontmatter (cheap) and by default **skips `Deprecated` and `Superseded`** ADRs, so -retired decisions are not offered as live prior art. Reference the relevant candidates under -`Related ADRs`, and note any this ADR would supersede. Prefer this to reading `docs/adr/index.md`, -which is an un-ranked full-corpus table meant for human browsing. +retired decisions are not offered as live prior art. Keep the returned candidates (`id` + `title` + +`status` + `summary`) — you feed them to the drafting sub-agent in Step 6 so it can cite or +distinguish them, and you flag any that this ADR would supersede. -If no prior art is found, note that and proceed. +`read_adr_metadata` is the query interface over the same frontmatter that `docs/adr/index.md` +tabulates — a ranked, retired-filtered, capped view for exactly this discovery step. Prefer it over +reading `index.md`, which is an un-ranked full-corpus table meant for human browsing, not for loading +into design context. -### Step 4: Create ADR Document +If no prior art is found, note that and proceed. -Create `docs/adr/{NNNN}-{focus-area}.md` with the following template: +### Step 5: Gather Inputs for the Sub-Agent + +The sub-agent starts with a clean context — it only knows what you put in its prompt. +Gather (read) the inputs it needs so you can pass their text or paths: + +- `specs/{current-spec}/requirements.md` — the parent requirement the ADR must address. +- Each existing ADR listed in `specs/{current-spec}/.adr-list` (read from `docs/adr/`) — + so the new ADR stays focused on a *distinct* decision and references siblings correctly. +- The **prior-art candidates from Step 4** (`id` + `title` + `summary`) — so the sub-agent + references related decisions correctly and does not contradict or silently duplicate one. +- `.agent_instructions/documentation.md` — **read it yourself**; you paste four of its sections + into the sub-agent prompt in Step 6 (see *Readability and diagram requirements*). +- `.agent_instructions/design_principles.md` — pass the path; the sub-agent reads it itself. +- `.agent_instructions/adr_frontmatter.md` — pass the path; the sub-agent reads the tag taxonomy + from it to propose `tags`. + +If any required document is missing (e.g. no `requirements.md`), stop and tell the user to +run the appropriate command first. Do NOT launch the sub-agent with missing inputs. + +### Step 6: Launch Sub-Agent to Draft the ADR + +**Verify inputs with the user before launching (MAIN agent).** The `Plan` sub-agent is +one-shot and has no `AskUserQuestion` — it cannot ask the user anything once launched. So +before launching, review the gathered inputs for ambiguity or open design decisions (which +architectural direction to take, the intended scope of this single ADR, trade-off +preferences, anything under-specified in `requirements.md`) and resolve them with the user +via `AskUserQuestion`. Then launch the sub-agent with the clarified inputs folded in. All +user interaction stays in the main agent — never the sub-agent. + +Launch an `Agent` with `subagent_type: "Plan"` and **`model: "opus"`**. The +prompt MUST include all of the following: + +1. The full text of `requirements.md` (or its path if too large to inline). +2. The full text of each existing ADR for this spec (or their paths), and an instruction + that the new ADR addresses a DISTINCT decision: **{focus-area}**. Also pass **one line per + sibling ADR stating the single thing it decides** — the sub-agent needs those to build the + `### Where this ADR sits` map table, and a map built from a summary it inferred will drift + from the one the sibling states about itself. +3. The **prior-art candidates from Step 4** (`id` + `title` + `summary`), with an instruction to + reference related ones under `Related ADRs`, and to state explicitly if this ADR supersedes any + of them (so the main agent can mark the old one `Superseded` in Step 7). +4. The proposed number and filename (`{NNNN}-{focus-area}.md`) and today's date — so the + header and `Related ADRs` references are correct. +5. The ADR template (below) — the sub-agent fills it in. +6. The **Drafting guidance** and **Grounding requirements** blocks below, verbatim, plus the four + `documentation.md` sections named under **Readability and diagram requirements** — pasted in, + not referenced by path. +7. An explicit instruction: **RETURN the completed ADR as markdown text. Do NOT write any + file.** Use Read/Glob/Grep to verify references; do not use Write/Edit. +8. An explicit instruction to **also return, clearly separated from the ADR body, a proposed + frontmatter `SUMMARY:` (1–2 sentences stating WHAT was decided, so a reader can decide whether + to open the ADR) and `TAGS:` (1–4 tags drawn from the controlled taxonomy in + `.agent_instructions/adr_frontmatter.md` — do not invent ad-hoc tags).** The main agent uses + these to stamp the frontmatter in Step 7. + +#### ADR Template (the sub-agent fills this in) ```markdown # {Number}. {Title} @@ -82,52 +158,107 @@ Proposed ## Context -{Describe the specific architectural problem this ADR addresses} +{2–4 sentences, in plain language: what exists today, what is wrong with it, and why that +matters. Do NOT open by naming four interfaces — a reader cannot hold type names before they +know what the problem is. Name the defect in terms a user would recognise.} **Parent Requirement**: [specs/{spec-dir}/requirements.md](../../specs/{spec-dir}/requirements.md) -**Scope**: This ADR focuses specifically on {the architectural decision area}. +**Scope**: This ADR decides one thing — {the decision, in one bold clause}. It discharges {FR/NFR/constraint ids}. + +{What this ADR deliberately does NOT decide, and which sibling ADR decides each of those things.} + +### Where this ADR sits + +{INCLUDE THIS SECTION ONLY IF THE SPEC HAS MORE THAN ONE ADR. A table mapping every ADR for +this spec to the one thing it decides — this one bolded and marked *(this one)*:} + +| ADR | Decides | +| --- | --- | +| 00NN | {the one thing it decides} | +| **00NN** *(this one)* | {the one thing THIS ADR decides} | + +{Then, if the ADRs share one, the single sentence that unifies them — stated identically in +every sibling that applies it. If you cannot write it in one sentence, they are not one story.} + +### {The problem, named as a behaviour rather than as a structure} -{Describe the problem and context}: -- What specific aspect of the requirement needs an architectural decision? -- What are the forces at play (technical, political, social, project)? -- Why is this decision important? -- What constraints exist? +{Lead with the artefact that orients — a comparison table or a diagram — then state the +consequences that follow from it, and only then the mechanism that produces them. A reader +wants to know what goes wrong before they want to know which class does it.} + +### The forces + +{Bulleted. Each force is one constraint that narrows the solution space, and the reader should +be able to see why the Decision below is the shape it is. Keep `file:line` citations to the +minimum that makes the force checkable — density belongs in Implementation Approach.} ## Decision -{Describe the specific architectural decision that was made} +**{The decision, in one bold sentence. A reader who reads only this sentence should know what +was decided and be able to recognise it in the code.}** + +{One short paragraph on the shape that takes, and why. Still no type signatures, still no file +paths.} -- What approach are we taking for this aspect? -- What are the key technical choices? -- What patterns or practices will we follow? +### The mechanism, end to end -### Architecture Overview +{BEHAVIOUR FIRST. What happens, in what order, at run time or at registration time. Lead with +the orienting artefact — a mermaid `sequenceDiagram`, a `flowchart`, or a decision-ladder table +— then two or three sentences reading the load-bearing invariants off it. This section is the +one a human reviewer will actually read; it is why the ADR exists.} -{Describe the architecture for this specific decision} -{Use ASCII art or mermaid diagrams where helpful} +### Where the pieces live + +{STRUCTURE SECOND. A mermaid `flowchart` with one `subgraph` per assembly or package, showing +what is new, what changes, and which way the dependencies point.} ### Key Components -{List and describe the main components affected by this decision} +#### The roles, and what each is responsible for + +{A table, per Responsibility-Driven Design — this is where the design principles become an +artefact rather than an aspiration:} + +| Role | Type | Stereotype | Responsibility | +| --- | --- | --- | --- | +| {role} | {type} ({where it lives}) | **knowing** / **doing** / **deciding** | {what it is answerable for} | + +{Follow the table with a sentence on the split that matters most — usually the one a reader +would otherwise collapse.} + +#### {Each significant type} + +{The signature, then a contract table: Member | Input | Output | Error conditions.} + +#### Where each type is touched + +{A table: Assembly | Type | Change. Then a paragraph naming what is deliberately UNCHANGED, +so a reviewer does not read an omission as an oversight.} ### Technology Choices -{Document specific technology/library choices for this aspect and why} +{Why this mechanism and not the obvious one. Each as a bolded question — "Why X rather than +Y." These are the paragraphs a future maintainer comes back for.} ### Implementation Approach -{Outline how this specific aspect will be implemented} +{The implementor's section, and the ONLY place where `file:line` citation density belongs. +Numbered steps, in the order they would be committed, with structural changes separated from +behavioural ones per Tidy First.} ## Consequences ### Positive - {What becomes easier or better with this decision?} +- {Benefits specific to this architectural choice} ### Negative - {What becomes harder or more complex?} +- {What tradeoffs are we making?} +- {Additional complexity introduced} ### Risks and Mitigations @@ -136,37 +267,127 @@ Proposed ## Alternatives Considered -{Describe other approaches that were considered and why they were not chosen} +{Describe other approaches that were considered for this specific aspect and why they were not chosen} ## References - Requirements: [specs/{spec-dir}/requirements.md](../../specs/{spec-dir}/requirements.md) -- Related ADRs: {List related ADRs} -- External references: {Links to relevant documentation} +- Related ADRs: {List related ADRs, especially others for the same requirement} +- External references: {Links to relevant documentation, articles, etc.} ``` -### Step 4a: Stamp Frontmatter and Regenerate the Index +#### Drafting guidance (include in the sub-agent prompt) -After writing the ADR body: +When creating the ADR: +- Focus on **one specific architectural decision** - keep it focused +- Focus on the **why** of decisions, not just the **what** +- Use mermaid diagrams (see *Diagram requirements* below) — prefer them to ASCII art +- Consider: + - Data model and schema changes + - API design (public interfaces) + - Security implications + - Performance considerations + - Testing strategy + - Deployment considerations + - Backward compatibility + +**Responsibility-Driven Design** — read `.agent_instructions/design_principles.md` and apply it: +- Check for a design focused on behavior: does the ADR think about *roles* and *responsibilities*? +- Responsibilities are "knowing", "doing", and "deciding". +- Allocate responsibilities into roles, focusing on cohesion. +- Roles are interfaces or abstract types. +- A class can implement one or more roles. If it implements multiple roles, they should be related. -1. **Stamp frontmatter** with the `write_adr_metadata` skill - (`.claude/commands/adr/write_adr_metadata.md`), with a summary (1–2 sentences stating WHAT was - decided) and 1–4 tags from the controlled taxonomy in [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md): +#### Readability and diagram requirements (include in the sub-agent prompt) + +These live in [`.agent_instructions/documentation.md`](../../../.agent_instructions/documentation.md) +§ *Architecture Decision Records*, which is the single source shared with the standalone `/adr` +command so the two cannot drift. **Read that file yourself and paste these sections into the +sub-agent prompt verbatim** — do not paraphrase them, and do not merely pass the path, because the +shape of what the sub-agent returns depends on them: + +- § *ADR structure* — the skeleton, and what belongs under each heading +- § *ADR readability* — general to specific, behaviour before structure, lead with the orienting + artefact, concentrate `file:line` citations, name what is unchanged +- § *Diagrams in ADRs* — which diagram form to use, when a decision-ladder table beats a + flowchart, and the mermaid traps that pass review and then fail to render +- § *Writing tone for design documents* — write for a future reader, not for this conversation + +The template above is that skeleton as a fill-in form. If the two ever disagree, +`documentation.md` is authoritative and the template is what needs fixing. + +#### Grounding requirements (include in the sub-agent prompt) + +The ADR must be grounded in the real codebase — an ADR that references files, classes, or +namespaces that don't exist misleads everyone who reads it. Therefore: +- USE Glob and Grep to verify every file path, class name, type name, and namespace the ADR + references actually exists. Do not guess from a name. +- Reference real existing patterns in the codebase; if you propose a new pattern, justify + why the existing ones don't fit. +- Specify interface contracts with inputs, outputs, and error conditions. +- Document consequences honestly (both positive AND negative). A design with only positive + consequences is suspicious. +- List genuine alternatives with real rationale for rejection (not strawmen). + +### Step 7: Validate, Write the ADR, Stamp Frontmatter, and Update Tracking + +After the sub-agent returns: + +1. **Validate** the returned ADR before writing: + - It contains all template sections (Status, Context, Decision, Consequences with both + Positive AND Negative, Alternatives Considered, References). + - The header number/date and `Parent Requirement` / `Related ADRs` links are correct. + - The sub-agent returned a `SUMMARY:` and 1–4 `TAGS:` from the taxonomy. If missing or a tag is + outside the taxonomy, fix it (or ask the sub-agent to revise) before stamping frontmatter. + - **Re-check the ADR number** (closes the concurrent-run race noted in Step 3): re-run + `ls docs/adr/` and confirm `{NNNN}-{focus-area}.md` does not already exist. If it does, + increment to the next free number and update the ADR header before writing. + - Spot-check at least one codebase reference the sub-agent cited (Glob/Grep) — if a + referenced file/class doesn't exist, send it back to the sub-agent to fix rather than + writing a misleading ADR. + - **The headings match the template** — same wording, same order, same nesting level. Drift + here is what makes a corpus unnavigable, and it is free to fix before the file is written. + - If anything is missing or wrong, ask the sub-agent to revise, or fix it yourself before writing. + +2. **Write** the validated ADR body to `docs/adr/{NNNN}-{focus-area}.md` using the Write tool. + +3. **Run the mechanical checks** — specified in + [`.agent_instructions/documentation.md`](../../../.agent_instructions/documentation.md) + § *Before an ADR is committed*. Both, every time; they catch defects that survive a careful + read. Roughly one diagram in six fails to render on first draft, and a diagram that does not + render is a broken ADR that looked fine in review. + + ```bash + npx -y -p @mermaid-js/mermaid-cli@11 mmdc -i diagram.mmd -o diagram.svg + grep -c '<\|>\|&' docs/adr/{NNNN}-{focus-area}.md # must be 0 + ``` + + Render the most complex diagram to PNG (`-o diagram.png -w 1600 -b white`) and actually *look* + at it with Read — a diagram can parse cleanly and still be unreadable, which is the signal to + convert it to a decision-ladder table. If the render check cannot run (no network for `npx`), + say so plainly rather than reporting the diagrams as verified. + +4. **Stamp frontmatter** with the `write_adr_metadata` skill + (`.claude/commands/adr/write_adr_metadata.md`), passing the sub-agent's proposed summary and tags: ``` write_adr_metadata docs/adr/{NNNN}-{focus-area}.md init --summary "{summary}" --tags "{tags}" ``` This derives `id`/`title`/`created` from the file, sets `status: Proposed`, and keeps the - frontmatter in sync with the body `## Status`. Confirm the checks the skill reports pass. + frontmatter in sync with the body `## Status`. Confirm the six checks the skill reports pass + (`id` == filename stem, `status` ∈ enum, tags ⊆ taxonomy, valid date, all seven keys, frontmatter + status matches body). - If this ADR **supersedes** a prior ADR you identified in Step 3a, also mark that older ADR: + If this ADR **supersedes** a prior ADR you identified in Step 4, also mark that older ADR on disk: ``` write_adr_metadata docs/adr/{old-id}.md supersede --by {NNNN}-{focus-area} ``` -2. **Regenerate the ADR index** so `docs/adr/index.md` reflects the new ADR (and any supersession): +5. **Regenerate the ADR index** so `docs/adr/index.md` reflects the new ADR (and any supersession). + This is the single canonical command (documented in + [`.agent_instructions/adr_frontmatter.md`](../../../.agent_instructions/adr_frontmatter.md)): ```bash awk -f .claude/commands/adr/generate_adr_index.awk docs/adr/[0-9]*.md > docs/adr/index.md @@ -174,40 +395,31 @@ After writing the ADR body: `docs/adr/index.md` is a regenerable cache — never hand-edit it; always regenerate from frontmatter. -3. **Recommend `/spec:write_release_notes` when this ADR breaks something.** If the ADR you just - wrote or amended records, in its `## Consequences`, a change that breaks an existing behaviour - or interface, tell the user so and recommend running `/spec:write_release_notes` for this spec. - Do **not** run that command yourself — recommend it, and let the user choose when to run it. - -### Step 5: Additional Guidance - -When creating the ADR: -- Focus on **one specific architectural decision** - keep it focused -- Focus on the **why** of decisions, not just the **what** -- Use ASCII art or mermaid diagrams where helpful for architecture +6. **Update tracking**: `echo "{NNNN}-{focus-area}.md" >> specs/{current-spec}/.adr-list` -### Step 6: Design Principles +7. **Back-fill the sibling map.** If `.adr-list` now holds more than one ADR, every *earlier* + ADR's `### Where this ADR sits` table is stale — it does not list the one you just wrote. + Add the new row to each, keeping each file's own row bolded and marked *(this one)*. Where + an earlier ADR was written before this section existed, add the whole section. -Review against the design principles + This costs one table row per file and it is the step that decides whether the map is worth + anything: a map that is right for the newest ADR and wrong for all the others is worse than + no map, because a reader who checks it once and finds it stale will not check it again. -- Read ".agent_instructions/design_principles.md" -- Review the ADR using the design guidelines from "Use Responsibility-Driven Design" -- Check for a design focused on behavior: does the ADR think about *roles* and *responsibilities*. -- Responsibilities are "knowing", "doing", and "deciding" -- Allocate responbilities into roles, focusing on cohesion. -- Roles are interfaces or abstract types -- A class can implement one or more roles. If it implements multiple roles, they should be related. -- Provide feedback and suggest improvements +8. **Recommend `/spec:write_release_notes` when this ADR breaks something.** If the ADR you just + wrote or amended records, in its `## Consequences`, a change that breaks an existing behaviour + or interface, tell the user so and recommend running `/spec:write_release_notes` for this spec. + Do **not** run that command yourself — recommend it, and let the user choose when to run it. -### Step 7: Next Steps +### Step 8: Next Steps 1. Remind user to: - Review and complete the ADR with technical details - - Commit the ADR and the regenerated index: `git add docs/adr/{NNNN}-{focus-area}.md docs/adr/index.md && git commit -m "docs: add ADR for {focus-area}"` + - Commit the ADR, the regenerated index, and any sibling ADRs whose map you back-filled in + Step 7.7: `git add docs/adr/{NNNN}-{focus-area}.md docs/adr/index.md {back-filled siblings} && git commit -m "docs: add ADR for {focus-area}"` - The first ADR should typically be the first commit on the feature branch + - Can create/update draft PR with ADR for early feedback 2. Multiple ADRs: - If requirement needs more architectural decisions, run `/spec:design [another-focus-area]` again - Each ADR should address a distinct architectural concern 3. When all ADRs are complete: `/spec:approve design` to proceed to tasks phase - -Use the Write tool to create the ADR document in `docs/adr/`. diff --git a/.claude/commands/spec/review.md b/.claude/commands/spec/review.md index 86512899..3a955788 100644 --- a/.claude/commands/spec/review.md +++ b/.claude/commands/spec/review.md @@ -1,5 +1,5 @@ --- -allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(grep:*), Read, Write, Glob, Grep, Agent +allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(grep:*), Bash(mkdir:*), Bash(npx:*), Bash(python3:*), Read, Write, Glob, Grep, Agent description: Review current specification phase argument-hint: [requirements|design [adr-number]|tasks] [threshold] --- @@ -49,6 +49,13 @@ Read ALL documents the sub-agent will need. The sub-agent gets a clean context - Read `specs/{current-spec}/.adr-list` to find ADRs - Read each ADR from `docs/adr/{filename}` (or just the specified one) - Read `specs/{current-spec}/requirements.md` (for cross-referencing) +- Pass the path to `.agent_instructions/documentation.md` — § *Architecture Decision Records* is + the canonical skeleton, readability rules, diagram rules and pre-commit checks that the + *Structure and Readability* criteria in Step 4 grade against, and § *Writing tone for design + documents* is the tone contract. The sub-agent reads it itself. +- **Even when reviewing a single ADR, read the whole `.adr-list` set.** Two of the design criteria + — the sibling map and the unifying sentence — are properties of the set, not of one file, and + cannot be judged from one ADR alone. **For tasks review:** - Read `specs/{current-spec}/tasks.md` @@ -59,7 +66,7 @@ Read ALL documents the sub-agent will need. The sub-agent gets a clean context Launch an Agent (subagent_type: "general-purpose") with the prompt below. -**Sub-agent tool access**: The sub-agent (general-purpose) inherits tool access. For design and tasks reviews it SHOULD use Read, Glob, and Grep to read documents and verify codebase references. The sub-agent should NOT write the findings file — it should return the findings as text. The main agent writes the file after validating the output. +**Sub-agent tool access**: The sub-agent (general-purpose) inherits tool access. For design and tasks reviews it SHOULD use Read, Glob, and Grep to read documents and verify codebase references. For **design** reviews it SHOULD additionally use `Bash` to run the diagram render check and the escaped-markdown grep in the *Diagrams* criteria — those are verifications, not judgements, and a review that skips them has not checked the one class of defect that is invisible to a careful read. The sub-agent should NOT write the findings file — it should return the findings as text. The main agent writes the file after validating the output. **IMPORTANT**: The sub-agent prompt must include: 1. The review criteria for the relevant phase (from Step 4 below) @@ -129,6 +136,62 @@ You are a skeptical reviewer. Assume the design has problems — your job is to - Is the error handling strategy explicit? - Are concurrency/threading concerns addressed where relevant? +**Structure and Readability:** + +An ADR that is correct and unreadable has failed. Its primary audience is a human reviewer six +months from now reading for the *why* of a behaviour — behaviour endures, structure changes under +refactoring. Grade against `.agent_instructions/documentation.md` § *ADR structure* and +§ *ADR readability*: + +- **Headings.** Do they match the canonical skeleton — same wording, same order, same nesting + level as the sibling ADRs? Heading drift is what makes a corpus unnavigable, and an ADR that + invents its own section names or nests them one level off its siblings is a finding even when + every word of its content is right. +- **Behaviour before structure.** Does the Decision open with `### The mechanism, end to end` + (what happens, in what order) before `### Where the pieces live` (which assemblies) before the + signatures? An ADR whose Decision opens with an interface declaration has it backwards. +- **The opening.** Does Context open in plain language a user would recognise, or does it name + four interfaces before the reader knows what the problem is? +- **Orientation first.** Does each section lead with the artefact that orients — a table, a + diagram, a contract — and then read the consequences off it? Or does it make the reader assemble + the picture from paragraphs and only then show them the diagram? +- **The roles table.** Is there a `#### The roles, and what each is responsible for` table with + **knowing** / **doing** / **deciding** stereotypes? If it is absent, or its rows are types rather + than roles, Responsibility-Driven Design was applied to the review and not to the design. +- **Citation density.** Are `file:line` references concentrated in `Implementation Approach` and + the `Where each type is touched` table? Citations are load-bearing for an implementor and pure + noise inside an argument — a forces bullet carrying three of them is a finding. +- **What is unchanged.** Does `Where each type is touched` close by naming the types deliberately + *not* touched, so a reviewer does not read an omission as an oversight? +- **One decision.** Can the ADR state its unifying rule in one sentence? If that takes a paragraph, + suspect it is more than one decision and say so. + +**Diagrams — verify these actively, do not eyeball them:** + +- **Render every mermaid block.** Extract each to its own `.mmd` file under the scratchpad and run + `npx -y -p @mermaid-js/mermaid-cli@11 mmdc -i diagram.mmd -o diagram.svg`. A non-zero exit or a + missing `.svg` is a **defect in the ADR** — report it at High or above. Roughly one diagram in + six fails on first draft, and a broken diagram looks perfectly fine in a read-through. If the + render cannot run at all (no network for `npx`), say so in the findings rather than reporting the + diagrams as verified. +- The three traps that produce a clean-looking, non-rendering diagram: **`;` is a statement + separator in `sequenceDiagram`**; **`<` or `>` in a label** is eaten as HTML, so `Lease<T>` loses + its type parameter; **HTML entities** (`<`, `>`, `&`) break the escaped-markdown check. +- **Escaped markdown**: `grep -c '<\|>\|&' docs/adr/{file}.md` must be `0`. +- **Is any flowchart carrying more than about four decision points?** That renders as an unreadable + column with edges spanning its whole height. It should be a decision-ladder table — one row per + situation, in evaluation order. A diagram can parse cleanly and still be unreadable, so render + the most complex one to PNG (`-o diagram.png -w 1600 -b white`) and look at it. + +**Writing tone** (§ *Writing tone for design documents*): + +- Does the ADR reference **participants in the authoring conversation** — "at the user's + direction", "the user explicitly chose", "per the user's feedback" — rather than durable + artefacts like requirement ids, other ADRs or code locations? This defect has survived into + Accepted ADRs before, so check for it rather than assuming it away. +- Does it reference **ephemeral state** — `PROMPT.md`, the current spec phase, unresolved review + back-and-forth? Either the substance is folded in or the sentence goes. + **Frontmatter:** - Does each ADR carry the YAML frontmatter defined in `.agent_instructions/adr_frontmatter.md` (`id` == filename stem, `status` matching the body `## Status`, a `summary` that states what was @@ -141,6 +204,17 @@ You are a skeptical reviewer. Assume the design has problems — your job is to `<!-- spec: {this spec's directory name} -->`)? A breaking change with no such marked section is a finding — recommend running `/spec:write_release_notes`. +**Cross-Reference (when reviewing all ADRs, or one ADR within a set):** +- Do the ADRs collectively cover all the requirements? +- Are there gaps — requirements that no ADR addresses? +- Are there contradictions between ADRs? +- **Does every ADR in the set carry `### Where this ADR sits`, and is every table complete?** ADRs + are written one at a time, so an earlier ADR's map goes stale the moment the next one lands — + check each table lists *every* ADR in the set, with that file's own row bolded and marked + *(this one)*. A map that is right for the newest ADR and stale for the rest is worse than no map. +- **Is the unifying sentence stated identically across the siblings that apply it**, or has it been + paraphrased into three not-quite-equivalent versions? + --- #### Tasks Review Criteria @@ -221,6 +295,16 @@ Tell the sub-agent to produce output in this exact format: - **50-69 (Medium)**: Worth addressing. Vagueness, missing examples. - **0-49 (Low)**: Suggestion or nit. +**Calibration for design reviews:** +- **A mermaid diagram that does not render IS High (70-85)** — it is a broken artefact that reads as perfectly fine, so nothing else will catch it before it ships. +- **An ADR that references a participant in the authoring conversation** ("at the user's direction", "the user explicitly chose") **IS Medium-High (65-75)** — it fails the document's only audience, and once the ADR is Accepted the sentence tends to stay. +- **A stale `### Where this ADR sits` map** — one that omits a sibling added after it was written — **IS Medium-High (60-75)**. A reader who finds a map stale once stops trusting every other map in the corpus. + +Calibrating the structure criteria specifically — they are about navigability, not conformity: +- Heading **order or nesting level** drifting from the siblings is Medium-High (60-75): it breaks the reader's ability to navigate the next ADR using what they learned on this one. +- Heading **wording** differing slightly while order and nesting are right is Low-Medium (35-55). +- **Do not demand an artefact where there is nothing to put in it.** An ADR that genuinely introduces no new roles — a pure ordering, timing or naming decision — needs no roles table, and a missing one there is Low or no finding at all. The same goes for a diagram on an ADR whose decision has no sequence and no layering. Report the *absence of orientation*, not the absence of a specific heading. + **Verdict logic**: If ANY finding scores >= threshold -> NEEDS WORK. Otherwise -> PASS. ### Step 6: Validate, Write Findings, and Present Summary diff --git a/CLAUDE.md b/CLAUDE.md index 864581de..d6db7dc1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -225,6 +225,6 @@ For comprehensive guidance on working with this codebase, Claude should read the - [Code Style](.agent_instructions/code_style.md) - C# conventions and architectural patterns - [Design Principles](.agent_instructions/design_principles.md) - Responsibility-Driven Design and architectural guidance - [Testing](.agent_instructions/testing.md) - TDD practices, test structure, and testing guidelines -- [Documentation](.agent_instructions/documentation.md) - XML documentation standards and licensing requirements +- [Documentation](.agent_instructions/documentation.md) - XML documentation standards, the ADR structure/readability/diagram contract, writing tone for design documents, and licensing requirements - [ADR Frontmatter](.agent_instructions/adr_frontmatter.md) - ADR YAML frontmatter schema, status vocabulary, tag taxonomy, and the generated ADR index - [Dependency Management](.agent_instructions/dependency_management.md) - Package management with Directory.Packages.props From 5ce58b5d5ae63fde9496aafc4060c8dd893b3d1c Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 10:34:07 +0100 Subject: [PATCH 06/12] =?UTF-8?q?feat(skills):=20/spec:review=20=E2=80=94?= =?UTF-8?q?=20work=20findings=20by=20issue,=20not=20by=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port Brighter's Step 8: when review findings are worked, name the concept each finding is about, find and edit every statement of it together, re-sweep the old phrasings, and run a narrow opus contradiction check before committing and re-reviewing. Fixing only the quoted sentence leaves the concept stated two ways, which the next pass finds as a higher-scored contradiction. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .claude/commands/spec/review.md | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/.claude/commands/spec/review.md b/.claude/commands/spec/review.md index 3a955788..3aaf25b5 100644 --- a/.claude/commands/spec/review.md +++ b/.claude/commands/spec/review.md @@ -313,7 +313,8 @@ After the sub-agent returns: 1. **Validate the output** before writing 2. **Write the findings file** to `specs/{current-spec}/review-{phase}.md` -3. **Present a summary to the user** +3. **Present a summary to the user**, and remind them to use `/spec:approve {phase}` when ready, or + to work the findings (Step 8) and re-run `/spec:review {phase}`. ### Step 7: Spec Status @@ -322,3 +323,42 @@ Display overall spec status: - Requirements: Approved / In Progress - Design: {X} ADRs ({Y} approved, {Z} proposed) - Tasks: Approved / In Progress / Not Started + +### Step 8: Working the Findings — Fix Issues, Not Lines + +This step is not part of the review. It applies whenever the findings of a `requirements`, `design` +or `tasks` review are worked, in this session or a later one, and it runs only when the user asks +for them to be addressed. + +**Why it exists.** A finding quotes one sentence, but the concept that sentence states is usually +stated in several places: a definition, an FR, an NFR, a constraint, Out of Scope, one or more ACs, +a worked example, and, for a design, sibling ADRs. Editing only the quoted sentence leaves the other +places saying the old thing, so the fix becomes a new contradiction. The next pass then finds it, +often scored *higher* than the original finding, because a contradiction outranks a vagueness. +Fixing lines instead of issues is how a review loop stops converging. + +1. **Name the issue, not the sentence.** For each finding being worked, write down in a few words + the concept it is about: "who may add a release-notes marker", not "line 121". Findings that + turn out to be about the same concept are worked together. +2. **Find every statement of the concept.** Grep the whole document with several search terms, not + only the words the finding quoted. For a design, grep every ADR in `.adr-list` and + `requirements.md` too. Check Definitions, FRs, NFRs, Constraints, Out of Scope, the ACs and any + worked examples. List the sites before editing any of them. +3. **Edit every site together**, so the concept is stated one way everywhere. When a fix needs a + choice, prefer the option that does not contradict a rule stated elsewhere. If every option + does, or the choice is the owner's, ask the user before editing. +4. **Re-sweep the old phrasings.** Grep for the wording you replaced and read every hit. Fix any + survivor that is now false. +5. **Run a quick contradiction check before committing.** Launch a sub-agent + (`general-purpose`, `model: "opus"`, forbidden to ask the user anything or to write files). Give + it the diff of the edit, the findings file and the list of concepts touched. Its job is narrow + and is not a re-review of the document. For each concept it: + - greps every statement of the concept; + - reports any sentence, edited or untouched, that now disagrees with another; + - confirms which findings are resolved; + - flags new wording that two implementers would read differently. + + Fix anything it scores **≥ 50** before committing, repeating steps 2–4 for that concept. You may + fix lower items in the same edit when they sit in a concept already touched. +6. **Commit, then re-run `/spec:review {phase}`** as the next pass. The commit message names the + issues worked, not only the finding numbers. From 8b0d4d01e7393637d6bb9cc835933270936f65bf Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 11:12:16 +0100 Subject: [PATCH 07/12] docs: add AGENTIC_CODING.md, a guide to contributing with a coding agent Adapted from Brighter's guide: what we provide for agents, choosing a workflow, the spec workflow and driving in review gears (step vs loop drivers, /spec:gear), the bugfix workflow, keeping working state across sessions in PROMPT.md, and reviewing agent-authored PRs. Omits pieces Darker does not have (AGENTS.md, Copilot instructions, SlopWatch, /refactor:smells, /spec:show-me). Linked from README.md and CLAUDE.md. Gitignore PROMPT-*.md companions and the RALPH_STOP kill-switch, both of which the guide describes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .gitignore | 6 +- AGENTIC_CODING.md | 290 ++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 2 + README.md | 4 + 4 files changed, 301 insertions(+), 1 deletion(-) create mode 100644 AGENTIC_CODING.md diff --git a/.gitignore b/.gitignore index f53ce192..9c7f402e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. -# Claude Code session prompt +# Claude Code session prompt (see AGENTIC_CODING.md) PROMPT.md +PROMPT-*.md + +# /spec:ralph-implement kill-switch +RALPH_STOP # TDD review gear — untracked working state, not a spec artifact. # Holds which gear a spec is currently being run in (review-before | review-after). diff --git a/AGENTIC_CODING.md b/AGENTIC_CODING.md new file mode 100644 index 00000000..6f937d9b --- /dev/null +++ b/AGENTIC_CODING.md @@ -0,0 +1,290 @@ +# Agentic Coding with Darker + +This guide explains how to use a coding agent to contribute to Darker. We have focused on [Claude Code](https://claude.com/claude-code), but most of what is here (our instructions, our workflow, the reasoning behind our gears) applies to other agents too. + +We welcome code authored with an agent. **You remain responsible for the code you submit.** Read it, understand it, and make sure it follows our contribution guidelines, which [`.agent_instructions/`](.agent_instructions/) restates for agents. Darker shares its workflow with [Brighter](https://github.com/BrighterCommand/Brighter), and the tooling described here is kept in step with Brighter's. The workflows below exist to make that practical. + +## Table of Contents + +- [What We Provide](#what-we-provide) +- [Choosing a Workflow](#choosing-a-workflow) +- [The Spec Workflow](#the-spec-workflow) + - [Design: Requirements, ADRs and Tasks](#design-requirements-adrs-and-tasks) + - [Implementation: Driving in Gears](#implementation-driving-in-gears) + - [Choosing a Driver: Step or Loop](#choosing-a-driver-step-or-loop) + - [Shifting Gear with /spec:gear](#shifting-gear-with-specgear) + - [Worked Example](#worked-example) +- [The Bugfix Workflow](#the-bugfix-workflow) +- [Working Across Sessions: PROMPT.md](#working-across-sessions-promptmd) +- [Smaller Changes: /test-first and /tidy-first](#smaller-changes-test-first-and-tidy-first) +- [Reviewing Agent-Authored Pull Requests](#reviewing-agent-authored-pull-requests) + +--- + +## What We Provide + +| Location | Purpose | +| --- | --- | +| `CLAUDE.md` | Entry point for Claude Code. Points at `.agent_instructions/` and sets the non-negotiables, such as the TDD approval gate. | +| `.agent_instructions/` | An agent-oriented version of our contribution guidelines: build, code style, design principles, testing, documentation (including the ADR contract), ADR frontmatter, and dependency management. | +| `.claude/commands/` | Slash commands that drive our preferred workflows: `/spec:*`, `/bugfix:*`, `/test-first`, `/tidy-first` and `/adr`. See the [commands README](.claude/commands/README.md). | +| `docs/adr/index.md` | A generated index of every ADR, built from each ADR's YAML frontmatter. Agents use the frontmatter (via `read_adr_metadata`) to find prior decisions without reading every ADR. | +| `PROMPT.md` | Not provided: you create it. A gitignored file of session state that lets work continue across agent sessions. See [Working Across Sessions](#working-across-sessions-promptmd). | + +Because our instructions live in plain Markdown under `.agent_instructions/`, any agent can use them. + +## Choosing a Workflow + +| You are... | Use | Why | +| --- | --- | --- | +| Building a feature or new capability | `/spec:*` | Requirements, ADR design, adversarial review, and a reviewed task list before any code. | +| Fixing a bug whose cause is not yet proven, or that arrived with a suggested fix | `/bugfix:*` | Proves the root cause before any fix is written. | +| Making a small change whose behavior is already obvious | `/test-first` | Test-first discipline without the ceremony. | +| Restructuring code | `/tidy-first` | Keeps structural and behavioral changes in separate commits. | + +--- + +## The Spec Workflow + +The `/spec` commands implement Darker's contribution workflow: **Issue → Requirements → ADR(s) → Tasks → Tests → Code**. Every phase ends with your explicit approval; the agent does not move on until you approve. The full command reference is in the [spec README](.claude/commands/spec/README.md). + +``` + GitHub Issue + │ + ▼ + requirements.md ───► /spec:requirements [issue] → /spec:review → /spec:approve requirements + │ + ▼ + docs/adr/NNNN-*.md ► /spec:design [focus-area] → /spec:review → /spec:approve design + │ (one ADR per architectural decision; repeat as needed) + ▼ + tasks.md ──────────► /spec:tasks → /spec:review → /spec:approve tasks + │ + ▼ + Implement ─────────► /spec:implement one task at a time, in the main session + │ /spec:ralph-implement an unattended loop over the same tasks.md + │ ▲ + │ └── /spec:gear shifts the review gear at any point + ▼ + Pull Request +``` + +### Design: Requirements, ADRs and Tasks + +| Command | What it does | +| --- | --- | +| `/spec:new <name>` | Creates `specs/NNN-name/` and makes it the current spec. | +| `/spec:requirements [issue]` | Drafts `requirements.md`, from a GitHub issue if one is given. Requirements say *what* users need, not *how*. | +| `/spec:design [focus-area]` | Drafts an ADR in `docs/adr/`, after looking up related ADRs from their frontmatter. Write one ADR per architectural decision; a feature often needs several. ADRs follow the shared skeleton and readability rules in [documentation.md](.agent_instructions/documentation.md). | +| `/spec:review [phase]` | Runs an **adversarial review** of the current phase in a separate agent. Run it more than once: fix the findings, then review again. When you fix findings, fix the *issue* everywhere it is stated, not just the quoted line. | +| `/spec:approve <phase>` | Records your approval and unlocks the next phase. Approving `tasks` freezes the *content* of `tasks.md`; ticking checkboxes is still allowed. | +| `/spec:tasks` | Breaks the approved design into a test-first task list. | +| `/spec:write_release_notes` | Writes the spec's section of `release_notes.md` when the design breaks an existing behaviour or interface. `/spec:design` tells you when to run it. | +| `/spec:status`, `/spec:switch` | Show every spec and its phase (including its current gear), or change the active spec. | + +The design phases are where you and the agent build a shared *theory* of the change. Reviewing and approving them yourself, rather than waving them through, makes the implementation phase cheap to review. + +### Implementation: Driving in Gears + +How much should the agent do before you look at its work? One test? One task? The whole feature? + +Our answer comes from [Coding Agents: Driving In Gears](https://ian-cooper.writeas.com/coding-agents-and-driving-in-gears). In brief: + +- **The code is where the theory lives.** Natural language is how we talk to the agent about the model; the code *is* the model. Reading it lets us check that the agent shares our theory, gives us fresh insights, and prevents **cognitive debt**: a team that cannot whiteboard how its own code works. +- **What is scarce is human understanding, not approvals.** An agent writes code far faster than we can build a theory of it, and it copies whatever idioms it finds, good or bad. Without oversight it amplifies poor quality as quickly as good. +- **"Human in the loop or not" is the wrong question.** The right one is *how certain am I about this theory right now?* The less certain you are, the smaller the step you take before you review. The more certain, the larger. +- **Blast radius is the second dial.** Certainty is how likely you are to be wrong; blast radius is how much being wrong would cost. A boring change to something with a high cost of failure, such as the query pipeline or a public API, still calls for a lower gear. + +Kent Beck's *driving in gears* metaphor from *TDD By Example* turns this into practice: + +| | Low gear | Medium gear | High gear | +| --- | --- | --- | --- | +| **You are…** | Unsure of the theory. The problem, technology or domain is new to you. You want the agent to be a researcher, not a delegate. | Confident you'll find a workable theory quickly because you've solved something similar. You want to explore the solution with the agent. | Holding the theory, and bored by the details. The work is well-trodden boilerplate. | +| **Design** | Several rounds of dialogue. Use the agent to research options and spike unfamiliar tools before you agree a design. | A couple of rounds of back-and-forth on behaviors, responsibilities and key abstractions. | Agree the design, have it adversarially reviewed, and skim it for surprises. | +| **You review…** | Every test, *before* it is implemented, then each implementation as it goes green. You guide the refactoring. | After each task, or every two or three similar tasks. You look for code smells and insights to feed back into the design. | The finished pull request, relying on adversarial agent reviews along the way. | +| **Gear setting** | `review-before` (the default) | `review-after` | `review-after` | +| **Driver** | `/spec:implement` | `/spec:ralph-implement 1`, rising to `2` as the design settles (or `/spec:implement`) | `/spec:ralph-implement 3`, or more, up to all remaining tasks | + +#### When to shift + +You will shift within a single spec, sometimes within an afternoon. + +- **Shift up on boredom and confidence.** The design has stopped moving, reviews no longer surface fresh insights, and the agent has picked up the code style from the work it has already done. Boredom is an honest signal that you hold the theory and what remains is labor. +- **Shift down on opacity and reverts.** You don't recognize the code the agent just wrote, you keep reverting tasks, you keep correcting the same kind of mistake, or the tests assert things you would not have approved. +- **Impatience is not a signal.** The economics always push toward high gear because it is faster and cheaper. Upshift because you have earned it, not because you want to be done. + +Spend extra effort in the first few tasks of any spec. The agent learns how this code should be written from the code it has already written, so early review pays off for the rest of the run. + +### Choosing a Driver: Step or Loop + +There is one task list, `tasks.md`, and two ways to run it. Switching between them costs nothing; each picks up from the next unticked task. + +| | `/spec:implement` | `/spec:ralph-implement` | +| --- | --- | --- | +| **Shape** | One task at a time, in your session. | A self-driving loop that runs tasks in sequence until a bound is reached. | +| **Gear** | Honours whatever gear is set, task by task. | Always `review-after`. It sets the gear when it starts, asking you for a reason. | +| **Model** | Run the session on Sonnet; the command will prompt you to switch. | Run the orchestrator on Opus with **auto mode** enabled so it is not stopped by permission prompts. Each task runs in a fresh Sonnet sub-agent. | +| **Use when** | Low gear, or medium gear when you want to stay at the keyboard. | Medium and high gear. | + +**Setting the loop's bound.** `/spec:ralph-implement` asks how far to go before it stops and hands back to you: + +- **Tasks:** stop after *N* tasks complete. `/spec:ralph-implement 3` sets this directly. This is the gear lever in practice: `1` for medium gear, `2` as the design settles, `3` or more for high gear. +- **Turns:** stop after *N* attempts, successful or not, which caps effort. +- **Budget:** stop after roughly *N* output tokens. + +**Context stays small.** Each task's red-green-refactor cycle runs in its own sub-agent, so only the orchestrator builds up context across the run. If the orchestrator's context grows too large, stop it and restart; it resumes from the first unfinished task. The loop is deliberately sequential because it assumes you, the reviewer, are the bottleneck. + +**Stopping the loop.** The loop always finishes the task in flight, and nothing already completed is undone. + +- `/spec:gear review-before` (from another terminal) stops it cleanly so you can carry on with `/spec:implement` under the restored gate. +- `touch RALPH_STOP` at the repository root is the kill switch. +- It also stops when the bound is reached, when the gear's scope is exhausted, or when no tasks remain. A task that fails is marked `- [!]` and skipped for you to look at. + +### Shifting Gear with /spec:gear + +There are two gear settings. The low/medium/high distinction above comes from combining a setting with a driver and a bound. + +| Setting | Approval gate | Meaning | +| --- | --- | --- | +| `review-before` | ✅ armed | You approve each test in your IDE before the agent implements it. **The default.** | +| `review-after` | ➖ not armed | The agent proves each test fails, implements it, and you review the batch afterwards. | + +```bash +/spec:gear # which gear am I in, and why? + +/spec:gear review-after "Phase 5 — Provider rejection tests" \ + --because "Phase 4's six tests were all approved unchanged" + +/spec:gear review-before --because "the last two tests asserted the wrong thing" +``` + +- **A reason is required.** A gear change is a judgement about certainty; the reason records it. +- **Scope an upshift where you can.** Name a heading in `tasks.md` (or a range such as `tasks 6-11`) and the gear applies only there; tasks outside it, and the tasks after it, fall back to `review-before`. A scoped upshift expires on its own. This is why task phases with clear, stable names are worth having. +- **The gear is working state, not a decision record.** It lives in `specs/{spec}/.current-gear`, which is gitignored, and it applies only to that spec. `/spec:gear` adds a pointer to [`PROMPT.md`](#working-across-sessions-promptmd) so a fresh session can find it. If the file is missing, can't be read, or names a scope that doesn't match, the gear is `review-before`; it fails safe. +- **`/test-first` and `/bugfix:test` are always gated.** Only the spec drivers read the gear. + +**What `review-after` does *not* remove.** It removes the human pause and nothing else. In either gear the agent must: + +- write the test and watch it **fail for the right reason** before writing any production code; +- run the **full regression suite**, not just the new test; +- make **two commits** per task: the change (`feat:`, `test:`, `fix:` or `refactor:`), then a separate `docs:` commit ticking the task in `tasks.md`; +- follow every test-authoring convention in [testing.md](.agent_instructions/testing.md). + +Even in `review-after`, the agent stops and asks if a test asserts something the task did not ask for, needs a design decision the ADRs don't cover, or duplicates an existing test. The reasoning behind this mechanism is in Brighter's [ADR 0071](https://github.com/BrighterCommand/Brighter/blob/master/docs/adr/0071-tdd-review-gear.md); the full reference is [gear.md](.claude/commands/spec/gear.md). + +### Worked Example + +```bash +# Design: build the theory with the agent, reviewing each phase +/spec:requirements 123 +/spec:review requirements +/spec:approve requirements +/spec:design query-caching +/spec:review design +/spec:approve design +/spec:tasks +/spec:review tasks +/spec:approve tasks + +# Low gear: the early tasks set the shape and the style, so review every test +/spec:implement + +# Medium gear: the shape has settled; review each task as it lands +/spec:gear review-after "Phase 2 — Cache key generation" --because "the test shape is settled" +/model opus +/spec:ralph-implement 1 + +# High gear: the remaining tasks are boilerplate; review the batch +/spec:gear review-after "Phase 3 — DI registrations" --because "repetitive and low blast radius" +/spec:ralph-implement 5 + +# Something looks wrong: downshift, and carry on one test at a time +/spec:gear review-before --because "the last test asserted the wrong thing" +/model sonnet +/spec:implement +``` + +If the design breaks an existing behaviour or interface, `/spec:write_release_notes` drafts the spec's section of `release_notes.md`. + +--- + +## The Bugfix Workflow + +Bugs differ from features in one important way: **the root cause is a hypothesis until it is proven.** Issues often arrive with a suggested fix, sometimes from another agent, that is wrong, incomplete, or treats a symptom. The `/bugfix` commands are a lighter workflow than `/spec`, with no requirements or ADR phases, built around a gate that proves the diagnosis first. The full reference is in the [bugfix README](.claude/commands/bugfix/README.md). + +``` + Issue ──► /bugfix:triage [issue] restate the symptom, locate the code, form a hypothesis + │ (any suggested fix is recorded as UNVERIFIED) + ▼ + /bugfix:confirm ✋ prove or refute the hypothesis before any fix + │ (refuted → back to triage) + ▼ + /bugfix:test ✋ write the failing regression test (via /test-first) + ▼ + /bugfix:fix the minimal change to go green, scoped to the confirmed cause + ▼ + /bugfix:verify run the suite; prepare a fix: commit and a "Fixes #N" PR +``` + +- **Confirm is the point of the workflow.** It accepts either a failing reproduction (preferred, when the bug reproduces cheaply) or a code trace with `file:line` evidence (when a reproduction would need a real database or external service). Confirming the cause often changes the scope of the fix, for example by revealing a second defect the suggested fix missed. +- **The regression test is always gated.** `/bugfix:test` delegates to `/test-first` and always waits for you to approve the test. Bugfixes do not use gears. +- **Fixes stay minimal.** If the fix needs restructuring first, `/bugfix:fix` hands that to `/tidy-first`, giving a separate `refactor:` commit before the `fix:` commit. +- **State lives in `bugfixes/NNNN-slug/bugfix.md`**, filled in as you go: symptom, hypothesis, confirmed cause, evidence, test and fix. `/bugfix:status` lists open bugs; `/bugfix:switch` changes the active one. + +--- + +## Working Across Sessions: PROMPT.md + +A spec or a bugfix rarely fits in one agent session. The context window fills up, compaction drops detail, you stop for the day, or you `/clear` to give the agent a clean start on the next phase. The repository already holds the durable state: `requirements.md`, the ADRs, the ticks in `tasks.md`, and `bugfix.md`. It does not hold the *working* state: what you were in the middle of, what you agreed in conversation but have not yet written down, and what the agent should leave alone. We keep that in `PROMPT.md`. + +**What it is.** `PROMPT.md` is a Markdown file at the root of your checkout, or of your worktree if you use one. It is gitignored, along with any `PROMPT-*.md` companions, so it is never committed or reviewed, and each worktree has its own. It belongs to you. `CLAUDE.md` tells the agent to keep cross-session state here rather than in its private memory, where you cannot see or edit it. + +**The loop.** + +``` + work ──► "update PROMPT.md" ──► /clear, compaction, or tomorrow ──► "read PROMPT.md and continue" + ▲ │ + └──────────────────────────────────────────────────────────────────────────────┘ +``` + +- **Update it at natural breaks**: a phase approved, a review round finished, a PR opened, or just before you `/clear`. Ask the agent to do it; it knows what it has been doing. +- **Start the next session with "read PROMPT.md and continue".** The agent should tell you where it thinks it is before it does anything. Check that it matches your understanding. +- **Rewrite it; don't append to it.** It is a snapshot of where the work stands now, not a log. When the work ships, cut that work down to a line or two. + +**What goes in it.** Keep it short, because the agent reads it at the start of every session. + +| Section | Holds | +| --- | --- | +| Where we are | Branch, worktree, current spec or bugfix, phase, PR number. | +| Done | What is complete, with commit SHAs, so the agent does not redo it. | +| Decisions | Choices made in conversation that are not yet written down anywhere else, and instructions such as "the report is approved; do not regenerate it". | +| Review gear | A pointer to `specs/{spec}/.current-gear` when a spec runs in `review-after`. `/spec:gear` writes and removes this line for you. | +| Leave alone | Untracked or unrelated files in the checkout that the agent must not touch or commit. | +| Next | The next step, or "the user has a new task". | + +If one topic needs more room, such as a long hand-over, put it in a `PROMPT-<topic>.md` companion and link to it from `PROMPT.md`. + +**What does not go in it.** + +- **Anything that should outlive the work.** A decision that shapes the code belongs in the requirements, an ADR or `bugfix.md`, where reviewers will see it. If you find `PROMPT.md` holding a design decision, move it there. +- **Progress the repository already records.** The ticks in `tasks.md` (and `/spec:status`) are the authority for which tasks are done, and `bugfix.md` is the authority for a bugfix's phase. `PROMPT.md` points at them rather than duplicating them. If they disagree, the repository wins. +- **Waivers.** Never use `PROMPT.md` to switch off the approval gate or any other rule in `CLAUDE.md`. A prose waiver in `PROMPT.md` is invisible to everyone else and enforced by nothing; that is why `/spec:gear` exists. Now `PROMPT.md` only points at the gear file. +- **Links from durable artifacts.** Never cite `PROMPT.md` in code comments, ADRs, commit messages or pull request descriptions; nobody else can read it ([documentation.md](.agent_instructions/documentation.md)). `/spec:review` flags such references. + +Nothing here is specific to Claude Code: any agent that can read a file can work this way. + +--- + +## Smaller Changes: /test-first and /tidy-first + +- **`/test-first <behavior>`** runs one red-green-refactor cycle. It always stops after writing the failing test so you can review it in your IDE before any implementation. See the [TDD README](.claude/commands/tdd/README.md). +- **`/tidy-first <change>`** separates structural changes (refactoring) from behavioral ones, following Kent Beck's *Tidy First*, so each lands in its own commit. See the [refactoring README](.claude/commands/refactor/README.md). +- **`/adr <title>`** creates a correctly numbered ADR outside the spec workflow, with frontmatter, and regenerates the ADR index. See the [ADR README](.claude/commands/adr/README.md). + +--- + +## Reviewing Agent-Authored Pull Requests + +Whichever gear you drove in, review the pull request yourself before asking anyone else to. An agent's green test suite tells you the code does what its tests say. It does not tell you that the tests say the right thing, or that you could explain the design without looking at the code. + +If you ask an agent to review the pull request, treat it like any other review: useful, occasionally wrong, and never a substitute for a human reviewer signing off. diff --git a/CLAUDE.md b/CLAUDE.md index d6db7dc1..d27dbd33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -218,6 +218,8 @@ Place test-specific handlers, queries, and decorators in `test/Paramore.Darker.T - Cache key: `Linux-nuget-${{ hashFiles('**/Directory.Packages.props') }}` ## Detailed Instructions +For how contributors drive these workflows with an agent (choosing a workflow, review gears, `PROMPT.md` across sessions), see [AGENTIC_CODING.md](AGENTIC_CODING.md). + For comprehensive guidance on working with this codebase, Claude should read the following files as needed: - [Build and Development Commands](.agent_instructions/build_and_development.md) - Build scripts, test commands diff --git a/README.md b/README.md index 8a91a6eb..7d7e63b7 100644 --- a/README.md +++ b/README.md @@ -220,3 +220,7 @@ supported for streams. These are intentional semantics, not limitations to be wo | **Re-enumeration** | Each `await foreach` over the same `IAsyncEnumerable` re-executes the handler with a fresh pipeline. The stream is cold, not cached. To iterate twice over the same data, buffer it yourself (`await ToListAsync()`). | | **Caller-supplied context** | A `queryContext` passed to `ExecuteStream` is scoped to a **single enumeration**. Concurrent or repeated enumeration is only safe when the processor creates the context (pass `null`). | | **Legacy attributes** | `[RetryableQuery]` and `[FallbackPolicy]` do **not** apply to streams. Use `[UseResiliencePipelineStream]` for stream resilience. Applying a mismatched attribute throws `ConfigurationException`. | + +## Contributing with a coding agent + +We welcome contributions authored with a coding agent. See [AGENTIC_CODING.md](AGENTIC_CODING.md) for how to use our agent instructions and slash commands — the spec and bugfix workflows, review gears, and keeping state across sessions — and remember that you remain responsible for the code you submit. From e5016b54492bda2d475f0d1444ad0fabf5610275 Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 11:34:59 +0100 Subject: [PATCH 08/12] =?UTF-8?q?feat(skills):=20/spec:review=20code=20?= =?UTF-8?q?=E2=80=94=20review=20the=20branch=20against=20the=20approved=20?= =?UTF-8?q?specs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port Brighter's code phase: `/spec:review code [--base=ref] [threshold]` diffs the branch against its base (default master) and has an opus sub-agent review it against requirements.md, the ADRs and tasks.md, and against code_style.md, testing.md and design_principles.md. Criteria cover spec conformance, TDD conventions (naming, Real > Simple > InMemory > Mock, TestDoubles, no InternalsVisibleTo), correctness, sync/async/streaming pipeline parity, security and hygiene. Findings go to review-code.md with a verdict; auto-detect reviews code once all three phases are approved. Also correct test-double paths in gear/implement/ralph-implement to the real per-project TestDoubles/ directories (e.g. Paramore.Darker.Core.Tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .claude/commands/spec/README.md | 8 ++ .claude/commands/spec/gear.md | 2 +- .claude/commands/spec/implement.md | 4 +- .claude/commands/spec/ralph-implement.md | 6 +- .claude/commands/spec/review.md | 135 ++++++++++++++++++++--- 5 files changed, 136 insertions(+), 19 deletions(-) diff --git a/.claude/commands/spec/README.md b/.claude/commands/spec/README.md index 2647d3ea..37af173a 100644 --- a/.claude/commands/spec/README.md +++ b/.claude/commands/spec/README.md @@ -209,8 +209,16 @@ Review the current specification phase or specific ADR. /spec:review requirements /spec:review design 0043 /spec:review tasks +/spec:review code # Branch diff vs. master +/spec:review code --base=origin/master # Diff vs. another base ref ``` +`/spec:review code` reviews the implementation — the branch diff — adversarially against the approved +requirements, ADRs and `tasks.md`, plus the project's code-style and testing conventions (including +sync/async/streaming pipeline parity), and writes `review-code.md`. Code has no approval marker: +fix the findings and re-run until clean, then open the PR. With no phase given and every phase +approved, `/spec:review` defaults to `code`. + --- ### `/spec:tasks` diff --git a/.claude/commands/spec/gear.md b/.claude/commands/spec/gear.md index cb65867a..d6bb0fac 100644 --- a/.claude/commands/spec/gear.md +++ b/.claude/commands/spec/gear.md @@ -175,7 +175,7 @@ explicitly whenever you shift: - **Every standing test-authoring convention** in [.agent_instructions/testing.md](../../../.agent_instructions/testing.md): `When_[condition]_should_[behavior]` method and file naming, `[Behavior]Tests` class naming; one - test per file; Arrange/Act/Assert; test doubles in `test/Paramore.Darker.Tests/TestDoubles/`, one + test per file; Arrange/Act/Assert; test doubles in each test project's `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`), one class per file; Real > Simple > InMemory > Mock; no mocks for isolation; public exports only. If a run in `review-after` is dropping any of these, that is a defect in the run, not a consequence diff --git a/.claude/commands/spec/implement.md b/.claude/commands/spec/implement.md index 1e0327e5..03d510ef 100644 --- a/.claude/commands/spec/implement.md +++ b/.claude/commands/spec/implement.md @@ -145,7 +145,7 @@ For each task, follow this strict workflow: 4. **Create/Update Test File**: Use Write or Edit tool to create the test -5. **Run the Test**: Use Bash to run: `dotnet test test/Paramore.Darker.Tests/ --filter "FullyQualifiedName~When_[test_name]"` +5. **Run the Test**: Use Bash to run: `dotnet test test/{TestProject}/ --filter "FullyQualifiedName~When_[test_name]"` - Verify the test FAILS (Red), for the expected reason (behavior doesn't exist yet) - **`CHARACTERISE` task, test passes on first run**: an earlier task already delivers the behaviour — that is expected, not a reason to rewrite the test. Apply the task's **named RED @@ -287,7 +287,7 @@ terminal while you were working, and a downshift must take effect at the very ne - **The two-commit shape** — `feat:`/`test:` for the behaviour, then a separate `docs:` for the task-list tick - **The test-authoring conventions** — `When_[condition]_should_[behavior]` naming; one test per - file; test doubles in `test/Paramore.Darker.Tests/TestDoubles/`, one class per file; + file; test doubles in each test project's `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`), one class per file; Real > Simple > InMemory > Mock; no mocks for isolation ### Code Quality Requirements diff --git a/.claude/commands/spec/ralph-implement.md b/.claude/commands/spec/ralph-implement.md index 6dc5c3c1..9988f397 100644 --- a/.claude/commands/spec/ralph-implement.md +++ b/.claude/commands/spec/ralph-implement.md @@ -76,7 +76,7 @@ Running unattended changes *who reviews when*. It changes nothing else. Every ta separate `docs:` commit ticking the task off in `tasks.md`. - **Obeys every standing test-authoring convention** in `.agent_instructions/testing.md`: `When_[condition]_should_[behavior]` method and file naming, `[Behavior]Tests` class naming; one - test per file; Arrange/Act/Assert; test doubles in `test/Paramore.Darker.Tests/TestDoubles/`, one + test per file; Arrange/Act/Assert; test doubles in each test project's `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`), one class per file; Real > Simple > InMemory > Mock; no mocks for isolation; public exports only. ## Your Task @@ -247,8 +247,8 @@ from conversation. highlight evident data. - Test behavior, not implementation — public exports only; prefer real/Simple/InMemory* test doubles over mocks (Real > Simple > InMemory > Mock); no mocks for isolation. -- Test-specific handlers, queries and decorators go in `test/Paramore.Darker.Tests/TestDoubles/` - (namespace `Paramore.Darker.Tests.TestDoubles`), one class per file. +- Test-specific handlers, queries and decorators go in each test project's `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`) + (namespace `{TestProject}.TestDoubles`, e.g. `Paramore.Darker.Core.Tests.TestDoubles`), one class per file. - Write the test file at the path the task specifies. - Run the verify command and confirm the test **FAILS** for the right reason (behavior doesn't exist yet). **Do not skip this.** Running unattended does not make this test-after. diff --git a/.claude/commands/spec/review.md b/.claude/commands/spec/review.md index 3aaf25b5..a76ad858 100644 --- a/.claude/commands/spec/review.md +++ b/.claude/commands/spec/review.md @@ -1,17 +1,19 @@ --- -allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(grep:*), Bash(mkdir:*), Bash(npx:*), Bash(python3:*), Read, Write, Glob, Grep, Agent -description: Review current specification phase -argument-hint: [requirements|design [adr-number]|tasks] [threshold] +allowed-tools: Bash(cat:*), Bash(test:*), Bash(touch:*), Bash(ls:*), Bash(echo:*), Bash(grep:*), Bash(mkdir:*), Bash(npx:*), Bash(python3:*), Bash(git diff:*), Bash(git log:*), Bash(git status:*), Bash(git rev-parse:*), Bash(git merge-base:*), Bash(git show:*), Read, Write, Glob, Grep, Agent, AskUserQuestion +description: Review current specification phase (requirements, design, tasks, or code) +argument-hint: [requirements|design [adr-number]|tasks|code [--base=ref]] [threshold] --- ## Adversarial Specification Review Current spec directory: specs/ -**Workflow**: Issue -> Requirements -> ADR(s) -> Tasks -> Tests -> Code +**Workflow**: Issue -> Requirements -> ADR(s) -> Tasks -> Tests -> Code -> **Code Review** **Philosophy**: The first draft is never good enough. This review is skeptical and adversarial — it assumes problems exist and looks for them. The goal is to force iteration towards quality. +**Phases reviewable here**: `requirements`, `design`, `tasks`, and `code`. The first three review specification documents. `code` reviews the actual implementation against the approved specs — the diff vs. a base branch, cross-referenced against requirements.md + ADRs + tasks.md. + ## Your Task ### Step 1: Parse Arguments and Determine What to Review @@ -21,12 +23,13 @@ Read `specs/.current-spec` to determine the active specification directory. **Error handling**: If `.current-spec` does not exist, tell the user to run `/spec:new` first and stop. If the spec directory doesn't exist, tell the user and stop. If the document for the requested phase doesn't exist, tell the user to run the appropriate creation command first and stop. Do NOT launch the sub-agent with missing documents. Parse $ARGUMENTS: -- Extract **phase**: first word — `requirements`, `design`, or `tasks` +- Extract **phase**: first word — `requirements`, `design`, `tasks`, or `code` - Extract **adr-number**: for `design` phase, a zero-padded 4-digit number (e.g., `0053`). **Precedence rule**: a 4-digit zero-padded number is ALWAYS an ADR number, never a threshold. +- Extract **base-ref**: for `code` phase, a token starting with `--base=` (e.g., `--base=origin/master`). Default base is `master`. - Extract **threshold**: any other numeric value (default: **60**) -- If phase is empty: auto-detect (first unapproved phase by checking `.requirements-approved`, `.design-approved`, `.tasks-approved` markers). If ALL phases are approved, tell the user all phases are approved and suggest `/spec:status` or `/spec:implement` instead. +- If phase is empty: auto-detect. Check approval markers in order: `.requirements-approved`, `.design-approved`, `.tasks-approved`. The first missing marker is the phase to review. If all three are approved, default to `code` (review the implementation against the approved specs). If no branch is checked out or there is no diff vs. base, tell the user there is nothing to review and suggest `/spec:status`. -**Approved phase warning**: If the user explicitly requests review of an already-approved phase, note this in the sub-agent prompt and include a note in the output. +**Approved phase warning**: If the user explicitly requests review of an already-approved phase, note this in the sub-agent prompt and include a note in the output: "This phase is already approved. Findings are informational — consider whether any warrant re-opening the phase." (Not applicable to `code` — code is never "approved" by the spec system.) Examples: - `/spec:review` -> auto-detect phase, threshold 60 @@ -36,6 +39,9 @@ Examples: - `/spec:review design 0053 80` -> review ADR 0053, threshold 80 - `/spec:review design 70` -> review all ADRs, threshold 70 - `/spec:review tasks 50` -> review tasks, threshold 50 +- `/spec:review code` -> review branch diff vs. master, threshold 60 +- `/spec:review code 70` -> review branch diff vs. master, threshold 70 +- `/spec:review code --base=origin/master 70` -> diff vs. origin/master, threshold 70 ### Step 2: Gather Documents for the Sub-Agent @@ -62,11 +68,24 @@ Read ALL documents the sub-agent will need. The sub-agent gets a clean context - Read `specs/{current-spec}/requirements.md` (for cross-referencing) - Read `specs/{current-spec}/.adr-list` and each ADR (for cross-referencing) +**For code review:** +- Resolve `{base-ref}` (default `master`). Run `git rev-parse --verify {base-ref}` — if it fails, tell the user the base ref doesn't exist and stop. +- Run `git merge-base {base-ref} HEAD` to find the divergence point. +- Run `git diff --stat {base-ref}...HEAD` to get the summary of changed files. +- Run `git log --oneline {base-ref}..HEAD` to get the commit list. +- Run `git status --porcelain` to check for uncommitted changes (flag in findings if present). +- If the diff is empty, tell the user there is nothing to review vs. `{base-ref}` and stop. +- Read `specs/{current-spec}/requirements.md`, all ADRs from `.adr-list`, and `specs/{current-spec}/tasks.md`. These are the contracts the code must satisfy. +- Read `.agent_instructions/code_style.md`, `.agent_instructions/testing.md`, and `.agent_instructions/design_principles.md` — these encode project conventions the sub-agent must check against. +- The sub-agent itself will use `Bash(git diff:* / git show:*)`, `Read`, `Glob`, and `Grep` to pull specific file-level diffs and surrounding context. Do NOT attempt to inline the full diff in the sub-agent prompt — pass the file list, commit list, stats, and base ref, and let the sub-agent drill in where it needs to. + ### Step 3: Launch Sub-Agent for Adversarial Review -Launch an Agent (subagent_type: "general-purpose") with the prompt below. +**Verify scope with the user before launching (MAIN agent).** All user interaction stays in the main agent — never the sub-agent. If the review scope is ambiguous (which phase to review, which ADR for a design review, the base ref for a code review, the threshold to apply), confirm it with the user via `AskUserQuestion` before launching. + +Launch an Agent (subagent_type: "general-purpose", **model: "opus"**) with the prompt below. Review is reasoning-heavy work, so it uses opus per the model policy (see `.claude/commands/spec/README.md` → "Sub-agents & model policy"). -**Sub-agent tool access**: The sub-agent (general-purpose) inherits tool access. For design and tasks reviews it SHOULD use Read, Glob, and Grep to read documents and verify codebase references. For **design** reviews it SHOULD additionally use `Bash` to run the diagram render check and the escaped-markdown grep in the *Diagrams* criteria — those are verifications, not judgements, and a review that skips them has not checked the one class of defect that is invisible to a careful read. The sub-agent should NOT write the findings file — it should return the findings as text. The main agent writes the file after validating the output. +**Sub-agent tool access**: The sub-agent (general-purpose) inherits tool access. For design and tasks reviews it SHOULD use Read, Glob, and Grep to read documents and verify codebase references. For **design** reviews it SHOULD additionally use `Bash` to run the diagram render check and the escaped-markdown grep in the *Diagrams* criteria — those are verifications, not judgements, and a review that skips them has not checked the one class of defect that is invisible to a careful read. For **code** reviews it SHOULD additionally use `Bash(git diff:* / git show:* / git log:*)` to pull file-level diffs on demand, and MAY run `dotnet build Darker.Filter.slnf` / `dotnet test Darker.Filter.slnf` to check a claim about the build or the suite. The sub-agent should NOT write the findings file — it should return the findings as text. The main agent writes the file after validating the output. **IMPORTANT**: The sub-agent prompt must include: 1. The review criteria for the relevant phase (from Step 4 below) @@ -75,6 +94,15 @@ Launch an Agent (subagent_type: "general-purpose") with the prompt below. 4. The threshold value 5. The output format instructions (from Step 5 below) 6. An instruction to RETURN the findings as text output, NOT to write a file +7. An instruction to NOT ask the user any questions — review the supplied inputs and return + findings; any clarification was handled by the main agent before launch + +**Code review sub-agent prompt must additionally include:** +- The resolved base ref, the commit list, and the `git diff --stat` output +- The full text of requirements.md, tasks.md, and each ADR (so the sub-agent can cross-reference without re-reading) +- The paths to `.agent_instructions/code_style.md`, `testing.md`, `design_principles.md` (sub-agent reads them directly) +- An explicit instruction to drill into at least the top-N changed files (by size) using `git diff {base}...HEAD -- <file>` and to cite specific file:line references in every finding +- A reminder that uncommitted changes reported by `git status --porcelain` should be surfaced as at least a Medium finding ### Step 4: Phase-Specific Review Criteria @@ -253,6 +281,69 @@ You are a skeptical reviewer. Assume the task list has problems — your job is --- +#### Code Review Criteria + +You are a skeptical, **adversarial** reviewer. Your job is to find problems, not to validate work that has already been done. Assume: + +- The author convinced themselves the code is right — they are not a reliable narrator. +- Tests that pass locally may still be wrong (asserting the wrong thing, testing the stub, or passing vacuously). +- Commit messages and PROMPT.md describe intent, not outcome — verify against the actual diff. + +Every finding MUST cite concrete evidence: a file path, a line range, a diff hunk, or a spec section. No vibes. + +**Requirement & ADR Fidelity (highest priority):** +- For each FR-N in requirements.md, locate where it is implemented in the diff. If you cannot find it, that is a finding (High or Critical depending on FR importance). +- For each AC, locate a test that asserts it. If the AC is satisfied by a unit test where the spec implies integration, downgrade trust and flag it. +- For each ADR decision, locate the corresponding code. Flag any deviation where the code does something different from the ADR without a documented reason. +- Flag scope creep: changed files or new abstractions that don't trace back to any FR, AC, or ADR decision. +- Flag scope holes: tasks in `tasks.md` marked done but with no visible code. + +**TDD Compliance (Darker-specific):** +- For each behavioral change, confirm a test exists. Prefer finding the test commit BEFORE (or in the same commit as) the implementation in `git log {base}..HEAD` — that's the TDD signature. Absence isn't proof of violation, but combined with tests committed *after* implementation, it's a finding. +- Test naming: method and file `When_[condition]_should_[expected_behavior]`; class `[Behavior]Tests`. Violations are Low–Medium unless pervasive. +- Test doubles follow Real > Simple > InMemory > Mock: Moq used where a real registry, `SimpleHandlerFactory`, `SimpleHandlerDecoratorFactory`, `InMemoryDecoratorRegistry` or `InMemoryQueryContextFactory` would do is a finding. Mocks used to isolate a class (rather than replace I/O) are at least Medium. +- Test-specific handlers, queries and decorators belong in `test/Paramore.Darker.Tests/TestDoubles/` (namespace `Paramore.Darker.Tests.TestDoubles`), one class per file; shared doubles in `test/Paramore.Darker.Testing.Ports/`. +- Tests reach only public exports. `InternalsVisibleTo` added for testing is a High finding. +- Check for `[Fact(Skip=...)]`, commented-out `Assert`, or empty test bodies — any of these in the diff is at least Medium. + +**Correctness:** +- Walk the logic of non-trivial new methods. Look for: off-by-one, null derefs, resource leaks (handlers, decorators, scopes or streams not released/disposed), async methods that swallow exceptions, race conditions in shared or static state (e.g. memoisation caches). +- For each new `if`/`else`, ask: is the else branch tested? Is there a condition where both branches are wrong? +- Exceptions raised through reflection must still surface unwrapped (the `ExceptionDispatchInfo` pattern in `QueryProcessor` / `PipelineBuilder`); a new path that leaks a `TargetInvocationException` is a finding. +- For new configuration/DI changes: is the lifetime correct (singleton vs scoped vs transient)? Are disposables registered correctly? Is anything released in `PipelineBuilder.Dispose()` that should be? + +**Pipeline parity (Darker-specific):** +- Darker has parallel sync, async and streaming pipelines (handlers, decorators, attributes, factories, registries). A behavioural change on one path without its twin — or without a documented reason the twin does not need it — is a finding. Check that each twin also has its test. +- New decorators: step ordering via the attribute is respected, and the decorator is registered/resolved the same way on every path it claims to support. + +**Security (apply even in "internal" code):** +- Injection or path traversal at any trust boundary (query parameters, context bag values, file names, connection strings) +- Secrets or connection strings committed to code, logs, or test fixtures +- Unsafe deserialization of attacker-controlled data; query-logging that writes sensitive query contents +- Weak crypto or `Random` used for security-sensitive values + +**Project Conventions (read `.agent_instructions/code_style.md` yourself to verify):** +- Primary constructors where possible for new classes (project default) +- `Async` suffix on async methods; `ValueTask` vs `Task` consistency with surrounding code +- Nullability annotations consistent with the file's existing style +- XML doc comments on public APIs, and the MIT licence header on new files (project requires them) +- No dead code, speculative abstractions, or commented-out blocks +- No mixing of structural and behavioral changes in the same commit (tidy-first rule) +- Package versions managed centrally in `Directory.Packages.props` — a `Version=` attribute on a project `PackageReference` is a finding + +**Hygiene:** +- `git status --porcelain` non-empty → Medium finding ("uncommitted work on branch at time of review"). +- New files that look misplaced (e.g., test files in src/, or vice versa). +- Binary files, generated files, or `.DS_Store` committed by accident. +- Build warnings introduced (if the diff touches a project the user has recently built, note whether warnings regressed). + +**Grounding — no hallucinated findings:** +- Before filing a finding, verify: did you actually read the file, or are you guessing from the name? If guessing, either read it or don't file the finding. +- If the diff references a class or method, grep for it in the actual codebase (post-change) to confirm it exists as described. +- A finding that says "you should have done X" when X is already present is worse than no finding — it destroys trust in the whole review. Prefer fewer, grounded findings over comprehensive-looking speculation. + +--- + ### Step 5: Output Format for Sub-Agent Tell the sub-agent to produce output in this exact format: @@ -263,6 +354,10 @@ Tell the sub-agent to produce output in this exact format: **Date**: {today's date} **Threshold**: {threshold} **Verdict**: {PASS or NEEDS WORK} +{For code reviews: **Base**: {base-ref} | **Head**: {HEAD commit sha}} + +{If NEEDS WORK: "N findings at or above threshold {threshold}. Address these before approving."} +{If PASS: "No findings at or above threshold {threshold}. Consider addressing lower-scored items."} ## Findings @@ -295,6 +390,13 @@ Tell the sub-agent to produce output in this exact format: - **50-69 (Medium)**: Worth addressing. Vagueness, missing examples. - **0-49 (Low)**: Suggestion or nit. +**Calibration for code reviews:** +- A FR or ADR decision with no corresponding code IS High or Critical (70-95), depending on the FR's importance. +- A behavioural change on one of the sync, async or streaming pipeline paths without its twin IS High (70-80), unless the spec documents why the twin does not need it. +- A test that passes vacuously, or asserts the stub rather than the behaviour, IS High (70-85) — it reads as coverage and is not. +- A mock used to isolate a class where a real/Simple/InMemory double exists IS Medium (55-65). +- Naming-convention drift in tests is Low-Medium (35-55) unless pervasive. + **Calibration for design reviews:** - **A mermaid diagram that does not render IS High (70-85)** — it is a broken artefact that reads as perfectly fine, so nothing else will catch it before it ships. - **An ADR that references a participant in the authoring conversation** ("at the user's direction", "the user explicitly chose") **IS Medium-High (65-75)** — it fails the document's only audience, and once the ADR is Accepted the sentence tends to stay. @@ -311,10 +413,16 @@ Calibrating the structure criteria specifically — they are about navigability, After the sub-agent returns: -1. **Validate the output** before writing -2. **Write the findings file** to `specs/{current-spec}/review-{phase}.md` -3. **Present a summary to the user**, and remind them to use `/spec:approve {phase}` when ready, or - to work the findings (Step 8) and re-run `/spec:review {phase}`. +1. **Validate the output** before writing: the expected sections are present, the Summary counts + match the findings listed (count them yourself), and the verdict is consistent with the threshold +2. **Write the findings file** to `specs/{current-spec}/review-{phase}.md`. For the `code` phase the + file is `specs/{current-spec}/review-code.md`. +3. **Present a summary to the user**: verdict, counts by severity, the title and score of each + finding at or above threshold, and the path to the findings file. + - For `requirements`/`design`/`tasks`: remind them to use `/spec:approve {phase}` when ready, or + to work the findings (Step 8) and re-run `/spec:review {phase}`. + - For `code`: remind them that `code` has no approval marker — fix findings, commit, and re-run + `/spec:review code` until clean. When clean, the next step is commit/push/PR. ### Step 7: Spec Status @@ -323,6 +431,7 @@ Display overall spec status: - Requirements: Approved / In Progress - Design: {X} ADRs ({Y} approved, {Z} proposed) - Tasks: Approved / In Progress / Not Started +- Code: Reviewed at {commit sha} / Not reviewed (code is "reviewed" if `review-code.md` exists; include its verdict and the commit sha at the top of HEAD when the review was run) ### Step 8: Working the Findings — Fix Issues, Not Lines From c8226e054d73ed2fcdeda727226e15a214b8a865 Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 11:34:59 +0100 Subject: [PATCH 09/12] docs: add CONTRIBUTING.md Human-facing contribution guidelines, adapted from Brighter's and from Darker's .agent_instructions: first-time setup, ADRs (frontmatter and the generated index), code style, testing and test doubles, documentation and licensing, dependency management, build and test, commit messages, release notes, branching, submitting changes, the CLA, agentic coding, and the current project structure. README.md gains a Contributing section; AGENTIC_CODING.md links to CONTRIBUTING.md and recommends /spec:review code before opening a PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- AGENTIC_CODING.md | 6 +- CONTRIBUTING.md | 360 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 4 +- 3 files changed, 366 insertions(+), 4 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/AGENTIC_CODING.md b/AGENTIC_CODING.md index 6f937d9b..d21b197c 100644 --- a/AGENTIC_CODING.md +++ b/AGENTIC_CODING.md @@ -2,7 +2,7 @@ This guide explains how to use a coding agent to contribute to Darker. We have focused on [Claude Code](https://claude.com/claude-code), but most of what is here (our instructions, our workflow, the reasoning behind our gears) applies to other agents too. -We welcome code authored with an agent. **You remain responsible for the code you submit.** Read it, understand it, and make sure it follows our contribution guidelines, which [`.agent_instructions/`](.agent_instructions/) restates for agents. Darker shares its workflow with [Brighter](https://github.com/BrighterCommand/Brighter), and the tooling described here is kept in step with Brighter's. The workflows below exist to make that practical. +We welcome code authored with an agent. **You remain responsible for the code you submit.** Read it, understand it, and make sure it follows our [contribution guidelines](CONTRIBUTING.md), which [`.agent_instructions/`](.agent_instructions/) restates for agents. Darker shares its workflow with [Brighter](https://github.com/BrighterCommand/Brighter), and the tooling described here is kept in step with Brighter's. The workflows below exist to make that practical. ## Table of Contents @@ -76,7 +76,7 @@ The `/spec` commands implement Darker's contribution workflow: **Issue → Requi | `/spec:new <name>` | Creates `specs/NNN-name/` and makes it the current spec. | | `/spec:requirements [issue]` | Drafts `requirements.md`, from a GitHub issue if one is given. Requirements say *what* users need, not *how*. | | `/spec:design [focus-area]` | Drafts an ADR in `docs/adr/`, after looking up related ADRs from their frontmatter. Write one ADR per architectural decision; a feature often needs several. ADRs follow the shared skeleton and readability rules in [documentation.md](.agent_instructions/documentation.md). | -| `/spec:review [phase]` | Runs an **adversarial review** of the current phase in a separate agent. Run it more than once: fix the findings, then review again. When you fix findings, fix the *issue* everywhere it is stated, not just the quoted line. | +| `/spec:review [phase]` | Runs an **adversarial review** of the current phase in a separate agent. Once implementation is done, `/spec:review code` reviews the branch diff against the approved specs. Run it more than once: fix the findings, then review again. When you fix findings, fix the *issue* everywhere it is stated, not just the quoted line. | | `/spec:approve <phase>` | Records your approval and unlocks the next phase. Approving `tasks` freezes the *content* of `tasks.md`; ticking checkboxes is still allowed. | | `/spec:tasks` | Breaks the approved design into a test-first task list. | | `/spec:write_release_notes` | Writes the spec's section of `release_notes.md` when the design breaks an existing behaviour or interface. `/spec:design` tells you when to run it. | @@ -287,4 +287,4 @@ Nothing here is specific to Claude Code: any agent that can read a file can work Whichever gear you drove in, review the pull request yourself before asking anyone else to. An agent's green test suite tells you the code does what its tests say. It does not tell you that the tests say the right thing, or that you could explain the design without looking at the code. -If you ask an agent to review the pull request, treat it like any other review: useful, occasionally wrong, and never a substitute for a human reviewer signing off. +Before you open the pull request, run `/spec:review code`: an adversarial review of the branch diff against the approved requirements, ADRs and tasks, and against our code-style and testing conventions. Fix what it finds and run it again until it is clean. Treat any AI review like any other review: useful, occasionally wrong, and never a substitute for a human reviewer signing off. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..06b3eb44 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,360 @@ +# Contributing + +## How to Use This File + +Use this file to follow our coding guidelines when contributing to Darker. Darker is the query-side +counterpart of [Brighter](https://github.com/BrighterCommand/Brighter), and the two projects share +their conventions and workflow. The same guidance is restated for coding agents in +[`.agent_instructions/`](.agent_instructions/); if you work with an agent, also read +[AGENTIC_CODING.md](AGENTIC_CODING.md). + +## Table of Contents + +- [How to Use This File](#how-to-use-this-file) +- [First Time Contributing?](#first-time-contributing) +- [Architecture Decision Records](#architecture-decision-records) +- [Code Style](#code-style) +- [Testing](#testing) + - [TDD Style](#tdd-style) + - [Test Doubles](#test-doubles) +- [Documentation](#documentation) + - [Licensing](#licensing) +- [Dependency Management](#dependency-management) +- [Making Changes](#making-changes) + - [Build & Test](#build--test) + - [Commit Messages](#commit-messages) + - [Release Notes](#release-notes) + - [Repository Branching Strategy](#repository-branching-strategy) + - [Submitting Changes](#submitting-changes) + - [Contributor License Agreement](#contributor-license-agreement) +- [Support for Agentic Coding](#support-for-agentic-coding) +- [Project Structure](#project-structure) + +--- + +## First Time Contributing? + +Welcome! Here's how to get started: + +### Quick Setup +1. Fork and clone the repository +2. Ensure you have the .NET 8 and .NET 9 SDKs installed: `dotnet --list-sdks` +3. Build the solution: `dotnet build Darker.Filter.slnf` +4. Run the tests (no external dependencies needed): `dotnet test Darker.Filter.slnf` + +`Darker.Filter.slnf` excludes the MAUI sample app, which needs extra workloads. Use `Darker.slnx` +only if you are working on that sample. + +### Your First Contribution +1. Look through the [open issues](https://github.com/BrighterCommand/Darker/issues) for one you would like to pick up +2. Comment on the issue to let others know you're working on it +3. Read the relevant sections below: + - [Code Style](#code-style) - Understand our conventions + - [Testing](#testing) - Learn our TDD approach + - [Documentation](#documentation) - XML doc requirements +4. Make your changes following our guidelines +5. Submit a PR targeting the `master` branch + +### Key Guidelines at a Glance +- ✅ Use TDD where possible (write tests first) +- ✅ Prefer real, Simple or InMemory implementations over mocks in tests +- ✅ Add XML documentation to all public APIs +- ✅ Constants use `ALL_CAPS` naming convention +- ✅ Include license header: `#region Licence` (British spelling) +- ✅ Use Conventional Commits for commit messages +- ✅ Keep the sync and async pipelines in step — a change to one usually needs its twin + +--- + +## Architecture Decision Records + +- If adding a new capability to Darker, write an Architecture Decision Record (ADR) in `docs/adr/`. + - The ADR should focus on the *why* of your decision, over implementation details, which can be + better found in the code. + - Use the ADR to agree what you want to do, before you do it. + - Use the ADR to signal to others, including future maintainers, why Darker is built the way it is. +- Follow the skeleton, readability rules and diagram rules in + [documentation.md](.agent_instructions/documentation.md) § *Architecture Decision Records*. + [ADR 0001](docs/adr/0001-record-architecture-decisions.md) is the minimal template. +- Every ADR starts with a short YAML frontmatter block (`id`, `title`, `status`, `author`, + `created`, `summary`, `tags`), described in [adr_frontmatter.md](.agent_instructions/adr_frontmatter.md). + [`docs/adr/index.md`](docs/adr/index.md) is generated from that frontmatter, so read it to find + related decisions, but never edit it by hand. + +You can create the ADR as the first step on a new branch or fork. Your first commit then contains +the ADR describing the change, which lets you open a draft PR so others can check their +understanding of the change and give feedback early, before expectations drift apart. If you +change the design as you learn, add another ADR that supersedes the old one. + +## Code Style + +- Follow .NET C# conventions + - Use [Microsoft's C# naming conventions for identifiers](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names) + - For a const, use an All Caps naming convention, with underscores between words i.e. + `MAX_RETRY_COUNT`. This replaces rules in the Microsoft C# naming convention. + - Follow [Microsoft's C# coding conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) + - DO NOT use Microsoft's Framework Design Guidelines. They are not idiomatic and outdated. +- Prefer expression-bodied members for simple properties and methods. +- Prefer primary constructors where possible, especially for simple classes and records. +- Use readonly for fields that do not change after construction. +- Enable nullable reference types (`<Nullable>enable</Nullable>`), and make types nullable to + indicate optionality. +- You may use marker interfaces. We find them useful as a base type for async and sync interfaces. +- We support both sync and async pipelines. + - Suffix async methods with `Async`. + - Handlers, decorators and their attributes come in separate sync and async types + (see [ADR 0008](docs/adr/0008-split-handler-interfaces.md)). When you change one path, change + its twin, or say why not. +- Use assemblies to provide modularity, divided by optionality. + - Required behavior lives in `Paramore.Darker`. + - Optional behavior, such as DI integration, diagnostics, validation providers or caching, lives in + its own assembly, so users only take a dependency on the NuGet packages they need. +- Default to a class per source file, unless one class clearly exists as the details of another. +- Use Responsibility-Driven Design — see [design_principles.md](.agent_instructions/design_principles.md). + - Specify object behavior before object structure. + - Think of responsibilities for "knowing", "doing" and "deciding", and allocate them to roles. + - Distribute behavior: make objects smart, not just holders of data. + - Preserve flexibility: design objects so interior details can readily change. +- Avoid primitive obsession. Where a primitive could be replaced with a more expressive type, use a + class, struct or record. +- Follow Beck's "Tidy First" approach: + - Separate structural changes (renaming, extracting methods, moving code) from behavioral changes. + - Never mix the two in the same commit, and make structural changes first. + - Run the tests before and after a structural change to show behavior has not changed. +- Not all of our code follows these conventions yet. Follow the boy scout rule and fix what you + touch. + +The full guidance is in [code_style.md](.agent_instructions/code_style.md). + +## Testing + +- Use TDD where possible. +- Write developer tests using xUnit, with Shouldly for assertions. +- Name test methods `When_[condition]_should_[expected_behavior]`, and name the file after the test + method. Name test classes `[Behavior]Tests`, never with `When_`. +- Prefer one test case per file. +- Ensure all new features and bug fixes include appropriate test coverage. + +### TDD Style + +- We write developer tests: a failing test implicates the most recent edit. We do not use mocks to + isolate the system under test. + - You might want to watch [this video](http://vimeo.com/68375232) to understand our preferred + testing approach, or review + [TDD Revisited](https://github.com/iancooper/Presentations/blob/master/Kent%20Beck%20Style%20TDD%20-%20Seven%20Years%20After.pdf). +- Where possible, we are test first: Red, Green, Refactor. + - Only write the code a test needs to pass; do not write speculative code. +- Tests confirm behavior, not implementation details. It should be possible to refactor without + breaking tests. + - Use Arrange/Act/Assert, made explicit with comments. + - Use the Evident Data pattern: highlight the state that affects the outcome. +- The trigger for a new test is a new behavior, not a new method. +- Only test exports from an assembly: public methods on public classes. + - Do not make something public just to test it, and never use `InternalsVisibleTo`. If no existing + export reaches the behavior, read *Narrow and deep* in + [testing.md](.agent_instructions/testing.md) before widening the surface. + +### Test Doubles + +Prefer, in this order: + +1. **Real instances** — for example `QueryHandlerRegistry` or `InMemoryQueryContextFactory`. +2. **Simple implementations** — delegate-based, such as `SimpleHandlerFactory` and + `SimpleHandlerDecoratorFactory`. +3. **InMemory implementations** — such as `InMemoryDecoratorRegistry`. +4. **Mocks** — a last resort, only for I/O boundaries or interactions that cannot be observed through + behavior. + +Put test-specific handlers, queries and decorators in the test project's `TestDoubles/` directory +(for example `test/Paramore.Darker.Core.Tests/TestDoubles/`), one class per file. The full guidance +is in [testing.md](.agent_instructions/testing.md). + +## Documentation + +- Add or update XML documentation comments (`///`) for all exports from assemblies: public and + protected members of public types. Do not document internal or private members. +- Write for a developer reading IntelliSense: helpful, but short enough that they will read it. + - `<summary>` — what the type or member is for. Use `<paramref>` to refer to parameters. + - `<param>` — what the parameter is for, its effect, and any default if it is optional. + - `<returns>`, `<typeparam>`, `<exception>` and `<value>` as appropriate, with `<see cref=""/>` + for types. + - `<remarks>` — implementation notes or design decisions a maintainer would want to know. +- Prefer intention-revealing names to inline comments. In tests you may use `//Arrange`, `//Act`, + `//Assert`. +- Update documentation comments when APIs change. +- Document new features in the [Docs repository](https://github.com/BrighterCommand/Docs) of the + BrighterCommand organization. + +The full guidance, including the ADR contract and writing tone for design documents, is in +[documentation.md](.agent_instructions/documentation.md). + +### Licensing + +- We add an MIT license comment to the top of every source file, before any `using` statements. +- Put it in a `#region Licence` block (British spelling, no space). +- Add your name and the year for a new file. An agent should use the name of the contributor + directing it. + +```csharp +#region Licence + +/* The MIT License (MIT) +Copyright © [Year] [Your Name] [Your Contact Email] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ + +#endregion +``` + +## Dependency Management + +- We use Central Package Management: package versions live in `Directory.Packages.props`, and + project files reference packages without a version. + +```xml +<!-- Directory.Packages.props --> +<ItemGroup> + <PackageVersion Include="Polly" Version="8.7.0" /> +</ItemGroup> + +<!-- A project file --> +<ItemGroup> + <PackageReference Include="Polly" /> +</ItemGroup> +``` + +- Align all `Microsoft.Extensions.*` and `System.*` package versions. +- Avoid mixing preview and stable package versions. +- Keep the core `Paramore.Darker` package's dependencies to a minimum; an optional dependency + belongs in an optional assembly. + +## Making Changes + +- Make sure you have the latest version of `master`. +- Raise an issue in the GitHub issue tracker, after checking whether someone has already raised it. +- If you want to add a feature, as opposed to fixing something that is broken, still raise an issue. + It lets us give specific advice and check the feature does not conflict with work in progress. +- Comment on an issue when you pick it up, so everyone else knows. +- If you have a defect, include: + - Steps to reproduce the issue + - A failing test if possible + - The stack trace for any errors you encountered + +### Build & Test + +```bash +# Build (excludes the MAUI sample) +dotnet build Darker.Filter.slnf + +# Run all tests — none need external services +dotnet test Darker.Filter.slnf + +# Run one test project +dotnet test test/Paramore.Darker.Core.Tests + +# Run a single test +dotnet test test/Paramore.Darker.Core.Tests --filter "FullyQualifiedName~When_query_handler_not_registered" +``` + +Ensure all tests pass before submitting a PR. CI builds and tests against .NET 8 and .NET 9. + +### Commit Messages + +- Try to write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). +- Use the imperative mood (e.g., "Add support for X", "Fix bug in Y"). +- Reference issues or PRs where relevant. +- Use [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#specification) + (`feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`). + +### Release Notes + +If your change breaks an existing behavior or public interface, add a section for it under +`## Master` in [release_notes.md](release_notes.md), saying what breaks and how to migrate. The +`/spec:write_release_notes` command drafts this section from the ADRs for a spec. + +### Repository Branching Strategy + +| Branch | Description | +| --- | --- | +| `master` | The tip of active development. Anything in `master` should ship in the next release, and must compile and pass tests. | +| Release branches | The code for an actively supported release, created when `master` needs breaking changes the current release cannot take. | +| Other branches | Work that is not ready for `master` (for example, it would break CI) or is experimental. | + +**When submitting a pull request:** target `master` unless you're fixing a bug in a specific release +branch. + +### Submitting Changes + +- Fork the project and clone your fork +- Create a branch for your work +- Push to your fork +- Submit a pull request against `master` +- Respond to review feedback; we will get to it as soon as we can + +### Contributor License Agreement + +To safeguard the project, we ask you to sign a Contributor License Agreement. You keep your +copyright, but grant the project the right to use your contribution in perpetuity, so that the +project is not at risk from any one contributor withdrawing their grant of license. + +The agreement is in [CLA.txt](CLA.txt), and signing works through GitHub: +<a href="https://www.clahub.com/agreements/iancooper/Paramore">sign the Contributor License Agreement</a>. + +## Support for Agentic Coding + +We welcome code authored with a coding agent. However, you are responsible for the code you submit: +review it, understand it, and make sure the agent followed these guidelines. + +Our agent support is focused on Claude Code, though other agents can use the same instructions in +`.agent_instructions/`. See **[AGENTIC_CODING.md](AGENTIC_CODING.md)** for: + +- the instruction files and slash commands we provide; +- the `/spec` workflow for features: requirements, ADRs, adversarial review, tasks, TDD + implementation, and a code review of the branch; +- how to choose a review gear with `/spec:gear`, and whether to work a task at a time with + `/spec:implement` or run a loop with `/spec:ralph-implement`; +- the `/bugfix` workflow, which proves a bug's root cause before fixing it. + +### Contributor Code of Conduct + +Please note that this project is released with a Contributor Code of Conduct. By participating in +this project you agree to abide by its terms. The code of conduct is from the +[Contributor Covenant](http://contributor-covenant.org/). + +## Project Structure + +- `src/` — the libraries, one assembly per responsibility: + - `Paramore.Darker` — the core: `QueryProcessor`, the pipeline builder, registries and factories, + the built-in decorators (query logging, retry and fallback policies, Polly resilience + pipelines), streaming queries, and observability. + - `Paramore.Darker.Extensions.DependencyInjection` — integration with + `Microsoft.Extensions.DependencyInjection`, including handler assembly scanning. + - `Paramore.Darker.Extensions.Diagnostics` — wires Darker's tracing and metrics into OpenTelemetry. + - `Paramore.Darker.Caching` — the query result caching decorator. + - `Paramore.Darker.Validation` — the provider-agnostic validation decorator, with + `Paramore.Darker.Validation.DataAnnotations` and `Paramore.Darker.Validation.FluentValidation` + providers. + - `Paramore.Darker.Testing` — helpers for testing code that uses Darker. +- `test/` — test projects, partitioned to match the libraries (`Paramore.Darker.Core.Tests`, + `Paramore.Darker.Extensions.Tests`, `Paramore.Darker.Caching.Tests`, the validation test projects, + and so on), plus `Paramore.Darker.Tests.AOT` for trimming/AOT compatibility, + `Paramore.Darker.Benchmarks`, and the shared `Paramore.Test.Helpers`. +- `samples/` — `SampleMinimalApi` (an ASP.NET Core minimal API) and `SampleMauiTestApp`. +- `docs/adr/` — Architecture Decision Records, with a generated `index.md`. +- `specs/` — specifications produced by the `/spec` workflow. diff --git a/README.md b/README.md index 7d7e63b7..252918df 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,8 @@ supported for streams. These are intentional semantics, not limitations to be wo | **Caller-supplied context** | A `queryContext` passed to `ExecuteStream` is scoped to a **single enumeration**. Concurrent or repeated enumeration is only safe when the processor creates the context (pass `null`). | | **Legacy attributes** | `[RetryableQuery]` and `[FallbackPolicy]` do **not** apply to streams. Use `[UseResiliencePipelineStream]` for stream resilience. Applying a mismatched attribute throws `ConfigurationException`. | -## Contributing with a coding agent +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for our coding guidelines, how to build and test, and how to submit a change. We welcome contributions authored with a coding agent. See [AGENTIC_CODING.md](AGENTIC_CODING.md) for how to use our agent instructions and slash commands — the spec and bugfix workflows, review gears, and keeping state across sessions — and remember that you remain responsible for the code you submit. From 7cb80bedb41165b75fca56e83d00a345feb78c2f Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 11:36:01 +0100 Subject: [PATCH 10/12] docs: ask for PRs to be merged with a merge commit Squash or rebase merges drop the branch commits that spec tooling and review notes refer to by sha; a merge commit keeps them reachable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- CONTRIBUTING.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06b3eb44..55c5f9ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -307,6 +307,11 @@ branch. - Submit a pull request against `master` - Respond to review feedback; we will get to it as soon as we can +Please ask that your pull request is merged with a merge commit, not squashed or rebased. Some of +our tooling pins commit shas from a merged branch's history for later reference — a spec's +`tasks.md` ticks, and the commits that review findings were fixed in — and only a merge commit keeps +those commits reachable from `master` afterwards. + ### Contributor License Agreement To safeguard the project, we ask you to sign a Contributor License Agreement. You keep your From ec37795dbb2594764f8c3af83a3948fa0603284d Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 11:36:27 +0100 Subject: [PATCH 11/12] docs: correct the merge-commit rationale and test-project references Name the sha that tooling actually records (review-code.md), and drop references to test projects that are not in the repository. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .claude/commands/spec/review.md | 2 +- CONTRIBUTING.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude/commands/spec/review.md b/.claude/commands/spec/review.md index a76ad858..465d2453 100644 --- a/.claude/commands/spec/review.md +++ b/.claude/commands/spec/review.md @@ -302,7 +302,7 @@ Every finding MUST cite concrete evidence: a file path, a line range, a diff hun - For each behavioral change, confirm a test exists. Prefer finding the test commit BEFORE (or in the same commit as) the implementation in `git log {base}..HEAD` — that's the TDD signature. Absence isn't proof of violation, but combined with tests committed *after* implementation, it's a finding. - Test naming: method and file `When_[condition]_should_[expected_behavior]`; class `[Behavior]Tests`. Violations are Low–Medium unless pervasive. - Test doubles follow Real > Simple > InMemory > Mock: Moq used where a real registry, `SimpleHandlerFactory`, `SimpleHandlerDecoratorFactory`, `InMemoryDecoratorRegistry` or `InMemoryQueryContextFactory` would do is a finding. Mocks used to isolate a class (rather than replace I/O) are at least Medium. -- Test-specific handlers, queries and decorators belong in `test/Paramore.Darker.Tests/TestDoubles/` (namespace `Paramore.Darker.Tests.TestDoubles`), one class per file; shared doubles in `test/Paramore.Darker.Testing.Ports/`. +- Test-specific handlers, queries and decorators belong in the test project's `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`, namespace `Paramore.Darker.Core.Tests.TestDoubles`), one class per file. - Tests reach only public exports. `InternalsVisibleTo` added for testing is a High finding. - Check for `[Fact(Skip=...)]`, commented-out `Assert`, or empty test bodies — any of these in the diff is at least Medium. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 55c5f9ed..05fa51a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -308,9 +308,9 @@ branch. - Respond to review feedback; we will get to it as soon as we can Please ask that your pull request is merged with a merge commit, not squashed or rebased. Some of -our tooling pins commit shas from a merged branch's history for later reference — a spec's -`tasks.md` ticks, and the commits that review findings were fixed in — and only a merge commit keeps -those commits reachable from `master` afterwards. +our tooling pins commit shas from a merged branch's history for later reference — for example, +`/spec:review code` records the commit it reviewed in the spec's `review-code.md` — and only a merge +commit keeps those commits reachable from `master` afterwards. ### Contributor License Agreement @@ -359,7 +359,7 @@ this project you agree to abide by its terms. The code of conduct is from the - `test/` — test projects, partitioned to match the libraries (`Paramore.Darker.Core.Tests`, `Paramore.Darker.Extensions.Tests`, `Paramore.Darker.Caching.Tests`, the validation test projects, and so on), plus `Paramore.Darker.Tests.AOT` for trimming/AOT compatibility, - `Paramore.Darker.Benchmarks`, and the shared `Paramore.Test.Helpers`. + and `Paramore.Darker.Benchmarks`. - `samples/` — `SampleMinimalApi` (an ASP.NET Core minimal API) and `SampleMauiTestApp`. - `docs/adr/` — Architecture Decision Records, with a generated `index.md`. - `specs/` — specifications produced by the `/spec` workflow. From 6e766de3d4035703373325f83dfdfa7212e4548a Mon Sep 17 00:00:00 2001 From: Ian Cooper <ian_hammond_cooper@yahoo.co.uk> Date: Thu, 8 Oct 2026 11:37:30 +0100 Subject: [PATCH 12/12] docs: remove references to test and source projects that no longer exist - test/Paramore.Darker.Tests is now test/Paramore.Darker.Core.Tests: fix test commands and the test-doubles location (each test project's TestDoubles/ directory, namespace {TestProject}.TestDoubles). - Paramore.Darker.Testing.Ports no longer exists: drop it. - Paramore.Darker.Policies and Paramore.Darker.QueryLogging were merged into core by ADR 0011: describe them as core's Logging/ and Policies/, and fix the README's package links and using directives (Paramore.Darker.Logging.Attributes / Paramore.Darker.Policies.Attributes, matching the sample). The README also pointed at a removed Paramore.Darker.Attributes namespace and the old samples/SampleApi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- .agent_instructions/build_and_development.md | 6 +++--- .agent_instructions/project_structure.md | 6 ++---- .agent_instructions/testing.md | 3 +-- .claude/commands/spec/tasks.md | 2 +- .claude/commands/tdd/test-first.md | 4 ++-- CLAUDE.md | 11 ++++------- README.md | 13 ++++++------- 7 files changed, 19 insertions(+), 26 deletions(-) diff --git a/.agent_instructions/build_and_development.md b/.agent_instructions/build_and_development.md index e0e04c58..8e0d05df 100644 --- a/.agent_instructions/build_and_development.md +++ b/.agent_instructions/build_and_development.md @@ -23,13 +23,13 @@ dotnet build src/Paramore.Darker/Paramore.Darker.csproj dotnet test Darker.Filter.slnf -c Release --no-build # Run tests for specific project -dotnet test test/Paramore.Darker.Tests/ +dotnet test test/Paramore.Darker.Core.Tests/ # Run tests matching pattern -dotnet test test/Paramore.Darker.Tests/ --filter "FullyQualifiedName~QueryProcessorTests" +dotnet test test/Paramore.Darker.Core.Tests/ --filter "FullyQualifiedName~QueryProcessorTests" # Run a single test -dotnet test test/Paramore.Darker.Tests/Paramore.Darker.Tests.csproj --filter "FullyQualifiedName~QueryProcessorTests.ExecutesQueries" +dotnet test test/Paramore.Darker.Core.Tests/Paramore.Darker.Core.Tests.csproj --filter "FullyQualifiedName~QueryProcessorTests.ExecutesQueries" ``` ## Running Samples diff --git a/.agent_instructions/project_structure.md b/.agent_instructions/project_structure.md index 0a41fb8e..2a8ace8d 100644 --- a/.agent_instructions/project_structure.md +++ b/.agent_instructions/project_structure.md @@ -1,9 +1,7 @@ # Project Structure -- `src/Paramore.Darker/` - Core framework (QueryProcessor, PipelineBuilder, registries) +- `src/Paramore.Darker/` - Core framework (QueryProcessor, PipelineBuilder, registries), including the built-in decorators: query logging in `Logging/`, and retry/fallback policies and Polly resilience pipelines in `Policies/` (merged into core by ADR 0011) - `src/Paramore.Darker.Extensions.DependencyInjection/` - Microsoft.Extensions.DependencyInjection integration -- `src/Paramore.Darker.Policies/` - Polly-based retry and circuit breaker decorators -- `src/Paramore.Darker.QueryLogging/` - Request/response logging decorator - `src/Paramore.Darker.Testing/` - Testing utilities - `test/` - Test suites organized by component @@ -11,7 +9,7 @@ - **Test Framework**: xUnit with Moq for mocking and Shouldly for assertions - **Test Patterns**: Behavior-driven test naming -- **Test Doubles**: `Paramore.Darker.Testing.Ports` provides test queries and handlers +- **Test Doubles**: each test project keeps its test-specific queries, handlers and decorators in its own `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`) ## Package Management diff --git a/.agent_instructions/testing.md b/.agent_instructions/testing.md index 41bee795..58ecb306 100644 --- a/.agent_instructions/testing.md +++ b/.agent_instructions/testing.md @@ -185,8 +185,7 @@ A run in `review-after` that drops any of these is defective — it is not "a di 2. **Simple implementations** (e.g. `SimpleHandlerFactory`, `SimpleHandlerDecoratorFactory`) — delegate-based, lightweight, in `src/Paramore.Darker/`. Follow Brighter's `SimpleHandlerFactory` pattern. 3. **InMemory implementations** (e.g. `InMemoryDecoratorRegistry`) — in-memory state, suitable for testing and lightweight production use 4. **Mocks (Moq)** — last resort, only for I/O boundaries or verifying interactions that cannot be observed through behavior -- **Test doubles directory**: Place test-specific handler, query, and decorator doubles in `test/Paramore.Darker.Tests/TestDoubles/` following Brighter's `tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/` convention. Use the namespace `Paramore.Darker.Tests.TestDoubles`. -- Shared test doubles (used across test projects) belong in `test/Paramore.Darker.Testing.Ports/`. +- **Test doubles directory**: Place test-specific handler, query, and decorator doubles in the test project's `TestDoubles/` directory (e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/`), following Brighter's `tests/Paramore.Brighter.Core.Tests/CommandProcessors/TestDoubles/` convention. Use the namespace `{TestProject}.TestDoubles` (e.g. `Paramore.Darker.Core.Tests.TestDoubles`). - Do NOT use fakes or mocks for isolating a class. - We use developer tests: isolation is to the most recent edit, not a class. - Do not inject dependencies into a constructor or property for test isolation diff --git a/.claude/commands/spec/tasks.md b/.claude/commands/spec/tasks.md index cd439491..c5bac5d9 100644 --- a/.claude/commands/spec/tasks.md +++ b/.claude/commands/spec/tasks.md @@ -89,7 +89,7 @@ prompt MUST include: ```markdown - [ ] **TEST + IMPLEMENT: QueryProcessor throws when handler not registered** - **USE COMMAND**: `/test-first when query handler not registered should throw QueryHandlerNotFoundException` - - Test location: "test/Paramore.Darker.Tests" + - Test location: "test/Paramore.Darker.Core.Tests" - Test file: `When_query_handler_not_registered_should_throw_QueryHandlerNotFoundException.cs` - Test should verify: - QueryProcessor.Execute called with unregistered query type diff --git a/.claude/commands/tdd/test-first.md b/.claude/commands/tdd/test-first.md index 5d663717..f2a01e4b 100644 --- a/.claude/commands/tdd/test-first.md +++ b/.claude/commands/tdd/test-first.md @@ -227,7 +227,7 @@ User: /test-first when a query handler is not registered it should throw QueryHa RED: Writing test When_query_handler_not_registered_should_throw_QueryHandlerNotFoundException [Shows test code] -Test file: test/Paramore.Darker.Tests/When_query_handler_not_registered_should_throw_QueryHandlerNotFoundException.cs +Test file: test/Paramore.Darker.Core.Tests/When_query_handler_not_registered_should_throw_QueryHandlerNotFoundException.cs Running test... Test fails: "QueryHandlerNotFoundException not found" @@ -243,7 +243,7 @@ GREEN: Implementing QueryHandlerNotFoundException and handler lookup logic Running tests... - When_query_handler_not_registered_should_throw_QueryHandlerNotFoundException PASSED -- All tests in Paramore.Darker.Tests PASSED +- All tests in Paramore.Darker.Core.Tests PASSED REFACTOR: Extracting handler resolution into dedicated method diff --git a/CLAUDE.md b/CLAUDE.md index d27dbd33..99340acf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,10 +97,8 @@ Darker is the query-side counterpart of [Brighter](https://github.com/BrighterCo 6. Query flows through the pipeline: decorators -> handler -> result ### Key Components -- **src/Paramore.Darker**: Core library with QueryProcessor, PipelineBuilder, registries +- **src/Paramore.Darker**: Core library with QueryProcessor, PipelineBuilder, registries, and the built-in decorators: query logging (`Logging/`) and retry/fallback policies and Polly resilience pipelines (`Policies/`), merged into core by ADR 0011 - **src/Paramore.Darker.Extensions.DependencyInjection**: Microsoft.Extensions.DependencyInjection integration -- **src/Paramore.Darker.Policies**: Polly-based retry and circuit breaker decorators -- **src/Paramore.Darker.QueryLogging**: Request/response logging decorator - **src/Paramore.Darker.Testing**: Testing utilities ### Handler Lifecycle @@ -126,10 +124,10 @@ dotnet build Darker.slnx -c Release dotnet test Darker.Filter.slnf -c Release --no-build # Run tests for a specific project -dotnet test test/Paramore.Darker.Tests/Paramore.Darker.Tests.csproj +dotnet test test/Paramore.Darker.Core.Tests/Paramore.Darker.Core.Tests.csproj # Run a single test -dotnet test test/Paramore.Darker.Tests/Paramore.Darker.Tests.csproj --filter "FullyQualifiedName~QueryProcessorTests.ExecutesQueries" +dotnet test test/Paramore.Darker.Core.Tests/Paramore.Darker.Core.Tests.csproj --filter "FullyQualifiedName~QueryProcessorTests.ExecutesQueries" ``` ### Package Management @@ -186,7 +184,6 @@ Test projects use: - **xunit** for test framework - **Shouldly** for assertions - **Moq** for mocking (last resort — prefer real/Simple/InMemory implementations) -- **Paramore.Darker.Testing.Ports**: Shared test doubles and test queries/handlers ### Test Double Preference (Real > Simple > InMemory > Mock) @@ -199,7 +196,7 @@ Prefer real or lightweight implementations over mocks, following Brighter's patt ### Test Doubles Directory -Place test-specific handlers, queries, and decorators in `test/Paramore.Darker.Tests/TestDoubles/` (namespace: `Paramore.Darker.Tests.TestDoubles`), following Brighter's `TestDoubles/` convention. +Place test-specific handlers, queries, and decorators in the test project's `TestDoubles/` directory, e.g. `test/Paramore.Darker.Core.Tests/TestDoubles/` (namespace: `Paramore.Darker.Core.Tests.TestDoubles`), following Brighter's `TestDoubles/` convention. ### When Testing QueryProcessor 1. Create `QueryHandlerRegistry` (and `QueryHandlerRegistryAsync` for async) and register query -> handler mappings diff --git a/README.md b/README.md index 252918df..8f009fa6 100644 --- a/README.md +++ b/README.md @@ -31,10 +31,10 @@ public void ConfigureServices(IServiceCollection services) }) ``` -This example uses the request logging integration provided by [Paramore.Darker.QueryLogging](https://www.nuget.org/packages/Paramore.Darker.QueryLogging) -and policy integration provided by [Paramore.Darker.Policies](https://www.nuget.org/packages/Paramore.Darker.Policies). -Have a look at the [Startup.ConfigureServices](https://github.com/BrighterCommand/Darker/blob/master/samples/SampleApi/Startup.cs) method -in the [SampleApi](https://github.com/BrighterCommand/Darker/tree/master/samples/SampleApi) project for more examples on how to use the integrations. +This example uses the query logging and policy decorators, which are built into the core +[Paramore.Darker](https://www.nuget.org/packages/Paramore.Darker) package. +Have a look at [Program.cs](https://github.com/BrighterCommand/Darker/blob/master/samples/SampleMinimalApi/Program.cs) +in the [SampleMinimalApi](https://github.com/BrighterCommand/Darker/tree/master/samples/SampleMinimalApi) project for more examples on how to use the integrations. Inject `IQueryProcessor` and call `Execute` or `ExecuteAsync` to dispatch your query to the registered query handler. @@ -81,9 +81,8 @@ For most control, you can also implement `IQueryHandler<,>` directly. ```csharp using Paramore.Darker; -using Paramore.Darker.Attributes; -using Paramore.Darker.Policies; -using Paramore.Darker.QueryLogging; +using Paramore.Darker.Logging.Attributes; +using Paramore.Darker.Policies.Attributes; using System.Threading; using System.Threading.Tasks;