# Plan Milestone

> Break a milestone goal into epics and tasks with dependency ordering, acceptance tests, and task manager ticket creation. Works with any task manager via the adapter layer.

- Skill: `ronniepinnell/plan-milestone` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ronniepinnell/plan-milestone`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ronniepinnell/plan-milestone/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: ronniepinnell (https://skillmd.com/u/ronniepinnell)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ronniepinnell/plan-milestone

---


> **Factory mode:** If `CLAUDE_AUTO` is set in the environment, skip these instructions and instead run `./{scripts_dir}/skills/plan-milestone.sh "$@"` via Bash — the shell script handles autonomous dispatch.

> **MCP Tool Map (Gemini/Codex):** See `.claude/skills/_shared/mcp-tool-map.md` for tool name equivalents.

# Plan Milestone — Epic & Task Breakdown

Takes a milestone goal and produces a full execution plan: epics with dependency ordering, tasks with test criteria, and task manager tickets ready for the factory.

## Milestone Naming Rule — HARD GATE

**Milestone titles must describe a CAPABILITY being added, not a STATE being reached.**

The recurring failure mode is the "DAT004→008" pattern: milestones named
`Finish the Schema`, `Complete Phase 10`, `Wrap up the cleanup` are open-ended
buckets that absorb scope until they're marked Done without ever shipping a
discrete capability. Capability names force a clear acceptance test
(`Add Lineage Tracking` → "lineage column exists, populated for all rows").

### Rejected patterns (regex enforced before milestone creation)

| Reject if title matches | Why |
| --- | --- |
| `^(Finish\|Complete\|Close\|Wrap[- ]up\|Finalize)\b` | "Finish X" is state, not capability |
| `^Final\b` | "Final pass" never ships a thing |
| substring: `the schema`, `the cleanup`, `the audit`, `remaining work` | bucket nouns absorb scope |

Canonical regex (case-insensitive — apply with `(?i)` inline flag or
`/.../i` in JS-style engines). Markdown escapes the pipes in the table
above; the raw patterns are:

```regex
^(Finish|Complete|Close|Wrap[- ]up|Finalize)\b
^Final\b
```

Plus literal substring checks (case-insensitive) for:
`the schema`, `the cleanup`, `the audit`, `remaining work`.

### Required pattern

Title MUST start with an **action verb describing the capability added**:

`Add` · `Build` · `Enable` · `Ship` · `Deploy` · `Migrate` · `Lock-in` ·
`Harden` · `Retire` · `Replace` · `Extract` · `Introduce` · `Wire`

| Bad (state) | Good (capability) |
| --- | --- |
| `Finish the Schema` | `Add Lineage Tracking` |
| `Complete Phase 10` | `Ship Multi-Tenant RLS` |
| `Wrap up the cleanup` | `Retire Legacy CSV Loader` |
| `Final auth pass` | `Harden JWT Refresh Flow` |

The skill MUST refuse to create a milestone whose title matches a rejected
pattern. Surface the regex hit, suggest the verb list, and force the operator
to rename before `save_milestone` is called.

## Step 0: Load Project Context (MANDATORY)

Before any planning, read `.claude/project-context.md` to get:
- `task_manager` — which adapter to use
- `task_prefix` — ticket prefix (e.g. `APP`, `BEN`, `WEB`)
- `task_team_id` — team/project identifier
- `milestone_noun` — what this project calls milestones
- `main_branch` — PR target branch
- `areas` — project area codes (if defined)
- `storage_backend` — load `.claude/skills/_shared/storage/{storage_backend}.md`; route all memory reads/writes (`record_decision`, `list_decisions`, `save_task_plan`, `record_test_run`, …) through its operations. Absent → `none` (reads empty, writes discarded). `storage_schema_intel`/`storage_schema_qa` name the namespaces (defaults `intel`/`qa`).
- `agents` — capability→agent map; reference agents below as `{agents.<capability>}`. Absent → template defaults (`completion_audit: completion-audit`, `spec_audit: spec-audit`, `pragmatism_audit: pragmatism-audit`, `scalability_audit: future-self`). If a mapped agent is unavailable, skip that step with a logged note.

Then load the task manager adapter:
```
Read.claude/skills/_shared/adapters/{task_manager}.md
```

If `.claude/project-context.md` is missing: stop and tell the user to run `/project-init` first.

> **GATE ENFORCEMENT: This skill is the ONLY path to write task manager issues.**
> A PreToolUse hook (`guard-task-writes.sh`) blocks task creation calls unless
> `.claude/.plan-milestone-active` exists. This skill creates that flag on entry and
> removes it on exit.
>
> **Step 0b (MANDATORY — before any planning):**
> ```bash
> touch.claude/.plan-milestone-active
> ```
> **Final step (MANDATORY — after all issues created):**
> ```bash
> rm -f.claude/.plan-milestone-active
> ```

## Naming Conventions (CRITICAL)

### Milestone IDs — `{AREA_CODE}{3D}{LETTER}`

Format: 3-letter area code + 3-digit sequence + letter suffix.

**Area codes are defined per project in `.claude/project-context.md`** under `areas:`.
If no areas are defined, auto-derive the code from the area name:

```
Auto-derivation rules (applied in order):
1. Split area name into words
2. If multi-word: take first letter of each word, uppercase, max 3 chars
   "Computer Vision" → CV → pad to CVX? No — use first 3 letters of each word initial: CVX
   "Machine Learning" → ML → MLX (pad single/double initials to 3 with X)
   "Analytics" → ANL (first 3 consonant-rich chars)
