The /end-session sync
How a coding session closes itself out cleanly. It checks the working tree, gathers what changed, audits which lessons still hold, files and clusters new to-dos, then writes the whole handoff into one ledger and commits it. The next session reads that ledger and picks up where this one left off.
We replaced free-form handoff notes with a structured ledger because prose drifts. Whether that trade is worth the ceremony is the call we're still watching. The telemetry is meant to tell us. Reply on LinkedIn.
How to read this map
Pick a stage to walk, or start at the top
The walkthrough, as text
Every stage of the interactive walkthrough above, as plain readable text. Each stage has its own link.
Stage 1Working tree checkClean the tree before sync
Working-tree gate
Before anything is recorded, the system checks whether you have uncommitted work sitting around, and if you do, it stops and asks what to do with it rather than burying it.
A sync that records state on top of a dirty working tree records a lie. Resolve pending changes first, never silently proceed.
What happens
The orchestrator reads the working tree with git status --porcelain and git diff --stat. A clean tree falls straight through to Stage 2.
A dirty tree is a hard pause. The system surfaces the changes and forces a choice: commit them first (it suggests /pre-commit-check then a commit), stash them with a descriptive message, or, rarely, document the WIP in Current location's Pending field and proceed. Guessing is forbidden; it waits for your explicit decision.
Who acts
- git (Bash), tool: Reads porcelain status + diff-stat to detect any uncommitted work.
- AskUserQuestion, command: Forces the commit / stash / document-and-proceed decision when the tree is dirty.
The rules
- Clean tree proceeds.
git status --porcelainempty → straight to Stage 2, no questions. - Dirty tree, three options. Commit first (suggest
/pre-commit-check), stash withgit stash push -m "WIP: …", or document in Pending and proceed (rare, intentional cross-session WIP only). - Never silently proceed. It waits for an explicit user decision; declining all three options halts the sync entirely.
In: The current working tree at session end. Out: A clean tree (or a deliberately-documented dirty state) cleared to sync.
Stage 2Multi-session divergence checkDid another session touch STACK?
Divergence check
It checks whether a different session edited the ledger since this one started, because if it did, the picture this session thinks it's closing out may already be out of date.
Use git, not prose, to find when STACK was last synced. If another session committed in between, surface it before recording anything.
What happens
The system finds the commit that last touched STACK.md with git log -1 --format=%H -- STACK.md and compares it to HEAD. Anything committed after that point is candidate 'this session' work: git is the source of truth here, not parsing the ledger's prose.
If HEAD has moved but the intervening commits were not made in this session, another session synced in the meantime. That divergence may invalidate the Current location entry, so it's surfaced with the commit list and you choose: proceed with the sync, or pause to reconcile by hand.
Who acts
- git log / rev-parse, tool: Finds STACK's last-touch SHA and lists commits since, to detect a competing session.
- AskUserQuestion, command: On divergence: proceed with sync, or pause to reconcile manually.
The rules
- Git decides the baseline. Last-touch SHA from
git log -1 -- STACK.mdis more robust than reading prose dates out of the ledger. - In-sync is the happy path. If STACK's last-touch SHA equals
HEAD, no commits intervened, proceed silently. - Foreign commits are surfaced, not absorbed. Intervening commits from another session are listed with a warning; reconciling them is a human call.
In: STACK's last-sync SHA and the current HEAD. Out: A confirmed-clean baseline, or a flagged divergence to reconcile.
Stage 3Gather session stateWhat did this session do?
Gather state
The system assembles a picture of everything that happened this session (which commits landed, which files moved most, which plans were in play) and reads the ledger end to end.
Diff this session against its baseline: commits made, files most touched, plans referenced, and which Campaign/Phase/Step is active.
What happens
The session baseline is the last commit that touched STACK.md: the previous /end-session sync. Everything after it is this session's work, listed with git log --oneline BASELINE..HEAD.
The orchestrator then reads STACK.md in full, all sections, and identifies the active Campaign / Phase / Step. From the diff it builds a working summary for the interactive stage: commit count and subjects, the top three-to-five most-touched files, and any plan files referenced.
Who acts
- git log, tool: Lists commits since the baseline: the raw record of this session's work.
- Read (STACK.md), tool: Reads every section to locate the active Campaign / Phase / Step.
The rules
- Baseline = last STACK sync.
git log -1 --format=%H -- STACK.md; commits after it are this session's work. - Read STACK fully. All sections, Current location, Phases, lessons, follow-up (FU) lanes, not just the top.
- Build the working summary. Commit count + subjects, top 3–5 touched files, plan files referenced: the raw material for Stage 6.
In: The verified baseline + the full STACK.md. Out: A working summary of the session: commits, touched files, active Phase.
Stage 4Verify Current locationLedger vs reality, field by field
Verify · drift
It compares what the ledger currently claims the state is against what actually happened, one field at a time, and flags anything that drifted so it can be corrected before writing.
Compare STACK's Current location against actual git state field by field: any drift is marked for the Stage 7 write.
What happens
Four fields are checked against reality. Pipeline: does the recorded pipeline match what the commit subjects show (e.g. /change-heavy in the trail)? Pending: any AskUserQuestion left unresolved this session? Next move: still accurate, or did the session change direction? Last commit: compared against git rev-parse HEAD.
Each field that drifted is marked for update in Stage 7. Nothing is rewritten yet: this stage only diagnoses the gap between the ledger's claim and the git record.
Who acts
- git rev-parse / log, tool: Supplies the actual HEAD and commit subjects to check each field against.
- Read (Current location), tool: Reads the four recorded fields to compare against git reality.
The rules
- Four fields, one table. Pipeline · Pending · Next move · Last commit: each gets a 'STACK says' vs 'Actual' row.
- Diagnose, don't write. Drifted fields are marked for the Stage 7 edit, not corrected in place here.
- HEAD is the ground truth.
Last commitis checked againstgit rev-parse HEAD: the cheapest field to verify.
In: STACK's Current location + the git state from Stage 3. Out: A per-field drift list queued for the Stage 7 write.
Stage 5Detect phase completionsDid a phase or campaign close?
Detect closes
It scans the session for signs that a phase of work actually finished (a checkbox flipped, a 'RESOLVED' commit) because a close triggers extra rituals: a lessons audit and a cleanup of any to-dos that were anchored to the now-finished phase.
Scan commits and checkboxes for phase/campaign closes. A close arms the lessons audit and the phase-close FU garbage-collection.
What happens
The system scans this session's commit subjects for completion patterns, Step , Phase , Campaign CLOSED, and checks whether any STACK - [ ] was flipped to - [x].
Any close arms three things for later stages: the checkbox flip in Stage 7, the lessons audit in Stage 6b (cadence: every phase-close, plus campaign-close), and the phase-close FU garbage-collection: every follow-up homed at the closing phase must resolve, re-home, or promote. Skipping that GC is exactly what once orphaned 13 follow-ups across P1/P2, which is why the Stage 7 verify gate now enforces it. A campaign-close (the session flipped the last open phase in the campaign) arms the heavier ritual: a lessons forcing-function, a full FU sweep, and a global-doc update.
Who acts
- git log (grep), tool: Scans commit subjects for RESOLVED / SHIPPED / CLOSED completion patterns.
- Read (checkboxes), tool: Detects any STACK checkbox flipped from
[ ]to[x]this session.
The rules
- Two completion signals. Completion-pattern commit subjects (RESOLVED / SHIPPED / CLOSED) AND flipped STACK checkboxes: either one counts as a close.
- A close arms the audit. Phase-close → lessons audit (6b) becomes mandatory; no close → it's optional this session.
- A close arms the FU GC. Every FU homed at the closing phase/step is flagged for resolve / re-home / promote in Stage 7: fires at every phase close, not just campaign-close.
- Campaign-close is the heavy ritual. Last open phase flipped → lessons forcing-function, full FU sweep, and append the campaign's git range + synopsis to the global overview doc.
In: This session's commits + STACK checkbox state. Out: Flags for: checkbox flips, the lessons audit, and the phase-close FU GC.
Stage 6Interactive syncLessons, FUs, calibration, with you
Interactive sync
This is the conversation: the system walks you through which durable lessons surfaced, which still hold, which have earned a permanent home, and what new to-dos or bugs need tracking, then groups the new to-dos so cleanup stays manageable.
One question at a time: surface new lessons, audit existing ones, promote the proven, route new FUs by kind, then cluster them into batches.
What happens
Five sub-steps, each an AskUserQuestion. 6a, new lessons: durable discoveries that should bind future sessions are recorded with an [audits survived: 0] tag and tested ('one-line rule?', 'applies beyond this stack?'). 6b, phase audit (only if Stage 5 found a close): each in-force lesson is KEPT (increment the survival count), RETIRED to Archive, or DROPPED. 6c, promotion: a three-point filter (survived 3+ AND generalizes AND one-line-able) surfaces candidates; on 'Promote' the lesson is actually written into the right memory/feedback_*.md and removed from STACK.
6d, FUs & bugs: each surfaced item is routed by kind: bugs by severity (HIGH/security → the Action-required callout; medium/low → the FU lane), FUs through the two-week filter then tagged with type, pipeline, fully-qualified home, and a machine-verifiable anchor. The new FUs are then clustered by type into a batch-readiness report (≥5 same-type → /change-heavy; 2–4 → /cleanup-light): the lightweight inline version of /cluster-fus. 6e, recommendation calibration: any recommendation flips this session are logged with a root-cause tag, and recurring tags propose hardening or relaxing the calibration memory.
Who acts
- AskUserQuestion, command: Drives all five sub-steps: one question at a time, multi-select where it fits.
- memory/feedback_*.md, tool: Destination for promoted lessons + the recommendation-calibration log; written, not just noted.
The rules
- New lessons start at zero. Recorded with
[audits survived: 0]; must be a one-line rule, and if it doesn't generalize it's marked Archive, not In-force. - Promotion is a write, not a note. Three-point filter passes → append the one-line rule to a
memory/feedback_*.mdfile AND remove it from STACK (now auto-loaded from memory). - Route every FU by kind. Bugs by severity; FUs through the 2-week filter, then tagged
[type · pipeline] … home: … {anchor: …}: new rows should anchor soverify:fuscan check them. - Cluster into batches. Group new FUs by type and report batch-readiness:
type-design ×9 → /change-heavy,silent-failure ×4 → /cleanup-light.
In: The session summary + any phase-close flags from Stage 5. Out: Audited lessons, promotions written to memory, routed-and-clustered FUs, all queued for Stage 7.
Stage 7Apply updates to STACKWrite the ledger, then the gate
Write · verify gate
Everything decided so far is written into the ledger, refreshed status, lessons, new to-dos, flipped checkboxes, and then a machine check refuses to let the session close if any to-do is stranded or its citation is broken.
Edit STACK with every change from Stages 4–6, run the phase-close FU GC, then a hard verify:fus gate: 0 drifted AND 0 stale-home, or it stops.
What happens
The orchestrator edits STACK.md with everything collected: the four Current-location fields refreshed, new lessons appended at [audits survived: 0], survived counts incremented, retired lessons moved to Archive, new FUs filed to their home (phase-local → STACK, Standing → Standing-FUs.md), promoted lessons removed, and any phase checkboxes flipped. If a checkbox flipped, the phase-close FU GC runs: every FU homed at the now-closed phase must resolve, re-home, or promote. The diff is shown and each major edit is confirmed before writing.
Then the gate, run always after writing, not just when an anchor was filed: npm run verify:fus must report 0 drifted AND 0 stale-home. A drifted anchor is a typo'd citation, cheapest to fix now. stale-home > 0 is a hard stop: a phase was flipped [x] but a follow-up is still homed there, meaning the GC was incomplete. Each flagged FU is re-homed and the check re-run until it's clean. A close is never committed while it leaves orphans. That is the exact failure this gate exists to stop.
Who acts
- Edit (STACK.md), tool: Writes the four fields, lessons, FUs, and checkbox flips: the canonical ledger update.
- verify:fus, tool: Post-write gate: must report 0 drifted AND 0 stale-home before the run may close.
- AskUserQuestion, command: Confirms each major edit; the diff is shown before anything is written.
The rules
- One write, everything. Current location, lessons (new/survived/retired/promoted), FUs by home, checkbox flips, all in one reviewed diff.
- Phase-close GC is mandatory. If ANY checkbox flipped, every FU homed at the closed phase must resolve / re-home / promote: its omission once orphaned 13 FUs.
- Gate runs always.
npm run verify:fusafter every write, even when no anchor was filed this session. - stale-home is a HARD STOP. 0 drifted AND 0 stale-home required;
stale-home > 0means re-home the named FUs and re-run before any commit.
In: All updates queued in Stages 4–6. Out: A written STACK + a passing verify:fus gate (or a halt to re-home orphans).
Stage 8Push-eligibility checkPush now, or keep accumulating?
Push-eligibility
It works out whether this is a sensible moment to push to the remote (the rule is to wait for a phase to actually close rather than pushing in the middle of one) and records that verdict as part of the handoff.
Don't recommend a push mid-phase. Count commits ahead, check whether the active Phase is closed, and record the verdict in Current location.
What happens
Per the push-gate convention, a push isn't recommended mid-phase; it waits for a phase-close. The system counts unpushed commits with git rev-list --count origin/main..HEAD and reads STACK's Phases section to see whether the active phase is closed.
It outputs one of three states: mid-phase ('N commits ahead; NOT push-eligible; continue accumulating'), phase-close ('N ahead; phase closed; push-eligible'), or end-of-effort ('entire stack closed; push, then run the stack-close ritual'). That state is recorded in Current location in Stage 7: it's part of the handoff.
Who acts
- git rev-list, tool: Counts commits ahead of
origin/main: the 'how far unpushed' number. - Read (Phases), tool: Reads STACK's Phases section to decide if the active phase is actually closed.
The rules
- No mid-phase push. The push gate waits for a phase-close; pushing mid-phase is discouraged by convention.
- Three eligibility states. Mid-phase (keep accumulating) · phase-close (push-eligible) · end-of-effort (push then stack-close ritual).
- Verdict joins the handoff. Push state is recorded in Current location so the next session knows whether a push is owed.
In: The unpushed commit count + the active Phase's open/closed status. Out: A push-eligibility verdict, recorded into Current location.
Stage 9Commit: the address-tagged trailOne tagged commit, no handoff file
Commit
Finally it commits the updated ledger with a searchable, address-tagged subject. There is no separate handoff document: the ledger holds the state, and git history holds every past version of it.
The handoff IS Current location; there is no handoff file. Commit STACK with an address-tagged subject so the forensic trail stays greppable.
What happens
Stage 7 already wrote the full handoff into STACK's Current location: state, next-move-with-first-step, pending, standing rule. There is no separate handoff file; the richer orientation brief is composed on demand by /start-session, not stored. Git history is the forensic trail: every /end-session commit captures that session's Current location, recoverable via git show .
The diff is shown and you confirm the commit. On yes, the system stages STACK.md + Standing-FUs.md (plus any promoted memory/feedback_*.md or the global doc if a campaign closed) and commits with an address-tagged subject: chore(stack): end-session sync: . The tag makes the trail greppable: git log --grep="Sweep/1" retrieves every handoff for that phase. Decline, and the changes are left unstaged, surfaced as 'ready to commit when you're ready.'
Who acts
- AskUserQuestion, command: Shows the diff and confirms the commit: the final human checkpoint.
- git commit, tool: Stages STACK + Standing-FUs (+ memory/global doc if touched) and writes the address-tagged subject.
The rules
- No separate handoff file. Current location IS the handoff; git history holds every session's version, greppable by tag.
- Address-tagged subject.
chore(stack): end-session sync:, so/ ( ) git log --grepretrieves every handoff for a phase. - Stage exactly what changed. STACK + Standing-FUs, plus a promoted
memory/feedback_*.mdor the global doc only if those were touched. - Decline leaves it ready. If you don't commit, changes are left unstaged and surfaced as 'ready to commit when you're ready.'
In: The written, gate-passed STACK + push verdict. Out: One address-tagged commit: the session's recoverable forensic trail.
Stage ✓Done: the handoff is the ledgerSTACK is the handoff
Outcome
When the sync finishes, the next session can pick up cleanly: the ledger holds the state and the tactical next step, lessons are audited and promoted, new to-dos are tagged and clustered, and there's no pile of handoff notes to dig through, just git.
By the end, the handoff lives in structured fields, not free-form prose, and the forensic trail is git, not a stack of handoff docs.
What happens
Three outputs. STACK.md updated: Current location IS the handoff (state + tactical next-move + standing rule); lessons audited and promoted; new FUs tagged and clustered. One commit, if approved: the address-tagged chore(stack): end-session sync that forms the forensic trail. No separate handoff file: git history holds every session's Current location, and /start-session composes the orientation brief on demand.
The point of all this structure: variance between sessions drops sharply because everything lives in fields, not prose, and the record of how the work evolved is git history, recoverable and greppable, rather than a folder of one-off notes that no one re-reads.
Who acts
- STACK.md, tool: The canonical continuity ledger: its Current location is the handoff the next session reads.
The rules
- Ledger is the handoff. State + tactical next-move + pending + standing rule, all in structured Current-location fields.
- Lessons audited & promoted. In-force lessons survive, retire, or get written into memory and removed from STACK.
- FUs tagged & clustered. New follow-ups carry type/pipeline/home/anchor and are grouped into batch-ready clusters.
- Git is the only trail. No handoff docs;
git showrecovers any past session,:STACK.md git log --grepfinds it by tag.
In: The committed sync. Out: A clean handoff: ledger + one tagged commit, ready for /start-session.