Factory mode: If
CLAUDE_AUTOis 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.mdfor 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:
^(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 usetask_prefix— ticket prefix (e.g.APP,BEN,WEB)task_team_id— team/project identifiermilestone_noun— what this project calls milestonesmain_branch— PR target branchareas— 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_qaname the namespaces (defaultsintel/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-activeexists. This skill creates that flag on entry and removes it on exit.Step 0b (MANDATORY — before any planning):
touch.claude/.plan-milestone-activeFinal step (MANDATORY — after all issues created):
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
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:
- "What does 'done' look like for this milestone — what can the Operator do that they can't do today?"
- "Are there any scope items that should NOT be included, even if they seem related?"
- "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)
HONESTY_MODEenv var (non-interactive fallback for autonomous runs).- The Operator's
AskUserQuestionchoice in this step. resolve_honesty_mode({milestone_id})fromscripts.factory.lifecycle_helpers— applies theper_milestone_overridesglobs declared in.claude/project-context.md.
Procedure
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:
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:
# 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:
<!-- 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
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.
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.
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):
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}.mdexists 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:
## {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):
Ollama (local, free) — try
codestral:22bordeepseek-coder-v2first:ollama list 2>/dev/null | grep -E "codestral|deepseek-coder|qwen2.5-coder" | head -3If a suitable model is available:
# 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}.pyReview the output. If sensible, copy to the correct test path (see naming below). If garbage, fall back.
Codex (if available) — same brief, same isolation constraint. Codex does NOT see Claude's plan.
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}")— NOTpytest.skip.skiphides the test.NotImplementedErrorshows the gap. - Has a docstring stating which Exit Criterion it proves
# 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
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:
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:
# 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):
# 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:
# 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:
## [{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:
## [{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:
## [{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)