3. If single word ≤ 3 chars: uppercase as-is
4. If single word > 3 chars: first 3 chars, uppercase
   "Analytics" → ANL  |  "Factory" → FCT  |  "Platform" → PLT
5. Confirm with user before creating first milestone in a new area
```

Sequence is auto-incremented: fetch existing milestones for this area from the task manager,
find the highest sequence number, add 1. Start at 001 for new areas.

Examples (after derivation):
```
Computer Vision, seq 5, phase A  →  CVX005A
Machine Learning, seq 4, phase A →  MLX004A
Factory, seq 3, phase A          →  FCT003A
```

**Z suffix = hardening/verification** milestone (e.g. `CVX005Z — Verify CV Pipeline E2E`).
**NEVER use decimal suffixes.** Increment the letter (`A`→`B`→`C`…) for sub-phases.

### Epic Titles — `[{MILESTONE_ID}] {Title}`

```
[MLX004A] V3 Hybrid Architecture
[FCT003A] Budget & Safety Gates
[CVX005A] VERIFY: Baseline Verification
```

### Task Titles — plain descriptive

No prefix needed — the parent relationship provides context.

### Milestone Labels

Every epic and task gets a `milestone:{CODE###X}` label for cross-project filtering.

### Git Branching — Derived from `type:*` Label

Each epic gets ONE branch. All tasks commit to that branch. ONE PR per epic to `{main_branch}`.

| `type:*` label | Branch prefix | Example |
|----------------|--------------|---------|
| `type:feature` | `feature/` | `feature/{prefix}-1371-v3-architecture` |
| `type:fix` | `fix/` | `fix/{prefix}-993-auth-regression` |
| `type:refactor` | `refactor/` | `refactor/{prefix}-997-split-module` |
| `type:test` | `test/` | `test/{prefix}-1000-period-validation` |
| `type:audit` | `audit/` | `audit/{prefix}-996-verify-m0004d` |
| `type:docs` | `docs/` | `docs/{prefix}-1010-doc-automation` |

`{prefix}` = `task_prefix` from project-context.md (e.g. `BEN`, `APP`, `WEB`).

**Branch format:** `{type}/{prefix}-{epic_id}-{short-kebab-name}`
**PR target:** `{main_branch}` from project-context.md (NEVER `main` if configured otherwise)
**Commit prefix:** `[{TYPE}] {prefix}-{task_id}: {what changed}`

## Invocation

```
/plan-milestone "Computer Vision" 5 "Ship tracking pipeline"
/plan-milestone CVX005E --update          # patch existing issues to match current spec
/plan-milestone CVX005E --update --dry-run
```

If a milestone ID is passed directly (e.g. `CVX005E`), skip code derivation and use as-is.

When called from `/ccb`, the `--from-ccb` flag is implicit and CCB context (blockers, debt, acceptance criteria) is already in conversation.

### `--update` Flag — Bring Existing Issues Up to Date

When `--update` is passed, skip Steps 2-4 (no new planning). Instead:

#### Update Step 1: Load All Existing Issues

```python
issues = list_issues(team: "{task_team_id}", query: "[{milestone_id}]")
```

#### Update Step 2: Resolve Correct Project & Milestone IDs

Same as Step 5a-pre — look up the Linear project by code prefix and the milestone by name.

#### Update Step 3: Audit Each Issue

For every epic and task in this milestone, check:

| Field | Check | Fix |
|-------|-------|-----|
| `projectId` | Matches resolved project? | Set it |
| `milestoneId` | Matches resolved milestone? | Set it |
| `labels` | Has `milestone:{CODE}` label? | Add it |
| `labels` | Has `model:*` label? (epics + tasks) | Add default from routing table |
| `labels` | Has `machine:*` label? | Add default from routing table |
| `labels` | Has `type:*` label? (epics) | Add based on title prefix |
| `labels` | Has `epic` or `task` label? | Add based on parentId |
| Description | Has `## Execution Context`? | Add from routing defaults |
| Description | Has `## Goal` with real content? | Flag as thin — needs manual fill |
| `blockedBy` | Set correctly per dependency graph? | Flag if missing |
| Parent | Tasks have correct `parentId`? | Flag if orphaned |

#### Update Step 4: Show Audit Report

```
PLAN-MILESTONE UPDATE: {milestone_id}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  Issue         Field              Current              Fix
  ──────────── ──────────────────── ──────────────────── ────────────────────
  {prefix}-3433     projectId            (none)               → Computer Vision
  {prefix}-3433     milestoneId          (none)               → CVX005E
  {prefix}-3434     labels               missing model:*      → model:sonnet
  {prefix}-3435     Execution Context    (missing section)    → Add default block
  {prefix}-3436     milestone label      (none)               → milestone:CVX005E

  {fix_count} fixes needed across {issue_count} issues
```

If `--dry-run`: print report and stop.
If not `--dry-run`: use `AskUserQuestion` to confirm, then apply all fixes via `save_issue`.

#### Update Step 5: Verify

Run the same verification sweep as Step 5g — confirm all issues are now linked correctly.

---

## Step 0b: Operator Alignment Check

Ask before loading specs or generating anything:

```
Before planning {milestone_id}: anything to discuss, clarify, or flag?
(scope changes, locked decisions, timeline constraints, things that changed)
```

Use `AskUserQuestion` with options:
- **Nothing — let's plan** (default)
- **I have something to flag** — free-text; capture decisions via `record_decision` (storage) then continue
- **Hold — scope isn't settled yet** — STOP, do not proceed

**If `CLAUDE_AUTO=1`:** skip this step.

**Grill me (strategic milestones):** When the milestone has ≥ 3 epics planned or touches schema/ETL/auth, ask 2–3 clarifying questions before generating the epic breakdown:
1. "What does 'done' look like for this milestone — what can the Operator do that they can't do today?"
2. "Are there any scope items that should NOT be included, even if they seem related?"
3. "Any external dependencies (integrations, data sources, vendors) that could block this?"

Capture answers as decisions if they constrain scope. Skip if Operator answers "looks good" to any question.

---

## Step 0c: Honesty Stack Mode Selection

Before generating epics, lock in the honesty-stack mode that the milestone and every child epic will inherit. The mode controls which honesty hooks fire at `/epic close` / `/milestone close`:

* **`full`** — all hooks (R-43 lint + audit-doubt + verifier-isolation + transition + dependency-audit). Use for P0 milestones (FCT*, DAT*, HARDEN, Z) where the cost of a missed honesty signal is high.
* **`lite`** — cheap hooks only (R-43 lint + transition-validator). The default — keeps routine epics cheap.
* **`off`** — nothing fires. Emergency override for trivial fixes / Operator directive.

### Resolution order (highest precedence first)

1. `HONESTY_MODE` env var (non-interactive fallback for autonomous runs).
2. The Operator's `AskUserQuestion` choice in this step.
3. `resolve_honesty_mode({milestone_id})` from `scripts.factory.lifecycle_helpers` — applies the `per_milestone_overrides` globs declared in `.claude/project-context.md`.

### Procedure

```python
import os
from scripts.skills.plan_milestone_helpers import (
    read_honesty_mode_from_description,
    record_honesty_mode_decision,
    resolve_honesty_mode,
    upsert_honesty_block,
)

resolved_default = resolve_honesty_mode(milestone_id)  # e.g. "full" for FCT*
env_override = (os.getenv("HONESTY_MODE") or "").strip().lower()
existing_mode = read_honesty_mode_from_description(current_milestone_description)

if env_override in {"full", "lite", "off"}:
    # CLI override always wins, even over a persisted block.
    chosen_mode = env_override
    rationale = "HONESTY_MODE env var override"
elif existing_mode in {"full", "lite", "off"} and not honesty_block_is_stale(current_milestone_description):
    # CR PR #9817: re-running /plan-milestone on an already-planned milestone
    # must NOT re-prompt the operator for the mode. Reuse the persisted
    # block when it's fresh.
    chosen_mode = existing_mode
    rationale = "Reused persisted honesty mode from milestone description"
elif os.getenv("CLAUDE_AUTO") == "1":
    chosen_mode = resolved_default
    rationale = f"CLAUDE_AUTO=1, using resolved default for {milestone_id}"
else:
    # Interactive: surface the resolved default + ask the operator.
    # Use AskUserQuestion with three options. The label of the resolved
    # default carries "(Recommended)".
    chosen_mode = ask_via_AskUserQuestion(resolved_default)
    rationale = f"Operator choice (resolved default was {resolved_default})"
```

The `AskUserQuestion` block:

```text
Question: "Honesty stack mode for {milestone_id}? (resolved default: {resolved_default})"
Options:
  - "{resolved_default} (Recommended)" — recap of what {resolved_default} runs
  - "{other_mode_1}" — recap of what {other_mode_1} runs
  - "{other_mode_2}" — recap of what {other_mode_2} runs
```

### Persistence

After resolving `chosen_mode`:

```python
# 1. Write the block into the milestone description (idempotent — replaces
#    any existing block, otherwise inserts at end).
new_description = upsert_honesty_block(
    description=current_milestone_description,
    mode=chosen_mode,
    milestone_id=milestone_id,
    rationale=rationale,
)
# Apply via save_milestone (or save_issue for the
# tracker proxy).

# 2. Record the choice via record_decision (supabase → {storage_schema_intel}.decisions). Returns the decision_number on
#    success, None on failure (never raises).
record_honesty_mode_decision(milestone_id, chosen_mode, rationale)
```

The block rendered by `upsert_honesty_block` looks like:

```markdown
<!-- honesty-stack:begin -->
## Honesty Stack

- mode: `full`
- resolved: 2026-05-18 by /plan-milestone for FCT011C
- rationale: Operator choice (resolved default was full)
<!-- honesty-stack:end -->
```

`/milestone start`, `/pipeline run`, and `/epic start` read this block via `read_honesty_mode_from_description()` to seed each session's `honesty_mode` column.

### Skip conditions

* `CLAUDE_AUTO=1` → use resolved default silently (no prompt).
* Milestone description already contains a non-stale honesty-stack block AND no CLI override → re-use the existing mode, do NOT re-prompt.

---

## Step 1: Spec Grounding — HARD GATE (do NOT skip, do NOT plan from memory)

> Planning from the milestone title or from memory — instead of from the spec files on
> disk — is the #1 cause of drift and false Done marking. You may **not** write
> acceptance criteria (Step 2), epics, or tasks until you have produced and shown the
> **Spec Grounding Digest** below. This gate is as binding as the naming gate above.

### 1a. Discover the relevant specs — run these, don't guess

```bash
ls {spec_dir}/ ; cat {spec_dir}/.abstract.md 2>/dev/null   # what specs exist
# milestone-goal keywords → matching specs (substitute 2-4 real keywords):
grep -rilE '<keyword1>|<keyword2>|<keyword3>' {spec_dir}/ rules/ | head -20
test -f {spec_dir}/GITHUB_FACTORY_SPEC.md && echo "factory spec present"
```

Also load the constraints that bound the plan:
- `list_decisions()` — storage op (supabase → `{storage_schema_intel}.decisions`) — locked decisions (D-block etc.) that constrain choices.
- Prior-milestone `**Spec Section:**` refs and any open carry-over.
- Open / recently-closed issues: `gh issue list --state open --json number,title,labels,milestone --limit 100`.

### 1a-verify. External model cross-check — did you miss any specs? (MANDATORY)

Before reading anything, send your discovered spec list + the milestone goal to Ollama or Gemini.
The external model sees only filenames and the goal — it cannot read the files. Its job is to flag
specs you might have missed based on filename pattern and milestone topic alone.

```bash
python3 {scripts_dir}/skills/verify_spec_coverage.py \
  --goal "{milestone goal as plain English}" \
  --found-specs "{spec_dir}/FOO.md {spec_dir}/BAR.md rules/areas/etl.md" \
  --spec-dirs "docs/specs docs/runbooks rules/areas"
```

The script calls Ollama (→ Gemini fallback) and returns:
- **Confirmed:** specs your search found that the model agrees are relevant
- **Missed:** specs in the tree the model thinks you should also read
- **Irrelevant:** specs your search found that the model thinks don't apply

Any file in **Missed** is a mandatory read — add it to your list before 1b.
If the external model is unavailable, skip with a logged warning and proceed.

### 1b. READ each candidate spec end-to-end, then write the digest

Open every spec relevant to this milestone with the Read tool and read it in full. Then
write a **Spec Grounding Digest** to `.claude/.plan-specs-{milestone_id}.md` *and* show it
to the Operator. One row per relevant spec section:

| Spec ref (`file#§`) | Lines read | Requirement (VERBATIM quote from the file) | Status today | Epic |
|---|---|---|---|---|
| `{spec_dir}/X.md#§4.2` | L120–138 | "the manifest MUST declare every generated table…" | none | E01 |
| `{spec_dir}/X.md#§4.3` | L139–151 | "CI fails if declared ≠ reality" | partial | E02 |

The **verbatim quote** column is the forcing function: you cannot fill it without having
opened the file. Paraphrase-only rows are rejected. Every quote needs its `Lstart–Lend`.

### 1c. Spec Quiz — mechanical verification (do NOT skip)

After writing the digest, run the quiz against every spec file you read. The quiz pulls
verbatim lines and asks you to fill them in — it cannot be passed without having read the file.

```bash
python3 {scripts_dir}/skills/spec_quiz.py <spec_file1> [<spec_file2>...] --questions 2
```

- **Pass (≥70%):** proceed to gate check.
- **Fail:** return to 1b, re-read the flagged file end-to-end, retry once.
- **Still fail:** stop. Tell the Operator which spec is unclear before continuing.

Also verify the digest structure is valid (catches empty-quote cells):

```bash
python3 {scripts_dir}/skills/spec_quiz.py --digest.claude/.plan-specs-{milestone_id}.md
```

This must return `OK` before Step 2.

### 1d. Gate check — ALL must be true before Step 2

- [ ] Spec quiz passed (≥70%) for every spec file read.
- [ ] Digest structure check returned `OK` (no empty quote cells).
- [ ] Every spec path in the digest passed `test -f` (it is a real file).
- [ ] Every row has a **verbatim** quote **and** a line range — no empty/paraphrased quote cells.
- [ ] Every milestone epic in Step 3 maps to ≥1 digest row via its `**Spec Section:**` line.
- [ ] The digest file `.claude/.plan-specs-{milestone_id}.md` exists and is non-empty.
- [ ] Spec **gaps** are explicit: each section is marked `none` / `partial` / `done` — gaps drive the epic breakdown.

If you cannot quote a requirement, you have NOT read that spec — return to 1b. These rows are
the source feeding the §46 spec-coverage producer hook (Step 5a-post); a missing digest there
means you skipped this gate.

---

## Step 2: Generate Acceptance Criteria

**Before any epic planning**, define milestone-level acceptance criteria. These are the tests that the hardening epic will run.

Format:
```markdown
## {milestone_id} Acceptance Criteria

### Functional
- [ ] {User-visible behavior that must work end-to-end}
- [ ] {Another behavior}

### Technical
- [ ] {Performance/reliability requirement}
- [ ] {Integration requirement}

### Quality
- [ ] All new code has test coverage
- [ ] No new files over 500 lines
- [ ] All spec sections for this milestone have implementation
```

**OUTCOME-BASED CRITERIA RULE (CRITICAL):**
Every acceptance criterion MUST assert an observable OUTCOME, never an operation.

| BANNED (operation-based) | REQUIRED (outcome-based) |
|---|---|
| "Migration X applied successfully" | "Table X has columns A (bigint), B (text), C (uuid)" |
| "Script ran without errors" | "Query `SELECT count(*) FROM X` returns > 0" |
| "File was created" | "File exports function Y and passes type-check" |
| "DROP COLUMN ran" | "Column Z does NOT exist in information_schema" |
| "RLS policy created" | "pg_class.relrowsecurity = true for table X" |

Operation-based criteria are how false Done marking happens — a `DROP COLUMN IF EXISTS` silently no-ops, the agent checks "did it run?" (yes), marks Done, and the column is still there. Outcome-based criteria catch this because they check the actual state.

Present to Operator for approval. Adjust based on feedback.

### Step 2b: Write Epic Test Files BEFORE Tasks (TDD — Red First, Independent Model)

> **HARD RULE: Claude does NOT write the tests. A separate model writes them.**
> Claude's implementation plan is NOT shown to the test writer — only the Spec Grounding Digest
> and Exit Criteria. This is the only way tests are independent. Self-review is not review.

#### Step 2b-0: Dispatch Test Writing to External Model (MANDATORY)

After Step 2a (Exit Criteria are written), dispatch test writing to an external model. The test
writer sees ONLY the spec digest and exit criteria — never Claude's internal planning notes.

**Preferred dispatch order (try in sequence, use first available):**

1. **Ollama (local, free)** — try `codestral:22b` or `deepseek-coder-v2` first:
   ```bash
   ollama list 2>/dev/null | grep -E "codestral|deepseek-coder|qwen2.5-coder" | head -3
   ```
   If a suitable model is available:
   ```bash
   # Write the test brief (spec + exit criteria ONLY — no implementation context)
   cat > /tmp/test_brief_{milestone_id}.md << 'EOF'
   # Test Writing Brief — {milestone_id} {epic_id}

   You are a grumpy, adversarial test writer. You do not trust the implementation.
   Your job: write failing (RED) pytest tests that prove the exit criteria below are met.
   You have NOT seen the implementation. Write tests that WOULD CATCH a lazy implementation.

   ## Spec (verbatim from spec digest)
   {paste relevant rows from.claude/.plan-specs-{milestone_id}.md}

   ## Exit Criteria to Test
   {paste this epic's Exit Criteria from Step 2a}

   ## Rules
   - Every criterion gets ≥1 test function
   - Fail with raise NotImplementedError("RED: {criterion}") — NEVER pytest.skip
   - Docstring states which criterion is proven
   - Assert OUTCOMES, not "did the function run"
   - Be adversarial: write the test that would catch a stub or fake
   EOF

   ollama run codestral:22b < /tmp/test_brief_{milestone_id}.md > /tmp/test_output_{milestone_id}.py
   ```
   Review the output. If sensible, copy to the correct test path (see naming below). If garbage, fall back.

2. **Codex (if available)** — same brief, same isolation constraint. Codex does NOT see Claude's plan.

3. **Claude as fallback (last resort only)** — only if Ollama and Codex are both unavailable. If Claude
   must write its own tests, explicitly log in the milestone description:
   ```
   ⚠️ SELF-TESTED: No external model available at planning time. Tests written by Claude.
   Flag for adversarial review before epic closes.
   ```
   Then write tests using the Exit Criteria only — deliberately ignore the implementation plan.

For every epic in this milestone, write the actual test file(s) to disk NOW — before any task is
created in Linear. These tests FAIL immediately (RED). They turn GREEN when the epic is done.
This is not optional. If there is no test file, there is no proof of completion.

**Tests are always specific to what THIS epic builds.** Never write generic scaffolding or
`pytest.skip` placeholders. Derive the test content directly from the epic's Exit Criteria —
each criterion becomes one or more test functions.

#### Layer Routing Table — which suite for which epic type

| Epic builds... | Test layer | Suite folder | When it runs |
|---|---|---|---|
| A function, class, hook, CLI command | 1 — Unit | `tests/unit/` | Every commit on epic branch |
| DB queries, API endpoints, cross-module flows | 1+2 — Integration + Contract | `tests/integration/`, `tests/contract/` | Every PR |
| Schema migration, RLS policy, column change | 2+8 — Contract + Migration | `tests/contract/`, `tests/migrations/` | VERIFY-1 gate |
| ETL pipeline output, data transformation | 6 — Canonical | `tests/canonical/` | VERIFY-2 gate |
| Auth, RLS, COPPA/parental_consent, anon access | 5 — Security | `tests/security/` | VERIFY-4 gate |
| Dashboard page, user flow, UI interaction | 3 — E2E | `tests/e2e/` | VERIFY-4 gate |
| Fixes a known past bug or false Done claim | 8 — Regression | `tests/regression/` | Every commit forever |
| Invariants that must hold for any input | 4 — Property | `tests/property/` | VERIFY-FINAL + nightly |
| Latency SLO, throughput, scale | 7 — Performance | `tests/performance/` | VERIFY-FINAL + nightly |

Most epics need Layer 1 (unit) + one domain-specific layer. Never skip Layer 1.

#### Naming Convention (mandatory)

```
tests/{suite}/test_{milestone_id_lower}_{epic_short}_{what}.py

Examples:
  tests/unit/test_proc001a_e01_karen_blocks_done.py
  tests/contract/test_dat008a_e03_event_schema.py
  tests/security/test_proc001a_e02_apply_migration_blocked.py
  tests/regression/test_ben3706_onb001a_invite_flow.py   ← issue-named regressions
```

#### How to write the test (not a placeholder)

Read the epic's Exit Criteria. For each criterion, write one test function that:
- **Asserts the OUTCOME directly** — never "did it run?" but "does the result match?"
- **Fails right now** with `raise NotImplementedError(f"RED: {what this epic must deliver}")`
  — NOT `pytest.skip`. `skip` hides the test. `NotImplementedError` shows the gap.
- **Has a docstring** stating which Exit Criterion it proves

```python
# tests/unit/test_proc001a_e01_karen_blocks_done.py
"""
PROC001A E01: completion-audit pre-commit hook tests.
Written RED before implementation. All must pass before epic closes.
"""
import pytest

class TestKarenDoneGuard:
    def test_blocks_commit_when_test_file_missing(self, tmp_branch):
        """Exit Criterion: git commit with 'Done' fails if ## Tests file doesn't exist."""
        raise NotImplementedError("RED: guard-done-marking.sh not yet implemented")

    def test_blocks_commit_when_test_file_has_zero_functions(self, tmp_branch):
        """Exit Criterion: commit blocked if listed test file has no test_ functions."""
        raise NotImplementedError("RED: guard-done-marking.sh not yet implemented")

    def test_allows_commit_when_tests_pass(self, tmp_branch, passing_test_file):
        """Exit Criterion: commit succeeds when listed tests all pass."""
        raise NotImplementedError("RED: guard-done-marking.sh not yet implemented")
```

#### Three layers required per exit criterion (thin tests are rejected)

Every exit criterion must produce tests in all three layers. One criterion = three test functions minimum:

| Layer | Class prefix | What it asserts | Docstring must contain |
|---|---|---|---|
| 1 Mechanical | `TestMech_` | Code runs, returns correct type/shape | "Mechanical:" |
| 2 Spec/Contract | `TestSpec_` | Output matches verbatim spec requirement | "Spec: [quoted requirement]" |
| 3 Outcome | `TestOutcome_` | End user sees correct result | "Outcome: As a [user]..." |

A test file with only Layer 1 tests will be rejected by the TDD gate.
Layer 2 docstrings MUST quote the spec verbatim — no paraphrase.

#### After writing all epic test files

```bash
pytest tests/ --collect-only -q 2>/dev/null | tail -5  # confirm tests are collected
pytest tests/unit/test_{milestone}_{epic}*.py           # confirm they FAIL (RED)
git add tests/
git commit -m "[TEST] {milestone_id}: Write RED test files for all epics (pre-implementation)"
```

This commit is the proof that TDD is real. **The factory cannot claim an epic Done
unless its test file exists and pytest returns 0.**

The `## Tests` section of every Linear task must reference these exact file paths.

### Step 2b.1: Consume CCB Phase 3.26 output — testing-scenarios + walkthrough twin

If this milestone was planned via `/ccb`, Phase 3.26 already produced, per
feature epic: a `## Testing Scenarios` block, a walkthrough-twin issue under
`master_verification_milestone` (`.claude/project-context.md`), and a row in
`config/test_coverage_matrix.json`. `/plan-milestone` does NOT regenerate
these — it writes them verbatim into the epic description alongside `##
Tests`, and confirms the twin link:

```
for each feature epic:
  assert epic.body contains "## Testing Scenarios"      # from CCB 3.26
  assert epic.body contains "Verification twin: {id}"    # walkthrough issue link
  if either missing:
    # This milestone was NOT run through /ccb (or /ccb predates Phase 3.26).
    # /plan-milestone must generate them itself — do not skip.
    build the testing-scenarios block per the template in
    `.claude/skills/ccb/SKILL.md` § Phase 3.26, using this epic's acceptance
    tests + spec refs as source material.
    create the walkthrough-twin issue under master_verification_milestone.
    append the row to config/test_coverage_matrix.json.
```

This is what makes planning output "feature epic AND its verification
twin" with zero extra Operator steps — the twin either arrives pre-built from
`/ccb` or gets built here, but it always exists before the epic is written
to the task manager.

### Step 2c: Write Acceptance Test Scaffolding (Outcome Verification — Red First)

For every epic, also create acceptance test scaffolding in `tests/acceptance/` for each applicable tier. These answer "does this actually work for a real user?" — not just "does the code exist?"

**Tier selection — auto-determine from what the epic builds:**

| Epic builds | Scaffold file | Key assertion to pre-write |
|---|---|---|
| CLI command | `tests/acceptance/cli/test_{milestone}_{epic}.py` | "Running `{project_cli} X Y` returns expected output against live DB" |
| API endpoint | `tests/acceptance/api/test_{milestone}_{epic}.py` | "GET/POST to `/api/X` returns schema matching TS interface" |
| Dashboard page/component | `tests/acceptance/ui/test_{milestone}_{epic}.py` | "Page loads, key data renders, primary action works" |
| ETL table / migration | `tests/acceptance/data/test_{milestone}_{epic}.py` | "Table exists, row count in range, idempotency holds" |

All acceptance tests must:
- Use `pytest.mark.skipif(not os.environ.get("SUPABASE_URL"), reason="requires live credentials")`
- Fail RED with `raise NotImplementedError("RED: {what this must prove}")` until the epic is built
- Have a docstring stating the user story: "As a {user}, I can {action} so that {outcome}"

**Also write the User Story and End-User Verification stub for each epic** directly into the Linear epic description's `## End-User Verification` section. Pre-fill the format:

```
## End-User Verification
**As a {user type}, you can now:** {action in plain English}

**To verify:**
1. {Step 1 — e.g. "Run `{project_cli} X` or go to /page/url"}
2. {Step 2 — what to look for}
3. Expected: {what correct looks like}

**Regression check:** verify {top 2 adjacent features} still work.
```

Write `TBD — fill at close` for the regression check if unknown at planning time.

After writing all acceptance scaffolding:
```bash
pytest tests/acceptance/ --collect-only -q 2>/dev/null | tail -5  # confirm collected
git add tests/acceptance/
git commit -m "[TEST] {milestone_id}: Write acceptance test scaffolding (RED)"
```

---

## Step 3: Epic Breakdown

### 3a: Identify Epics

Group related work into epics. Each epic should:
- Map to 1-2 spec sections
- Be completable in 1-3 days of factory time (5-15 issues)
- Have clear entry criteria (what must exist before this epic starts)
- Have clear exit criteria (how do we know this epic is done)

### 3b: Dependency Ordering

Build a dependency graph:
```
[FCT003A] Codex/Cursor CLI ← no deps
[FCT003A] Clone Slots ← depends on CLI epic
[FCT003A] Review Loop ← depends on Clone Slots
[FCT003A] Auto-Merge ← depends on Review Loop
[FCT003Z] HARDEN ← depends on ALL above
```

### 3b.1: blockedBy Convention (CRITICAL — for all agents)

Every `blockedBy` relationship must be set explicitly via Linear. Never rely on implied ordering.

#### Within-Milestone Patterns

```
VERIFY epic:       blockedBy = []                    (always first, no deps)
Feature epic:      blockedBy = [VERIFY]              (all feature epics blocked by VERIFY)
Feature epic (cross-dep): blockedBy = [VERIFY, {prefix}-{other_feature}]
VERIFY-MECH epic:  blockedBy = [ALL feature epics]   (if selected in Step 3d.5)
HARDEN epic:       blockedBy = [VERIFY-MECH]         (or [ALL feature epics] if no VERIFY-MECH)
VERIFY-HUMAN epic: blockedBy = [HARDEN]              (if selected in Step 3d.5)
```

**Standard within-milestone blockedBy call:**
```python
# VERIFY: no blockers
# Feature epic E02:
save_issue({ id: "{E02}", blockedBy: ["{prefix}-{VERIFY}"] })
# Feature epic E03 (also blocked by E02):
save_issue({ id: "{E03}", blockedBy: ["{prefix}-{VERIFY}", "{prefix}-{E02}"] })
# VERIFY-MECH (blocked by all feature epics, if selected):
save_issue({ id: "{VERIFY_MECH}", blockedBy: ["{prefix}-{E02}", "{prefix}-{E03}", "{prefix}-{E04}"] })
# HARDEN (blocked by VERIFY-MECH, or all feature epics if no VERIFY-MECH):
save_issue({ id: "{HARDEN}", blockedBy: ["{prefix}-{E02}", "{prefix}-{E03}", "{prefix}-{E04}"] })
# VERIFY-HUMAN (blocked by HARDEN, if selected):
save_issue({ id: "{VERIFY_HUMAN}", blockedBy: ["{prefix}-{HARDEN}"] })
```

#### Cross-Milestone Patterns

When one milestone depends on another milestone's completion (e.g. DAT004E depends on DAT004D):

```python
# The VERIFY epic of the dependent milestone is blocked by the HARDEN epic
# of the prerequisite milestone. This is the official cross-milestone dependency wire.

# Example: DAT004E-VERIFY blocked by DAT004D-HARDEN
save_issue({
  id: "{DAT004E_VERIFY}",
  blockedBy: ["{DAT004D_HARDEN}"]
})

# TRK001A-VERIFY blocked by DAT004D-HARDEN (depends on migrations, not views):
save_issue({
  id: "{TRK001A_VERIFY}",
  blockedBy: ["{DAT004D_HARDEN}"]
})
```

**Rule:** Always wire `VERIFY` of the downstream milestone to `HARDEN` of the prerequisite. Never wire individual feature epics to epics in another milestone (too granular, breaks when plans change).

#### Task-Level blockedBy

Within an epic, tasks that must run sequentially:
```python
# Task 2 blocked by Task 1 (schema must exist before API can reference it):
save_issue({ id: "{task2}", blockedBy: ["{prefix}-{task1}"] })

# REVIEW exit gate task is ALWAYS blocked by ALL other tasks in the epic:
save_issue({
  id: "{REVIEW_task}",
  blockedBy: ["{t1}", "{prefix}-{t2}", "{prefix}-{t3}"]
})
```

Tasks that CAN run in parallel have no blockedBy relationship — omit or set `blockedBy: []`.

### 3c: Epic Template

For each epic, produce:

```markdown
## [{milestone_id}] {Title}

**Spec Section:** {spec_file}#§{section}
**Dependencies:** {prefix}-{prev}, {prefix}-{prev2}
**Entry Criteria:** {what must be true before starting}
**Exit Criteria:** {what must be true to close}

### Acceptance Tests (written BEFORE tasks)
- [ ] {Testable criterion 1}
- [ ] {Testable criterion 2}

### Tasks
1. {task title} — {size S/M/L} — {labels}
2. {task title} — {size S/M/L} — {labels}
...

### Task Dependency Order
{which tasks can run in parallel vs must be sequential}
```

### 3d: Add Verification Epic (FIRST)

ALWAYS add as the FIRST epic in any milestone:

```markdown
## [{milestone_id}] VERIFY: Prior Milestone Verification

**Dependencies:** None (runs first)
**Entry Criteria:** Previous milestone closed OR first milestone (baseline audit)

### Tasks
1. **Smoke test infrastructure** — verify build, CI, daemon, and core systems are healthy:
   - `npm run build` in ui/dashboard/ exits 0
   - `pytest tests/` core tests pass
   - Factory daemon starts and claims 1 test issue
   - Supabase connectivity verified
   Size: M — type:test
2. **Quality sweep** — run Phase 6.5 quality sweep on ALL milestone artifacts:
   - Milestone issue has required sections
   - All epics have Outcome/Tasks/Exit Criteria/Gate Level
   - All tasks have Goal/Steps/Outcome/Guardrails/factory labels
   - Cross-references intact (no orphans)
   Fix anything that fails. Size: M — type:audit
3. Run prior milestone acceptance criteria (regression check) — M — type:test
4. {agents.completion_audit} audit: verify prior milestone completions still hold — M — type:audit
5. {agents.spec_audit} audit: verify spec sections from prior milestone — M — type:audit
6. Fix any regressions found — variable — type:fix
7. Generate baseline test snapshot for this milestone — S — type:test

### Exit Criteria
Infrastructure healthy. All artifacts pass quality sweep. Prior milestone criteria still pass. No regressions. Baseline captured.
```

For the FIRST-EVER milestone (no prior), this becomes a reality reconciliation:
```markdown
## [{milestone_id}] VERIFY: Baseline Reality Audit

### Tasks
1. {agents.completion_audit}: audit all items marked DONE in the task-manager milestone — L — type:audit
2. {agents.spec_audit}: spec compliance sweep across all specs — L — type:audit
3. Fix critical lies/gaps found — variable — type:fix
4. Establish baseline acceptance test suite — M — type:test
```

### 3d.5: Gate Epic Selection — VERIFY-MECH / HARDEN / VERIFY-HUMAN

Not every milestone needs all three gate epics. Analyze the milestone scope and RECOMMEND
which gates apply. Present to Operator for approval via `AskUserQuestion`.

**Decision logic:**

| Milestone touches... | VERIFY-MECH | HARDEN | VERIFY-HUMAN |
|---------------------|-------------|--------|--------------|
| UI pages, dashboard, Next.js components | ✓ (full 30 modules) | ✓ | ✓ (full 30 flows) |
| Admin CRUD, forms, user-facing features | ✓ (full) | ✓ | ✓ (full) |
| API routes only (no UI) | ✓ (api-contracts, security modules only) | ✓ | ✗ skip |
| Data pipeline, ETL, dbt models | ✗ skip | ✓ (completion + spec audits) | ✗ skip |
| Schema migrations, RLS policies | ✓ (security + database-qa modules only) | ✓ | ✗ skip |
| Infrastructure, CI/CD, monitoring | ✗ skip | ✓ (completion audit only) | ✗ skip |
| Security hardening | ✓ (security modules only) | ✓ | ✗ skip |
| Mobile app | ✓ (cross-browser + responsive) | ✓ | ✓ (mobile-focused flows) |

**Ask the Operator:**

```
GATE EPIC RECOMMENDATION for {milestone_id}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

This milestone touches: {UI pages / data pipeline / security / etc.}

Recommended gates:
  ✓ VERIFY-MECH — {reason: "UI pages need Playwright testing across all roles"}
  ✓ HARDEN — {reason: "Always included — completion + spec audits"}
  ✓ VERIFY-HUMAN — {reason: "User-facing features need Operator walkthrough"}

  or for a data milestone:
  ✗ VERIFY-MECH — skip (no UI pages)
  ✓ HARDEN — completion + spec audits
  ✗ VERIFY-HUMAN — skip (no user-facing changes)
```

Use `AskUserQuestion`:
```
question: "Gate epics for {milestone_id}?"
options:
  - "All three (Recommended)" — VERIFY-MECH + HARDEN + VERIFY-HUMAN
  - "HARDEN + VERIFY-MECH only" — skip human walkthrough
  - "HARDEN only" — audits only, no Playwright
  - "Custom" — let me pick
```

If Operator picks "Custom", ask which modules/flows to include.

**If `CLAUDE_AUTO=1`:** use the recommendation without asking.

### 3e: Add VERIFY-MECH Epic (if selected in 3d.5)

Only add if Operator approved VERIFY-MECH in Step 3d.5. Runs `/verify mechanical`.

### 3f: Add Hardening Epic (ALWAYS included)

ALWAYS add after all feature epics. HARDEN runs the completion + spec audits (`{agents.completion_audit}` + `{agents.spec_audit}`). If VERIFY-MECH
exists, HARDEN depends on it. If no

…(truncated)
