| name | aiwfx-wrap-epic |
| description | Closes an aiwf epic โ verifies all milestones done, scaffolds a wrap artefact, harvests ADR candidates, runs scoped doc-lint, merges the epic branch into mainline with a trailered merge commit, promotes the epic to done. Use when the user says "wrap E-NN" or "close the auth epic" and every milestone in the epic is wrapped. Commit and push require explicit human approval. |
aiwfx-wrap-epic
Closes an epic. The epic itself is a coordination unit โ closing it means: every milestone is done, the integration branch merges to mainline via a trailered merge commit, the wrap artefact captures what shipped and what didn't, and the epic's status flips to done.
Principles
- Wrap is closure, not release. Tagging, packaging, publishing โ those are
aiwfx-release. This skill ends the planning unit.
- Branch cleanup is opt-in. Local branches are preserved (so
tig / gitk keep labelling history); origin branches for completed milestones are deleted to reduce remote refname clutter.
- Nothing is deleted at wrap. Specs (with their work-log sections), the wrap artefact โ all stay readable forever. Closure is a status change, not a deletion.
- The merge commit is trailered. The integration-target merge commit carries
aiwf-verb: wrap-epic, aiwf-entity: E-NNNN, aiwf-actor: human/<id> trailers โ exactly the keys the kernel's provenance-untrailered-entity-commit finding expects. Without the trailers, the rule fires once per entity file touched by the merge.
Precondition
- Every milestone in this epic has
status: done. Run aiwf check and verify; if any are still in_progress or draft, stop and surface them.
- The epic branch (if used) is up to date โ every milestone's final merge commit is on it.
- Working tree clean.
- Integration target identified (usually
main).
- The project's full local CI gate is green on the epic branch after integrating current mainline โ the same checks CI runs on push (e.g. a
make ci target), not a subset. A gate run that predates mainline's latest commits is green on a tree that omits them; it doesn't cover the branch that's about to merge. See "Reconcile the epic branch with mainline" below for the integrate-then-gate mechanics. Long-lived epic branches accumulate lint debt invisibly across milestone wraps; the merge to mainline is the last local moment to catch it. (If the last green run of that gate predates only frontmatter commits โ e.g. milestone promotes, which touch no Go/build inputs โ it is still valid; re-run it only when Go/build inputs changed since. Don't re-run a still-green gate.)
- Neither the epic's own spec nor any milestone's left a gap open that it explicitly claims to fix.
aiwfx-wrap-milestone's own wrap step should already have closed the milestone-level ones; this is the backstop for a milestone wrapped under an older ritual version, or one closed outside the ritual. Nothing checks the epic's own spec earlier.
If precondition 1โ5 fails, stop and report. Do not improvise around an unfinished epic. Precondition 6 is not a stop condition โ the epic itself is otherwise ready โ but a disposition to settle first: if a claimed-fixed gap surfaces still open, either close it (aiwf promote G-NNNN addressed --by-commit <sha>, citing the implementing commit), or, when the work advanced it without finishing it, correct the claim to say what landed and what remains. Don't close a partly-addressed gap to satisfy the check. Silence is the forbidden outcome: don't let it become a bare ## Follow-ups carried forward entry instead.
One-time setup (per consumer repo)
wrap.md is an extension artefact, not a kernel-recognized entity file. The aiwf kernel's aiwf check enforces a closed tree shape under work/ and will flag wrap.md as unexpected-tree-file unless it's whitelisted. Add this once to the consumer repo's aiwf.yaml:
tree:
allow_paths:
- "work/epics/E-*/wrap.md"
If you skip this, the first aiwf check after step 7 will warn (or, under tree.strict: true, error). Add the entry before staging the wrap artefact.
Workflow
1. Scaffold the wrap artefact
Create work/epics/E-NN-<slug>/wrap.md (staged, not yet committed):
# Epic wrap โ E-NN
**Date:** <today>
**Closed by:** <actor>
**Integration target:** main
**Epic branch:** epic/E-NN-<slug>
**Merge commit:** <SHA โ filled at step 6>
## Milestones delivered
- M-NNN โ <title> (merged <short-sha>)
- M-NNN โ <title> (merged <short-sha>)
## Summary
Two to four sentences on what shipped and why. Reference the goal from the epic spec; honest about what scope shifted mid-flight.
## ADRs ratified
- ADR-NNNN โ <slug> (or "none")
## Decisions captured
- D-NNNN โ <slug> (or "none")
## Follow-ups carried forward
- G-NNN โ <slug> (gap that survives the epic)
## Handoff
What is ready for the next epic; what is deliberately left open.
Use reference-phrasing for any list-derived count ("every ADR listed in ADRs ratified" rather than "all 4 ADRs"). Avoids drift.
2. ADR check โ harvest decisions worth keeping
Walk the epic's commits. For each candidate decision, ask: "Would a future reader regret missing the reasoning?" Signals an ADR is warranted:
- A default changed or a new default introduced.
- A strategy considered and rejected.
- A scope cut or framing shift affecting downstream work.
- A supersession of a prior ADR.
For each candidate, invoke aiwfx-record-decision and choose ADR (architectural, durable) or D-NNNN (project-scoped, more local). Record the resulting ids in the wrap artefact's ## ADRs ratified or ## Decisions captured section.
3. Doc-lint sweep (scoped)
Invoke wf-doc-lint against the epic's change-set (every file touched on epic/E-NN-<slug> since it diverged from the integration target).
Append the report to wrap.md under a ## Doc findings section. If findings include broken references or removed-feature docs, fix or open as gaps before proceeding. wf-doc-lint reports only โ prose fixes are deliberate edits here.
4. ๐ Declared-sequence gate โ close the epic (terminal local sequence)
This is the epic'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 10) and the origin-branch deletes (step 11) โ those are outward and stand as their own gates, never batched here.
The enumerated local sequence is merge โ wrap-artefact commit โ promote-done โ roadmap regen:
- Merge the epic branch into the integration target with a trailered merge commit (step 6).
- Wrap-artefact commit โ the CHANGELOG
[Unreleased] entry + wrap.md, trailered (step 7).
- Promote the epic to
done โ status-flip commit (step 8).
- Roadmap regen โ regenerate
ROADMAP.md now that the epic shows done (step 9), committed before the push gate (step 10).
Once the sequence is approved, execute it:
5. Reconcile the epic branch with mainline
Run this immediately before the merge โ not as an earlier precondition a concurrent push can invalidate. The target is your local mainline (the branch the epic merges into), not the remote-tracking ref.
-
Fetch, then fast-forward local mainline to its upstream โ folds in commits another clone pushed; concurrent local commits are already on it (substitute your mainline branch and remote; a project with no remote skips this step):
git fetch
git checkout main && git merge --ff-only origin/main
-
Check whether mainline has advanced past the epic branch's fork point (substitute the project's mainline ref):
git merge-base --is-ancestor main epic/E-NN-<slug>
-
If that check fails: integrate mainline into the epic branch, resolve any conflicts there, and re-run the project's full local CI gate on the reconciled epic branch. Mainline can move again during that gate, so re-run this check immediately before merging.
-
Only once the check passes does the merge (step 6) run.
6. Merge epic branch into integration target with a trailered merge commit
git checkout main
git pull --ff-only origin main
Stage the merge without committing so the next step can attach the required trailers explicitly:
git merge --no-ff --no-commit epic/E-NN-<slug>
--no-ff preserves the epic as a single merge commit; --no-commit leaves the merge staged so the commit-emitting step is the one carrying trailers. Without --no-commit, git produces an untrailered merge commit and the kernel's provenance-untrailered-entity-commit rule fires once per entity file touched by the merge.
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(epic): wrap E-NNNN โ <epic title>" \
--trailer "aiwf-verb: wrap-epic" \
--trailer "aiwf-entity: E-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. Record the resulting merge SHA in wrap.md.
Do not push yet.
7. Wrap-artefact commit โ CHANGELOG [Unreleased] + wrap.md
The [Unreleased] section of CHANGELOG.md is a per-epic accumulator: every wrapped epic adds an entry here, and aiwfx-release later rolls the accumulated entries into a versioned ## [X.Y.Z] heading. Without this step, releases ship with empty changelog entries โ that's the [Unreleased] drift this step prevents.
Edit CHANGELOG.md to add a new sub-section under ## [Unreleased]. Use a Keep-a-Changelog category as the heading: ### Added โ E-NN: <one-line summary>, ### Changed โ E-NN: <one-line summary>, or ### Fixed โ E-NN: <one-line summary> as appropriate. The body is a short paragraph (or bulleted milestone list, like prior epic entries in the file) summarising the user-visible delta: gaps closed, verbs added, behaviour changes, doctrine landed in CLAUDE.md. Internal refactors with no observable delta can be omitted; if everything is internal, a single line saying so still goes in (releases require some entry per the changelog-check workflow).
The wrap.md file already captures the structured detail (milestones, ADRs, gaps); the CHANGELOG entry distils it for a release reader who has not been following along. Reference-phrasing is fine ("every milestone listed in wrap.md โฆ") to avoid drift between the two documents.
Then stage and commit the wrap artefact. Its message and trailers were approved as part of the declared-sequence gate (step 4) โ there is no separate commit gate. The commit sits on top of the trailered merge and carries the same three trailer keys, so aiwf history E-NNNN surfaces it alongside the merge:
git add CHANGELOG.md
git add work/epics/E-NN-<slug>/wrap.md
git commit -m "chore(E-NN): wrap epic โ <one-line summary>" \
--trailer "aiwf-verb: wrap-epic" \
--trailer "aiwf-entity: E-NNNN" \
--trailer "aiwf-actor: human/<id>"
8. Promote the epic to done โ last entity-mutating commit in the bundle
aiwf promote E-NN done
aiwf validates active โ done, rewrites frontmatter, commits with aiwf-verb: promote. (If the epic is still proposed, that means no milestone ever started โ wrap doesn't apply. Investigate.)
Why promote is last among entity-mutating commits. The aiwf promote E-NN done commit ends the authorize scope that opened with aiwfx-start-epic. Any commit produced after this that goes through a kernel verb โ wrap artefact, CHANGELOG entry, reallocates, or other verb-driven wrap-bundle commits โ would carry aiwf-authorized-by: referencing the just-ended scope and trigger the kernel's provenance-authorization-ended finding on push, blocking the wrap with no clean remediation short of --no-verify or history rewrite. Keeping aiwf promote E-NN done as the last verb-driven commit in the wrap bundle guarantees every other verb commit lives under the live scope. The one exception is step 9's roadmap-regen commit: it's hand-composed via plain git commit (never routed through the CLI's scope-lookup/trailer-decoration path), so it can never receive an auto-stamped aiwf-authorized-by โ safe to land after promote-done, which is also the only point in the sequence where the roadmap can reflect the epic's actual final done state.
The completion date is recorded in wrap.md (step 1) and is recoverable from the aiwf-verb: promote commit via aiwf history E-NN. Do not add a completed: field to the epic frontmatter โ aiwf's epic schema does not include it, and the parse failure cascades into unresolved-reference findings on every entity that links to this epic.
9. Regenerate the roadmap
aiwf render roadmap --write
--write only rewrites ROADMAP.md on disk โ it never commits. Now that the epic shows done, stage and commit any resulting change as its own small step, with the same trailer set as the rest of the bundle:
git add ROADMAP.md
git commit -m "docs(roadmap): regenerate after E-NN wrap" \
--trailer "aiwf-verb: wrap-epic" \
--trailer "aiwf-entity: E-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. This step still runs inside the declared-sequence gate from step 4 (item 4) โ no separate approval, and no --no-ff merge output to reconcile since it's a plain file regen.
10. ๐ Push gate
Push is outward and irreversible โ its own gate, never part of the declared-sequence gate above. Confirm. Then:
git push origin main
11. ๐ Origin branch cleanup โ one gate per delete
Plan the deletions first. List the milestone and epic branches to delete. For each, verify it's merged:
git branch -r --merged main | grep "milestone/M-NNN"
git branch -r --merged main | grep "epic/E-NN"
If a branch isn't shown as merged, stop and report โ don't force.
Each git push origin --delete is an outward, irreversible action โ its own gate. Confirm per branch and delete one at a time; never batch-approve the list (a batched delete removes per-action judgment on irreversible remote refs). Local branches are not touched (operators prune those on their own schedule):
git push origin --delete milestone/M-NNN-<slug>
git push origin --delete epic/E-NN-<slug>
Constraints
- ๐ The terminal local sequence โ merge, wrap-artefact commit, promote-done, roadmap regen โ runs under one declared-sequence gate (step 4), enumerated verbatim and subset-approvable. The push (step 10) and each origin-branch delete (step 11) are outward and keep their own gates; never batch them.
- ๐ The merge commit and the wrap-artefact commit both carry the three required trailers. Skipping either is the regression the kernel's
provenance-untrailered-entity-commit finding catches.
- ๐
aiwf promote E-NN done is the last verb-driven commit in the bundle (step 8). It ends the active authorize scope; any commit produced after it that routes through a kernel verb carries an ended-scope aiwf-authorized-by: and fails the kernel's provenance-authorization-ended check on push. The step-9 roadmap-regen commit is the sole, deliberate exception โ hand-composed via plain git commit, never routed through the CLI's scope-lookup path, so it cannot receive that trailer regardless of position.
- ๐ Mainline is reconciled into the epic branch before the merge (step 5), not resolved on mainline mid-merge. After fetching and fast-forwarding local
main, if git merge-base --is-ancestor main epic/E-NN-<slug> is false, integrate mainline into the epic branch, resolve conflicts, and re-run the full local gate there first.
- Every milestone must be
done before wrap โ aiwf check and aiwf history E-NN confirm.
- Branch-cleanup is origin-only. Do not delete local branches.
- The wrap artefact is mandatory. Don't close an epic without one.
Anti-patterns
- Wrapping while a milestone is still
in_progress. Run aiwf check first.
- Force-deleting an unmerged branch. Reconcile the work or the name; don't force.
- Slipping a code change into the wrap commit. If the change is real, it's a milestone or a
wf-patch.
- Skipping the ADR harvest. The window to record "why we did it this way" closes when the team forgets.
- Pushing before approval.
- Merging without
--no-commit. Produces an untrailered merge commit; the kernel rule fires once per entity file touched.
- Hardcoding
<id> in the actor trailer. Resolve from git config user.email at run time per the provenance model.
- Promoting the epic to
done before the wrap-artefact and other verb-driven wrap-bundle commits. Ends the authorize scope mid-bundle; subsequent verb-driven commits carry an ended-scope aiwf-authorized-by: and fail provenance-authorization-ended on push. Promote is step 8, after the wrap-artefact commit โ the "Why promote is last among entity-mutating commits" section above explains why (and why the step-9 roadmap regen is the one safe exception).
- Resolving a mainline conflict on mainline itself, mid-merge. If mainline has advanced past the epic branch's fork point, reconcile on the epic branch (step 5) and re-run the gate there โ mainline only ever receives an already-validated result.
Out of scope
Version-tag cuts, the [Unreleased] โ [X.Y.Z] rename, package publishing, and deployment. Those belong to aiwfx-release.
Note: Adding the per-epic entry under ## [Unreleased] in CHANGELOG.md is in scope for this skill (step 7). The [Unreleased] heading is the per-epic accumulator; aiwfx-release only rolls the accumulated entries forward when cutting a version. Skipping the CHANGELOG-update step at wrap is the failure mode that produces empty release notes โ this skill owns prevention.
Next step
If a release follows: โ aiwfx-release.
If not: โ aiwfx-plan-epic for whatever's next.