| name | aiwfx-wrap-milestone |
| description | Closes an aiwf milestone — verifies all ACs met, runs scoped doc-lint, finalizes the milestone spec's wrap-side sections, promotes status to done, prepares the wrap commit. Use when the user says "wrap M-NNNN" or "finish the cache milestone" and the readiness check per `aiwfx-start-milestone` has passed. Commit and push require explicit human approval. |
aiwfx-wrap-milestone
Closes a milestone. Verifies completeness, finalizes the milestone spec's wrap-side sections, promotes the milestone to done, prepares the single wrap commit.
When to use
The milestone's implementation is complete and the readiness check has passed (aiwfx-start-milestone step 7 ran clean). The user says: "wrap M-NNNN", "finish the auth milestone", "close out the cache milestone".
If the milestone isn't actually done — failing tests, unmet ACs, broken build — stop and report. Don't paper over.
Workflow
1. Verify completion
- Re-read the milestone spec. Walk every AC in frontmatter
acs[]. Confirm each has at least one test that exercises it green.
- Run
aiwf show M-NNNN; confirm every AC's status is terminal (met, deferred, or cancelled) — none open. Under tdd: required, also confirm every met AC has tdd_phase: done (the kernel's acs-tdd-audit will surface it otherwise).
- Run
aiwf check. Zero error-severity findings on the milestone. The relevant codes: acs-shape, acs-tdd-audit, milestone-done-incomplete-acs, acs-body-coherence. Warnings (e.g. acs-body-coherence) are advisory but worth resolving before wrap.
- Run the full test suite. All pass.
- Run the project's build. Green.
- Run the project's full lint gate — the same linter set CI runs on push (e.g. a
make ci target), not a subset like go vet alone. Clean. Unpushed branches accumulate lint debt invisibly; the wrap is the cheap moment to catch it.
- Identify every gap this milestone's own body (Goal/Context prose, AC descriptions,
## References) explicitly names as something this work fixes or closes — distinct from a gap merely referenced for background, or one the spec discovered here and is deliberately leaving open (that one belongs in ## Deferrals, step 4, not here — promoting it would be wrong). For each fixed gap, confirm the implementing AC and its commit SHA from the ## Work log. These become the aiwf promote G-NNNN addressed --by-commit <sha> calls in step 13.
If anything is red, stop and report. Wrap does not paper over failure.
2. Independent two-lens review — before the wrap
This gates milestone closure, not the per-commit work: the implementation commits are already in, but the milestone is not yet wrapped, so there is still a chance to fix things inside the milestone. Findings become corrective commits on the milestone branch — before any AC flips to met and before the commit gate (step 7). The review feeds the human gate; it does not replace it.
Dispatch a fresh-context reviewer (a subagent with no authorship attachment) over the milestone's full change-set (git diff <base>..HEAD), briefed adversarially per wf-review-code §"Independence" (enumerate the load-bearing claims, instruct verify by measuring not reasoning, name the risk areas). Run two lenses:
- Code-quality (
wf-review-code): correctness, AC coverage, branch-coverage discipline, conventions, docs. For a large milestone, slice the review by concern or file group — one agent over thousands of lines goes shallow, the exact failure independence is meant to avoid.
- Design-quality (
wf-rethink): run on the design unit(s) the milestone introduced — those matching the wf-rethink trigger (a new module/package boundary, core abstraction, or data model; see wf-rethink §"The non-trivial-design trigger"). wf-rethink is per-unit by rule ("never run it over the whole codebase at once"), so name the unit(s) rather than pointing it at the whole diff. If the milestone introduced no such surface — only mechanical or local change — there is nothing to rethink; say so and move on.
Brief the reviewer to measure the change's shape before judging it. Every other check at wrap asks whether something is missing; none asks whether something is unnecessary, and a reviewer who never sees the size of what it reviews cannot tell whether the milestone spent more than it needed to. The reviewer derives these itself — numbers the author produced about their own work are what independence exists to replace:
git diff --numstat <base> HEAD
Then, in the project's own terms, naming the command used so the next reviewer can re-run it:
- Recurring obligation. Count the rules this milestone added — a lint rule, a CI check, a policy test, a required doc or template: anything a future change must satisfy. If that count is non-zero the answer cannot be "none"; name each one's owner and what retires it.
- Deletions. If nothing was retired from the test bucket, say what the milestone considered removing and why nothing qualified. "Nothing was redundant" is an answer; silence is not. Judge on what was genuinely retired — a modified line reads as a deletion and is not one.
- Same-outcome clusters. Group the new tests by the outcome their names claim — not by the test's kind, since a property or table suffix is not an outcome — and for any group of three or more, say whether its members fail for distinct reasons. Several spellings of one condition produce one test per noun. The reviewer decides this, not the author.
Handle the verdict: fix every blocking finding as a corrective commit on the milestone branch. A corrective commit for a defect carries the check that pins it — a test, a rule, a gate entry (wf-codebase-health D5); a defect you don't pin — because you can't, or because you judge it not worth the test — and any finding that reveals a requirement no AC covers, becomes a gap (aiwf add gap --title "..." --discovered-in M-NNNN) rather than a silent correction, with the resulting id mirrored under the spec's ## Deferrals (step 4) so it is reachable from the milestone and not only through --discovered-in. A judgment finding you accept becomes a written rule or a recorded decision (aiwfx-record-decision, mirrored under ## Decisions made during implementation); one you decline is recorded as a non-issue under ## Reviewer notes (step 4), so the next round's fresh reviewer meets a decision rather than a blank and does not re-raise it. Re-verify (re-run step 1's gates) if code changed; confirm judgment-level fixes by re-dispatching a fresh reviewer scoped to the changed surface (mechanical fixes can be confirmed mechanically — re-run the gate or scan).
Those scoped confirmations close a finding, not the review. The loop ends per wf-review-code §"Verdict" (its When the loop ends paragraph): the deciding pass is a fresh reviewer over the milestone's full change-set (git diff <base>..HEAD), never one narrowed to what the last fix touched. It may still be sliced by concern or file group as above — slicing for depth is fine, provided the slices together cover that change-set. Record the review outcome under the spec's ## Reviewer notes (step 4).
Then the residual self-checks — cheap, and not a substitute for the independent pass above:
- Skim for
TODO / FIXME left behind. If they're intentional, document them in the milestone spec's ## Reviewer notes section. If they're unintentional, fix or open as gaps (aiwf add gap --title "..." --discovered-in M-NNNN).
- Skim for debug code, commented-out blocks, scratch logging. Remove.
- Confirm public-API or schema changes are reflected in README, inline docs, or wherever the project publishes its surface.
3. Doc-lint sweep (scoped)
Invoke wf-doc-lint against the milestone's change-set (every file the milestone branch touched since diverging from its base). Surface the report inline.
If the report is clean, note "doc-lint: clean" and continue. If findings:
- Broken code references — fix in this milestone, or open a gap.
- Removed-feature docs — same.
- Orphan files / TODOs — record under the spec's
## Reviewer notes for the reviewer to consider; don't block wrap.
wf-doc-lint reports only — it does not rewrite prose. Any prose changes happen here as deliberate edits.
4. Finalize the milestone spec's wrap-side sections
The milestone spec itself carries the wrap-side sections; finalize them in place:
-
## Work log — confirm one entry per AC with the final outcome and commit SHA. The phase timeline is in aiwf history M-NNNN/AC-<N>; don't duplicate dates here.
-
## Decisions made during implementation — confirm every mid-flight decision is captured (each should already have an ADR-NNNN or D-NNN from aiwfx-record-decision invocations during work).
-
## Validation — paste the test-suite and build results.
-
## Deferrals — list any work this milestone deliberately punted. Before opening a gap for one, apply the cheap-fix test: if the change is small, lands in a file this milestone already touches, and is covered by a test you are already writing, make it now as a corrective commit on the milestone branch — the same route step 2's review fixes take, so it lands before the wrap commit rather than dirtying it — then record it under ## Reviewer notes. If it touched source or tests, re-run step 1's gates and re-enter step 2's scoped confirmation — a fix landing after the deciding review is still code no reviewer has seen. A gap is for work that needs its own branch, its own review, or a decision you are not ready to make. For each deferral that survives the test, open a gap entity so it survives:
aiwf add gap --title "<deferred-work>" --discovered-in M-NNNN
Then mirror the resulting G-NNNN id here. Deferred ACs (status deferred) get a one-line note pointing at the receiving milestone or gap.
-
## Reviewer notes — trade-offs, deliberate omissions, places where the obvious approach was rejected. The reviewer agent reads this first.
For ACs that were cancelled mid-implementation, link to the D-NNN decision (or the conversation context) explaining why under the cancelled AC's body section. The kernel only guards the structural state (status: cancelled, position-stable in acs[]); the why is the human's narrative.
5. Update the roadmap
aiwf render roadmap --write
--write only rewrites ROADMAP.md on disk — it does not commit. It rides into the wrap commit below (step 6) alongside the milestone spec, so there is no separate roadmap commit and no separate gate for it. This render still reflects the milestone as in_progress (the promote to done hasn't happened yet) — that's expected, not a bug: step 10's declared-sequence gate regenerates and commits it again once done actually lands, superseding this one. Keep both: dropping either reintroduces a dirty-tree or stray-uncommitted-file problem.
6. Stage all changes and prepare the wrap commit
The implementation is already committed, per-AC, from aiwfx-start-milestone step 6 — this step does not bundle any source or test files. The milestone spec carries all the wrap-side prose now (Work log, Validation, Deferrals, Reviewer notes); that, plus the regenerated roadmap from step 5, is what's left to stage:
git add work/epics/E-NNNN-<slug>/M-NNNN-<slug>.md ROADMAP.md
git status
git diff --staged --stat
(git add on an unchanged ROADMAP.md is a no-op — step 5 already skipped the write when content was unchanged, so nothing spurious lands here.)
Draft a conventional commit message: feat(<scope>): <one-line summary> (M-NNNN).
7. 🛑 Commit gate
Show the user:
git diff --staged --stat
- The proposed commit message.
- A summary of what landed: AC count green, doc-lint summary, deferrals opened (with gap ids), and a pointer to the per-AC implementation commits already on the branch (their SHAs are in the Work log — this commit adds no source or test files, only the wrap-side spec prose).
Stop and wait for explicit "commit" approval.
8. After commit approval
The wrap commit touches a milestone entity file (work/epics/E-NNNN-<slug>/M-NNNN-<slug>.md), so it carries the three required trailers — aiwf-verb: wrap-milestone, aiwf-entity: M-NNNN, aiwf-actor: human/<id>. Skipping any one of them trips the kernel's provenance-untrailered-entity-commit finding on the file touch. Parallel shape to aiwfx-wrap-epic's trailered merge + wrap-artefact commits:
git commit -m "<approved-message>" \
--trailer "aiwf-verb: wrap-milestone" \
--trailer "aiwf-entity: M-NNNN" \
--trailer "aiwf-actor: human/<id>"
The trailer keys are quoted from CLAUDE.md §"Commit conventions" verbatim — variant casings (e.g. Aiwf-Verb) fail the kernel's trailer-keys policy.
9. 🛑 Push gate
Push is an outward, irreversible action — it stands as its own gate, never folded into the declared-sequence gate below. Confirm with the user before pushing. Then:
git push -u origin milestone/M-NNNN-<slug>
Open the PR if the project's flow is PR-driven. Reference the milestone id in the PR title.
10. 🛑 Declared-sequence gate — close the milestone (terminal local sequence)
This is the milestone's terminal sequence of local, reversible mutations. Per CLAUDE.md's gate-discipline section, present it as a single declared-sequence gate that enumerates every action verbatim; the user may approve a subset ("all except the promote"), and any deviation (a merge conflict, a check finding, unexpected dirty state) aborts the sequence and re-gates from the point of deviation. Excluded from this gate: the push (step 9, outward) and any origin-branch delete (outward) — those stand as their own gates and are never batched here.
The enumerated local sequence is merge → promote-done → roadmap regen → local cleanup:
- Merge the milestone branch into the epic branch, with a trailered merge commit (step 12).
- Promote the milestone to
done — closing any gaps step 1 identified this milestone fixes first (step 13).
- Roadmap regen — regenerate
ROADMAP.md now that the milestone shows done (step 14).
- Local cleanup — delete the local milestone branch and its worktree, if one was used (step 15).
Once the sequence is approved, execute it in order:
11. Reconcile the milestone branch with the epic branch
Run this immediately before the merge — not as an earlier precondition. The epic branch is this merge's integration target.
-
Check whether the epic branch has advanced past the milestone branch's fork point:
git merge-base --is-ancestor epic/E-NNNN-<slug> milestone/M-NNNN-<slug>
-
If that check fails: the epic branch carries commits the milestone branch doesn't. Integrate the epic branch into the milestone branch, resolve any conflicts there, and re-run the full local CI gate (step 1) on the reconciled milestone branch.
-
Only once the check passes does the merge (step 12) run.
12. Merge the milestone branch into the epic branch with a trailered merge commit
If the project uses an epic-integration branch, follow the same pattern as aiwfx-wrap-epic's epic-into-trunk merge: stage the merge without committing so the merge commit's trailer set can be attached explicitly.
git checkout epic/E-NNNN-<slug>
git merge --no-ff --no-commit milestone/M-NNNN-<slug>
--no-ff preserves the milestone as a single merge commit (rather than fast-forwarding individual milestone commits into the epic). --no-commit leaves the merge staged so the commit-emitting step is the one carrying trailers — without it, git produces an untrailered merge commit and the kernel's trailer-verb-unknown warning fires (the operator's hand-typed aiwf-verb: merge is a fabrication; merge is a git concept, not a recognized ritual or kernel verb).
Resolve the operator identity from git config user.email (per CLAUDE.md Provenance model §"Identity is runtime-derived"); do not hardcode <id>. Then commit with the three required trailers and a Conventional Commits subject:
git commit -m "chore(milestone): wrap M-NNNN — <milestone title>" \
--trailer "aiwf-verb: wrap-milestone" \
--trailer "aiwf-entity: M-NNNN" \
--trailer "aiwf-actor: human/<id>"
The trailer keys are quoted from CLAUDE.md §"Commit conventions" verbatim — aiwf-verb, aiwf-entity, aiwf-actor. Variant casings (e.g. Aiwf-Verb) fail the kernel's trailer-keys policy. The aiwf-verb: wrap-milestone value names the ritual that produced the commit; the kernel's trailer-verb-unknown rule recognizes it via the ritualVerbs allowlist (sourced from the embedded ritual snapshot), mirroring aiwfx-wrap-epic's aiwf-verb: wrap-epic trailer at the equivalent step.
Why an aiwf-verb trailer on a git merge commit. The merge IS a kernel-meaningful structural transition (the milestone's work joins the epic's history); aiwf-verb: wrap-milestone records the ritual that produced it, not the underlying git operation. Do NOT write aiwf-verb: merge — merge is neither a Cobra verb nor an allowlisted ritual value; the commit-msg git hook materialized by aiwf init / aiwf update (the primary chokepoint) refuses the commit at message-composition time with a named-value error pointing at the canonical aiwf-verb: wrap-milestone shape. Historical commits authored before the hook landed are still surfaced by the trailer-verb-unknown rule at pre-push, with two cleanup paths (aiwf acknowledge illegal <sha> or push the warning forward, since amend is blocked by the trunk-aware push model).
Record the resulting merge commit SHA wherever the project tracks merge history (the milestone's ## Work log section is the natural place).
13. Promote the milestone to done
If step 1 identified any gap this milestone's own body explicitly claims to fix, close each one first — before the milestone's own promote-to-done step below, which ends a delegated milestone's authorize scope. A verb-driven commit produced after that point risks an ended-scope aiwf-authorized-by: trailer on push (the same hazard aiwfx-wrap-epic's promote-last ordering exists to avoid, applied here to the reverse ordering problem: gap closure must come before the scope-ending act, not after it):
aiwf promote G-NNNN addressed --by-commit <sha>
Cite the AC's own implementation commit, not the merge commit — --by-commit is mechanically guarded (aiwf refuses a SHA unreachable from HEAD), so this only works after the merge (step 12) has landed. Skip entirely if the milestone claims no gap.
Then promote the milestone itself:
aiwf promote M-NNNN done
aiwf validates in_progress → done, rewrites frontmatter, and commits with aiwf-verb: promote trailers. This is the moment of closure — the last status-flip commit in the sequence, landing after the merge (and after any gap closures) so a delegated milestone's authorize scope is still live for both.
14. Regenerate the roadmap
Now that the milestone's status has actually landed as done (step 5's render ran before this promotion, so it's stale the moment promote-done commits):
aiwf render roadmap --write
--write only rewrites the file on disk — it never commits. Landing this after promote-done is safe despite promote being "the last status-flip commit": the regen commit below is hand-composed via plain git commit, never routed through the CLI's scope-lookup/trailer-decoration path, so it cannot pick up an ended-scope aiwf-authorized-by: trailer regardless of position (unlike a kernel-verb commit, which would). If the content changed, stage and commit it as its own small step in this same declared sequence, with the ritual's trailers (mirroring the merge commit's hand-composed trailer set above):
git add ROADMAP.md
git commit -m "docs(roadmap): regenerate after M-NNNN wrap" \
--trailer "aiwf-verb: wrap-milestone" \
--trailer "aiwf-entity: M-NNNN" \
--trailer "aiwf-actor: human/<id>"
If aiwf render roadmap --write reported the file already up to date, skip the git add/git commit — there is nothing to stage.
15. Local cleanup
Delete the local milestone branch (and its worktree, if one was used):
git branch -d milestone/M-NNNN-<slug>
These are local and reversible, so they belong inside the gate above.
16. Origin cleanup
After the declared-sequence gate, finish up. The origin-branch delete is an outward action — its own gate, never batched into step 10:
- Delete the milestone branch on origin (
git push origin --delete milestone/M-NNNN-<slug>) if the branch/PR flow created one — outward, its own gate.
Constraints
- 🛑 Never commit or push without explicit human approval — the commit gate (step 7) and the push gate (step 9) are separate human gates.
- 🛑 The terminal local sequence — local merge, promote-done (including any gap tracker closure it triggers), roadmap regen, local cleanup — runs under one declared-sequence gate (step 10), enumerated verbatim and subset-approvable. Push and any origin-branch delete are outward and excluded; they keep their own gates.
- All ACs must be green before wrap proceeds. Wrap does not bury failure.
- Branch-coverage hard rule applies — re-run the audit if any code changed since
aiwfx-start-milestone's readiness check.
- Deferrals that survive the cheap-fix test must be captured as gaps. Don't leave deferred work as a
## Deferrals bullet that nothing else points at — and don't file a gap for a change you could have made inline while you were already in the file.
- A gap this milestone's own body claims to fix must be closed at wrap (step 13) —
aiwf promote G-NNNN addressed --by-commit <sha>, before the milestone's own promote-done. Don't leave the tracker silently overstating what's still open.
Anti-patterns
- Wrapping with red tests. Either fix the tests, escalate the AC failure, or cancel the milestone (
aiwf cancel M-NNNN). Don't wrap broken work as done.
- Wrapping with open ACs. The kernel's
milestone-done-incomplete-acs finding will fire — --force lands the verb but leaves the standing check red. Resolve every AC to a terminal state (met/deferred/cancelled) before wrap.
- Silent deferrals. Every "we'll do that later" that survives the cheap-fix test gets a gap entity.
- Ledger padding. A gap opened and closed inside the same wrap is a fix that should have been made inline. Filing it costs a title, a body, and a reader's attention that the fix itself would not have.
- Skipping doc-lint. Doc drift compounds; the milestone wrap is the cheap moment to catch it.
- Slipping unrelated code into the wrap commit. If the change isn't part of this milestone, it's a separate
wf-patch.
- Wrapping without checking whether the milestone's own prose claims to fix a gap. A milestone whose own body names a gap as what it fixes, wrapped without closing that gap, leaves the tracker silently overstating what's still open.
- Promoting a claimed-fixed gap after the milestone's own promote-done. Ordering matters (step 13) — closing gaps after
aiwf promote M-NNNN done risks an ended-scope aiwf-authorized-by: trailer on the gap-promote commit.
Next step
The milestone close is a natural /compact point — invoke aiwfx-handoff to emit a prime block before moving on.
If this is the last milestone in the epic: → aiwfx-wrap-epic E-NNNN.
Otherwise: → aiwfx-start-milestone <next-M>.