| name | work-end |
| description | Use when the current branch is complete and ready to close — user says "work-end", "close this branch", or "wrap up this issue". Must be invoked from the working branch, not main. Replaces "epic close".
|
work-end
Closes the current branch cleanly. Promotes artifacts, merges the journal,
closes the issue, rebases the project branch onto the project base branch, marks the
branch closed, returns to the workspace base (main).
**Code review is mandatory before any push or PR.** You MUST invoke the
`code-review` skill on the branch diff before Step 8j (push/PR). There are
NO exempt branches — not "mechanical" changes, not "tests already pass",
not "small diff". The review catches what you missed. Skipping it is the #1
failure mode of this skill.
Doc sync is mandatory. update-claude-md and implementation-doc-sync
are part of the pre-close sweep and default to ON. They catch convention drift
and stale documentation that compounds across sessions.
Main-branch mutations go through work-end only. Never run
git checkout main && git merge <branch> manually — this bypasses
pull-before-merge, squash-before-push, and fork-first delivery. If work
needs to land on main, use work-end on the branch. There is no safe shortcut.
The pre-push hook blocks diverged pushes, but prevention is better than detection.
Red Flags — thoughts that mean STOP
| Thought | Reality |
|---|
| "This branch was mechanical" | Mechanical changes have mechanical bugs. Review catches them. |
| "Tests passed, it's fine" | Tests verify behaviour, not code quality or spec compliance. |
| "The diff is small" | Small diffs have the highest bug-per-line ratio. |
| "I'll review after merging" | Post-merge review is post-incident review. |
| "Doc sync has nothing to sync" | Run it and let the skill decide. Your guess is often wrong. |
| "CLAUDE.md hasn't changed" | Conventions established during implementation need to be captured. |
Path Resolution (run first, always)
Run the bundled context script — installed, version-controlled, no hardcoded paths:
python3 ~/.claude/skills/project-init/ctx.py
Never write a script to /tmp/ for path resolution. /tmp/ is shared across sessions — a stale script from a previous session in a different project will silently return the wrong workspace and project paths, contaminating the entire close operation.
Use the printed values as concrete strings in ALL subsequent commands.
Never re-assign to shell variables. Replace every <WORKSPACE>, <PROJECT>, <BRANCH_NAME>,
<PROJECT_SHA>, <ISSUE_N>, <COVERS>, <OWNER_REPO>, <BASE_BRANCH> placeholder
with the actual value from the script output.
Pre-conditions
Run python3 ~/.claude/skills/project-init/ctx.py first. Use CURRENT_BRANCH from its output. Check in order:
-
If $WORKSPACE/design/.pause-stack exists and has entries — check whether
the target branch is in the stack:
- Current branch is in the stack (ending a paused branch without resuming it):
allowed. After all close steps complete, remove this branch from the stack
(Step 9 will handle it — see "Stack cleanup on end" below).
- Current branch is NOT in the stack but stack is non-empty: inform the user
the stack has N other paused branches. Continue — this is normal when ending
the active branch while others are paused.
-
$WORKSPACE/design/.meta must exist on the current branch → proceed.
-
If $WORKSPACE/design/.meta exists but $CURRENT_WORKSPACE == main (orphaned)
→ hard stop. Offer to switch to the surviving branch and close from there, or discard.
-
Workspace must have a clean working tree — run before any other work:
git -C "$WORKSPACE" status --short
If any output appears, hard stop:
"Workspace has uncommitted changes on $BRANCH_NAME. Commit or discard them
before running work-end — stash is not used in this workflow."
Do not proceed until the working tree is clean. Never stash automatically.
-
Project base branch must have a clean working tree — run before any other work:
git -C "$PROJECT" status --short
git -C "$PROJECT" log "$PROJECT_BASE_BRANCH"..origin/"$PROJECT_BASE_BRANCH" --oneline
- If
git status --short has output → hard stop:
"⚠️ Project $PROJECT_BASE_BRANCH has staged or unstaged changes — a previous operation
was left incomplete. Resolve before closing this branch."
- If remote is ahead of local → warn (non-blocking):
"⚠️ Remote $PROJECT_BASE_BRANCH is ahead of local — rebase before landing this branch's work."
Do NOT check whether local is ahead of remote. At work-end, local $PROJECT_BASE_BRANCH
will naturally be ahead once the branch is rebased onto it (step 8j). The mandatory fork push
in step 8j is the mechanism that ensures work is preserved — not a pre-condition check.
Checking "local ahead of remote → hard stop" would always fire incorrectly at work-end.
Step 0 + Step 1 — Context (resolved by Path Resolution script)
All values — WORKSPACE, PROJECT, OWNER_REPO, BASE_BRANCH, BRANCH_NAME,
PROJECT_SHA, ISSUE_N, ISSUE_REPO, COVERS — come from ctx.py output.
Do not re-extract them with shell commands.
COVERS is a comma-separated list of all issue numbers this branch closes (e.g. "5,19,32,24").
When the branch was started for a single issue, COVERS equals ISSUE_N. When absent from
.meta (branches created before this feature), COVERS defaults to ISSUE_N.
Branch summary — always print before proceeding
Immediately after running the Path Resolution script, print a summary using concrete values from its output — one command per line, no shell variables:
gh issue view <ISSUE_N> --repo <ISSUE_REPO> --json title --jq '.title' 2>/dev/null
git -C <PROJECT> log --oneline <BASE_BRANCH>..<BRANCH_NAME> 2>/dev/null
git -C <PROJECT> diff --shortstat <PROJECT_SHA>..HEAD 2>/dev/null
grep "^### " <WORKSPACE>/design/JOURNAL.md 2>/dev/null | wc -l
Output format:
╔══ Branch Summary ═══════════════════════════════════╗
║ Branch: <branch-name>
║ Issue: #<N> — <title> (primary)
║ Covers: #<N>, #<M>, #<P> ← omit if COVERS == ISSUE_N (single issue)
║ Started: <date from .meta>
║
║ Commits (<N>):
║ <git log --oneline output, one per line>
║
║ Changed: <N files, N insertions, N deletions>
║ Journal: <N entries> (or: no journal)
╚═════════════════════════════════════════════════════╝
If the issue title cannot be fetched (no network, no tracking), omit that line.
If $COVERS contains more than one issue, fetch and display each title.
If no commits are found on the branch (work landed directly on base), note that.
This summary is informational only — it does not block the close and requires no user input.
Step 2 — Flyway V re-scan
Re-scan at close time — another branch may have claimed the same V numbers since
branch creation.
git -C "$PROJECT" fetch --all 2>/dev/null || echo "⚠️ No network — scan skipped"
If conflict detected: offer [R] renumber affected migration files, [A] abort.
Block close until resolved.
Step 3 — Resolve routing and set DESIGN_REPO
Read three-layer routing cascade for each artifact type. Warn on deprecated
vocabulary (base, project repo, design-journal). Show resolved table;
user confirms before proceeding.
Capability detection — for each resolved destination:
detect_capability() {
local dest="$1"
if [ -d "$dest/.git" ]; then
git -C "$dest" remote get-url origin &>/dev/null 2>&1 && echo "remote-git" || echo "local-git"
else
echo "filesystem"
fi
}
Specs routing — check the CLAUDE.md Routing table for a specs row. If present,
honour it (workspace or project). If absent, default to project ($PROJECT/docs/specs/).
Unlike earlier skill versions, specs routing IS configurable — projects that keep all
methodology artifacts in the workspace should declare specs → workspace in their
CLAUDE.md Routing table and specs will be promoted there instead.
$DESIGN_REPO — read from .meta, do NOT re-derive from routing config:
DESIGN_REPO_KEY=$(grep "^design-repo:" "$WORKSPACE/design/.meta" | sed 's/design-repo: //')
case "$DESIGN_REPO_KEY" in
workspace)
DESIGN_REPO="$WORKSPACE" ;;
project)
DESIGN_REPO="$PROJECT" ;;
cross-repo:*)
CROSS_REPO_NAME="${DESIGN_REPO_KEY#cross-repo:}"
CANDIDATE="$(dirname "$PROJECT")/$CROSS_REPO_NAME"
if [ -d "$CANDIDATE/.git" ]; then
DESIGN_REPO="$CANDIDATE"
else
echo "⚠️ Cross-repo path not found at $CANDIDATE — cannot merge journal."
echo "Options: [S]kip journal merge [A]bort close"
fi ;;
*)
echo "⚠️ Unknown design-repo key '$DESIGN_REPO_KEY' — defaulting to project."
DESIGN_REPO="$PROJECT" ;;
esac
$DESIGN_REPO must remain available through Step 8d. Do not recalculate it in
subsequent steps.
Step 3b — Pre-close sweep
Before inventorying artifacts, verify the branch leaves nothing behind. Present
this checklist:
Pre-close sweep — create before presenting the close plan?
[x] 1 write-content capture any work on this branch worth a diary entry
[x] 2 adr record any significant architectural decisions without a formal ADR
[x] 3 protocol sweep formalise any project rules established or re-enforced this branch
[x] 4 forage sweep check for gotchas, techniques, or undocumented behaviours
[x] 5 update-claude-md sync any new workflow conventions to CLAUDE.md
[x] 6 implementation-doc-sync sync documentation with code changes this branch made
Type numbers to toggle, "all" to toggle all, or "go" to proceed:
Defaults: all six on. The user may deselect any that clearly don't apply (e.g. "go"
immediately if the branch was a one-line typo fix). Do not auto-skip — the point is
to make the decision explicit.
Run checked items in this order:
- Forage sweep — while context is full; findings may feed the blog entry
- Protocol sweep — while context is full (invoke
protocol skill with SWEEP operation)
- update-claude-md — sync new conventions before doc-sync reads them
- implementation-doc-sync — sync documentation with code changes
- adr — invoke
adr skill for each candidate identified
- write-content — last, so it can synthesise the full branch narrative including any forage/protocol submissions
Why this step exists: Step 4 inventories artifacts that were written. Without this
sweep, the close plan accurately reports "blog: no new entries" when it should say "blog:
no new entries (and none were considered)." The sweep converts the inventory from a
snapshot into a verified statement. Only after this step is complete does the close plan
accurately reflect what the branch leaves behind.
After all checked items complete, proceed to Step 3c.
Step 3c — Code review (mandatory — HARD GATE)
This step cannot be skipped. Invoke the code-review skill on the branch diff
before proceeding. The review covers the full branch — all commits from the base
branch to HEAD.
git -C "$PROJECT" diff "$PROJECT_BASE_BRANCH"..HEAD --stat
Invoke code-review with the branch diff. If the review surfaces issues:
- Critical/Important issues → fix before proceeding. Re-run review after fixes.
- Minor issues → fix or note. Do not block on minors alone.
Only proceed to Step 4 after code review passes (no critical/important issues open).
Why here and not at Step 8j: By Step 8j you've already built the close plan,
merged the journal, posted specs, and closed issues. Discovering a code problem
at that point means unwinding all of that. Reviewing here — before any close
machinery runs — means fixes are cheap and the close plan reflects reviewed code.
Step 4 — Inventory artifacts
ls "$WORKSPACE/adr/" 2>/dev/null | grep -v INDEX.md
ls "$WORKSPACE/blog/" 2>/dev/null | grep -v INDEX.md
ls "$WORKSPACE/snapshots/" 2>/dev/null | grep -v INDEX.md
ls "$WORKSPACE/specs/$BRANCH_NAME/" 2>/dev/null
ls "$WORKSPACE/plans/" 2>/dev/null | grep -v "^attic$"
cat "$WORKSPACE/design/JOURNAL.md"
Check whether the blog directory has any entries at all. This only determines
whether to run publish-blog — the skill itself handles the "what's new" check
by comparing the workspace blog against the destination:
BLOG_HAS_ENTRIES=$(ls "$WORKSPACE/blog/" 2>/dev/null | grep -v INDEX.md | grep -q "\.md$" && echo yes || echo no)
Step 5 — Journal validation
5a — DESIGN.md existence
If $DESIGN_REPO/DESIGN.md is missing:
[C] Create from journal entries — journal becomes the initial DESIGN.md content
[S] Skip merge entirely
5b — Section heading drift
Re-hash H2 headings in $DESIGN_REPO/DESIGN.md. Compare against design-section-hashes
in .meta. For each §Section anchor in JOURNAL.md, verify its heading still exists
unchanged in DESIGN.md.
grep "^design-section-hashes:" <WORKSPACE>/design/.meta
python3 ~/.claude/skills/project-init/section_hashes.py <DESIGN_REPO>/DESIGN.md
Use the first command's output as STORED, the second as CURRENT.
If drift: [U] update journal anchors, [S] skip drifted sections, [A] abort.
5c — Anchor validation
Count ^### .*·.*§ lines vs total ^### lines in JOURNAL.md.
If any entries lack anchors: [F] fix via java-update-design, [S] skip merge,
[C] continue accepting loss.
5d — Empty journal
If no entries at all:
[W] Write retrospective via java-update-design
[S] Skip and accept permanent loss
Step 6 — Select specs for GitHub posting
If tracking enabled: list $WORKSPACE/specs/$BRANCH_NAME/, ask which to post as
collapsible comments on the GitHub issue. Skip silently if tracking disabled.
Step 7 — Present close plan
Present the plan:
work-end close plan — <branch-name>
Flyway V check ✅ no conflicts
Artifact routing
├── adr/<N> → project [remote-git]
├── blog/<N> → workspace [remote-git]
├── specs/<N> → project [remote-git]
└── snapshots/<N> → workspace [remote-git]
Plan archiving → plans/attic/<branch-name>/ [workspace main]
Journal merge → DESIGN.md (<N> sections)
Spec posting → #<N> (<filenames>)
Issues → close #<all issues from COVERS, e.g. "#5, #19, #32, #24">
Publish blog → 8g (N unpublished entries → destination)
Project rebase <branch> → <base-branch>
Squash <blessed-remote>/main..HEAD (mandatory before any push)
Fork push → origin/main (mandatory, no skip — fork is always updated first)
Blessed repo → prompt: push / PR / skip (upstream remote, if present)
Approve all, or step by step? (all / step)
The Publish blog line is always shown. publish-blog compares the workspace
blog against the destination and publishes only what's missing — it handles the
"what's new" check. Do not attempt to pre-count new entries here.
Step 8 — Execute
Failures are reported but do not stop remaining steps, except: journal merge
failure prompts the user before continuing to issue close.
8a — Batch workspace-main operations (single main-visit)
Build a comma-separated list of all workspace-routed artifact paths from the Step 4
inventory (blog entries, snapshots, plan files to archive, etc.). Include plan files
that need archiving — the script handles mkdir -p and mv to plans/attic/ internally.
Run: python3 ~/.claude/skills/work-end/artifact_promote.py to-workspace-main <WORKSPACE> branch=<BRANCH_NAME> artifacts=<comma-sep-paths>
Read PROMOTED=<count> and PUSHED=yes|no from output.
WORKSPACE DESIGN REPO CASE: If $DESIGN_REPO_KEY = workspace, the journal merge
must also happen during this main-visit. After the script returns, cherry-pick
JOURNAL.md from the epic branch and run the 8d merge steps on workspace main
(baseline=$PROJECT_SHA, target=$WORKSPACE/DESIGN.md). Commit the merged DESIGN.md
and push. Then 8d is complete for the workspace case — skip the 8d block below.
8b — Project-routed artifact promotion (ADRs, specs)
Build a comma-separated list of all project-routed artifact paths from the Step 4
inventory (ADRs, specs, etc.) — paths relative to the workspace root.
Run: python3 ~/.claude/skills/work-end/artifact_promote.py to-project <PROJECT> <WORKSPACE> artifacts=<comma-sep-paths>
Read PROMOTED=<count> and PUSHED=yes|no from output. If PUSHED=no, report the push failure but continue.
8c — Spec cleanup (only if 8b push exit code was 0)
If 8b push failed, skip entirely — workspace copy is the only remaining copy.
Run: python3 ~/.claude/skills/work-end/artifact_promote.py cleanup-specs <WORKSPACE> branch=<BRANCH_NAME>
Read CLEANED=<count> and PUSHED=yes|no from output.
8d — Journal merge
Uses $DESIGN_REPO (set in Step 3) and $PROJECT_SHA (set in Step 1).
⚠️ Branch context matters: When $DESIGN_REPO_KEY = workspace, the merge MUST
happen during the 8a main-visit (see 8a above) — not here. For $DESIGN_REPO_KEY = project,
run the full merge below on the project epic branch (committed before the rebase in Step 8j).
Steps:
- Read baseline:
git -C "$DESIGN_REPO" show "$PROJECT_SHA":DESIGN.md
- Read current
$DESIGN_REPO/DESIGN.md
- Apply journal narrative per
§Section, preserving independent main-branch changes
- Write merged result
- Post-merge verification: re-read each
§Section; present to user ([A] accept,
[R] redo, [X] abort) before committing
- Commit and push:
git -C "$DESIGN_REPO" add DESIGN.md
git -C "$DESIGN_REPO" commit -m "docs($BRANCH_NAME): apply design journal"
git -C "$DESIGN_REPO" push
If journal merge fails: prompt user before continuing to issue close.
8e — Spec posting
Post selected specs (from Step 6) as collapsible comments on the GitHub issue.
8f — Issue close
Only if tracking enabled and $COVERS is non-empty. Close every issue in $COVERS
(comma-separated). COVERS always includes the primary ISSUE_N so no separate
call for the primary is needed.
Run: python3 ~/.claude/skills/work-end/artifact_promote.py close-issues <ISSUE_REPO> covers=<COVERS>
Read CLOSED=<count> from output. If ERRORS= is present, report which issues failed.
8g — Publish blog
Run on workspace main (switch if needed — workspace must be on main when this runs).
Resolve the blog destination from ~/.claude/blog-routing.yaml. For each workspace blog
entry not yet present at the destination, copy and commit:
python3 ~/.claude/skills/work-end/blog_dest.py <WORKSPACE>/blog <BRANCH_NAME>
The script outputs BLOG_DEST, BLOG_REPO, BLOG_SUBDIR, and UNPUBLISHED (comma-separated filenames).
It also copies unpublished entries to the destination. Then commit and push:
git -C <BLOG_REPO> add <BLOG_SUBDIR>/
git -C <BLOG_REPO> commit -m "chore: publish blog entries from <BRANCH_NAME>"
git -C <BLOG_REPO> push
Skip the commit if UNPUBLISHED is empty.
Hard stop if blog directory has entries and publish fails. Do not proceed to 8h until
every workspace blog entry exists at the destination. Verify with the same comm check.
8h — Final report
✅ ADRs → project
✅ Specs → project
✅ Blog → workspace
✅ Plans → attic
✅ Journal merged → DESIGN.md (N sections)
✅ Specs posted to #N, issue closed
✅ Blog published → <destination path> (N new entries) ← "0 new (all current)" if nothing to publish
❌ Push failed — <path>. Run: git -C <path> push
The Blog published line is always present — 0 new entries is a valid outcome,
not a skip. If the line is absent entirely, 8g was not run — stop and run it before
proceeding to 8i/8j.
Closing summary — always append after the artifact lines:
What this delivered:
· <capability or fix — concrete outcome, not a task description>
· <capability or fix>
(2–4 bullets; omit if obvious from the issue title alone)
What this enables:
→ <follow-on work now unblocked, or new capability now possible>
→ <second item if applicable>
Generate from branch context — commits, issue title, COVERS list, and any issue
cross-references. Check COVERS issues on GitHub for 'blocked by' or 'depends on'
language to find what this unblocks. Omit What this enables entirely if nothing
follows directly — do not pad. Omit What this delivered if the issue title already
says it clearly and there is nothing to add.
8i — Hygiene scan
Always run — not an offer. Checks:
1. Blog published — verify every workspace blog entry exists at the destination:
UNPUBLISHED=$(comm -23 <(ls "$WORKSPACE/blog/" | grep "\.md$" | grep -v INDEX | sort) \
<(ls "$BLOG_DEST/" | sort))
if [ -n "$UNPUBLISHED" ]; then
echo "⚠️ Unpublished blog entries:"
echo "$UNPUBLISHED"
echo "→ Return to 8g and publish before proceeding."
fi
If any unpublished entries are found, stop and return to 8g. Do not proceed to 8j.
2. Flyway conflicts — check for V-number collisions with other branches (if Flyway
was involved on this branch).
3. Stale workspace branches — list open branches (no design/EPIC-CLOSED.md in
design/) with no commits in the last 7 days. Report only; do not act.
8j — Rebase project branch onto project base branch, push to fork, prompt for blessed repo
This step is mandatory. Implementation commits on the project branch must land on $PROJECT_BASE_BRANCH before the branch is marked closed.
Detect remote topology first:
FORK_REMOTE=$(git -C "$PROJECT" remote get-url origin 2>/dev/null && echo "origin" || echo "")
BLESSED_REMOTE=$(git -C "$PROJECT" remote get-url upstream 2>/dev/null && echo "upstream" || echo "")
| Topology | Meaning |
|---|
upstream remote exists | Fork model — origin is the fork, upstream is the blessed repo |
No upstream remote | Single-remote model — origin is the blessed repo |
Rebase:
git -C "$PROJECT" fetch "$FORK_REMOTE" "$PROJECT_BASE_BRANCH" 2>/dev/null || echo "⚠️ No network — using local $PROJECT_BASE_BRANCH"
git -C "$PROJECT" checkout "$PROJECT_BASE_BRANCH"
git -C "$PROJECT" rebase "$BRANCH_NAME"
If rebase fails (conflict):
- Report the conflicting files verbatim.
- Stop. Do not proceed to Step 9.
- Instruct the user: resolve conflicts on
$PROJECT_BASE_BRANCH, then re-run work end to complete the close.
Squash before fork push (fork model only — mandatory):
Squash runs BEFORE the fork push so both fork and blessed repo receive identical history.
Run git-squash on the range $BLESSED_REMOTE/$PROJECT_BASE_BRANCH..HEAD. This is not optional
and must not be bypassed with --no-verify. The pre-push hook firing is the signal to run
git-squash, not to skip it. Noise commits (chore, docs follow-ons, journal applies, CI fixups)
must be compacted before the range is shared anywhere.
git -C "$PROJECT" log --oneline "$BLESSED_REMOTE/$PROJECT_BASE_BRANCH"..HEAD
Invoke /git-squash with the range $BLESSED_REMOTE/$PROJECT_BASE_BRANCH..HEAD.
Wait for the squash plan, user approval, and execution before proceeding.
If the user explicitly says "skip squash" or "no squash needed": accept and note it,
then proceed. Never silently skip.
Push to fork remote (mandatory — no skip option):
The fork push is always required. There is no [N]skip. The blessed repo can never receive
commits that the fork has not already received.
git -C "$PROJECT" push "$FORK_REMOTE" "$PROJECT_BASE_BRANCH"
If the fork push fails: stop. Do not proceed to blessed repo delivery. The fork must be
updated first — the blessed repo can never be ahead of the fork.
Blessed repo delivery (fork model only):
If $BLESSED_REMOTE is non-empty, always prompt — three choices:
"Deliver to $BLESSED_REMOTE/$PROJECT_BASE_BRANCH?
[P] Push directly [R] Open PR [N] Skip"
- P — Push directly:
git -C "$PROJECT" push "$BLESSED_REMOTE" "$PROJECT_BASE_BRANCH"
- R — Open PR:
gh pr create --base "$PROJECT_BASE_BRANCH" --head "$(git -C "$PROJECT" remote get-url "$FORK_REMOTE" \
| sed 's|.*github.com[:/]\(.*\)\.git|\1|'):$PROJECT_BASE_BRANCH" --title "<issue title>" --body "Closes #$ISSUE_N"
- N — Skip: leave blessed repo delivery for later; note it in the 8h report. Fork already has the commits.
If no $BLESSED_REMOTE: no prompt — fork push is the final delivery.
Why rebase and not merge --no-ff? Rebase keeps the project base branch history linear and avoids a merge commit that references a branch consumers never saw. Fast-forward is a safe subset — git rebase fast-forwards when possible, replays commits otherwise.
8j-cleanup — Remove scaffold from main (single-repo mode only)
Skip entirely in two-repo mode (SINGLE_REPO_MODE=no).
In single-repo mode, Step 8j's rebase brings the scaffold commit (.meta, JOURNAL.md)
from the epic branch onto main. These files are ephemeral branch artifacts — they must not
persist on main. HANDOFF.md and blog entries that also land via rebase are intentional and
must not be removed.
After 8j completes (workspace/project is now on main):
If $SINGLE_REPO_MODE = yes:
Run: python3 ~/.claude/skills/work-end/branch_cleanup.py cleanup-scaffold <WORKSPACE> single-repo=yes
Read CLEANED=yes from output. The script removes .meta and JOURNAL.md, removes the
design/ directory if empty, commits, and pushes.
Why this step exists: In two-repo mode, .meta and JOURNAL.md live on the workspace
epic branch only — they never reach workspace main. In single-repo mode, the workspace IS
the project repo, so the rebase in 8j brings them to main. This cleanup restores the
invariant. HANDOFF.md and blog entries reaching main via the same rebase are correct
behaviour — do not remove them.
8k — Final build verification (Java / Maven projects only)
Run after 8j. Skip for non-Java projects.
Read PROJECT_TYPE from the ctx.py output (already run in Path Resolution).
If PROJECT_TYPE is java, use AskUserQuestion with exactly these four options:
Build verification level?
[F] Fast (default) — mvn install -DskipTests -DskipITs
[U] Unit tests — mvn install -DskipITs
[A] All tests — mvn install
[S] Skip
Map the answer to a command:
- F or Enter:
mvn install -DskipTests -DskipITs
- U:
mvn install -DskipITs
- A:
mvn install
- S: skip step entirely
If the user types something else (e.g. "integration tests only"): run mvn install -DskipTests.
Run from project root:
mvn install [flags] -C "$PROJECT"
If the build fails → stop. Do not proceed to Step 9 (mark closed).
Report the failure and ask the user to fix it before closing.
If the build passes → add ✅ Build verified (mvn install) to the 8h report and continue.
Step path (alternative to all-at-once)
If user chose "step" in Step 7:
- Phase 1: Artifact routing (8a including publish-blog if blog/ has entries), 8b, 8c — confirm, execute, report → "Continue to journal merge? (y/n)"
- Phase 2: Journal merge (8d) — show each
§Section before/after, confirm → "Continue to GitHub posting? (y/n)"
- Phase 3: Spec posting (8e), issue close (8f) → "Continue to branch merge? (y/n)"
- Phase 4: Merge project branch to
$PROJECT_BASE_BRANCH (8j), build verification (8k if Java), EPIC-CLOSED.md, return workspace to main.
Note: publish-blog (8g) runs after issue close (8f), before 8i hygiene scan. It is not
an "offer" — it always runs. 8i then verifies the result; any unpublished entries block 8j.
Step 9 — Mark closed
EPIC-CLOSED.md lives in $WORKSPACE/design/ and is committed to the workspace
epic branch (not main), so the hygiene scan can traverse epic branches to detect it.
Two-repo mode: workspace is still on the epic branch at this point — commit directly.
Single-repo mode: after 8j the repo is on main. Switch to the epic branch to commit,
then return to main.
Run: python3 ~/.claude/skills/work-end/branch_cleanup.py create-epic-closed <WORKSPACE> branch=<BRANCH_NAME> date=$(date +%Y-%m-%d) issues=<COVERS> single-repo=<yes|no>
Read CREATED=yes from output. The script handles single-repo branch switching internally.
Branches are not deleted. EPIC-CLOSED.md is the signal for hygiene scan cleanup.
Stack cleanup on end: If this branch was in the pause stack (detected in Pre-conditions),
remove it now that the branch is closed:
Run: python3 ~/.claude/skills/work-end/branch_cleanup.py cleanup-stack <WORKSPACE> branch=<BRANCH_NAME>
Read REMOVED=yes|no from output. If yes, the branch was found and removed from the stack.
Step 10 — Return to base branches
Project is already on $PROJECT_BASE_BRANCH from Step 8j. Switch both repos to main:
Run: python3 ~/.claude/skills/work-end/branch_cleanup.py checkout-main <PROJECT> <WORKSPACE>
Read SWITCHED=yes from output. The script checks out main and pulls in both repos.
Step 11 — ARC42 stale scan
Only if HAS_ARC42STORIES=yes (from ctx.py output, already run in Path Resolution).
Catches cross-session drift not covered by work-end's per-commit scope — layer
statuses, resolved blockers, closed-issue forward refs.
See the handover skill's Step 2c for the three checks (layer statuses, external
blockers, forward-tense refs). Run the same procedure here. Commit fixes to
the project repo.
Skip silently if HAS_ARC42STORIES=no.
Step 12 — Write HANDOFF.md and close the session
work-end includes the full session wrap. There is no need to invoke the handover
skill separately after work-end — everything is handled here.
12a — HANDOFF.md
Follow the handover skill's Steps 1–6 (check previous handover, recall from
context, gather orientation, build references, write HANDOFF.md, commit to
workspace main). The pre-close sweep (Step 3b) already ran forage, protocol,
update-claude-md, doc-sync, and write-content — do not re-run them or show the
wrap checklist. Only write the HANDOFF.md file.
Important: HANDOFF.md must be committed to workspace main, not the epic
branch (which is now closed). The workspace should already be on main from Step 10.
12b — Session rename
Suggest a session rename if the session name appears auto-generated (random
three-word pattern). Generate a concise 2–4 word name from the session's
content. The user types /rename <name> — it is a Claude Code built-in.
12c — Session close summary
Output the final tick-list:
Session wrap complete.
✅ Epic hygiene (or ⏭ skipped)
✅ Forage sweep N entries submitted (or: nothing garden-worthy found)
✅ Protocol sweep N protocols captured (or: nothing new)
✅ update-claude-md (or ⏭ skipped)
✅ implementation-doc-sync N docs updated, N issues filed (or: nothing stale found / ⏭ skipped)
✅ journal-entry (or ⏭ skipped — not mid-epic)
✅ arc42 stale scan N items fixed (or: nothing stale found / ⏭ skipped — no ARC42STORIES.MD)
✅ write-content (diary) <entry filename> (or ⏭ skipped)
✅ Code review 0 findings (or: N findings fixed)
✅ HANDOFF.md committed <workspace>/HANDOFF.md → main
Show every item — both ticked and skipped with reason.