#+TITLE: Session Wrap-Up Workflow #+AUTHOR: Craig Jennings & Claude #+DATE: 2026-04-20 * Overview This workflow defines the process for ending a Claude Code session cleanly. It finalizes the session record, commits + pushes all work, and provides a warm handoff. Triggered by Craig saying "wrap it up," "that's a wrap," "let's call it a wrap," or similar. * The Session Record Throughout the session, =.ai/session-context.org= has been maintained with: - =* Summary= — structured distillation (empty or draft during session) - =* Session Log= — chronological narrative of what happened, written as you go At wrap-up, this file becomes the permanent session record by being renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=. No transcription elsewhere. The file IS the record. * Exit Criteria The wrap-up is complete when: 1. *Summary is written.* The =* Summary= section of =.ai/session-context.org= is populated by reading the =* Session Log= — Active Goal, Decisions, Data Collected / Findings, Files Modified, Next Steps. 2. *File is archived.* =.ai/session-context.org= has been renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=. The old path no longer exists. 3. *todo.org is clean.* Cleanup script ran. Any auto-fixes are staged for the wrap-up commit. Orphan planning lines surfaced for manual fix if there are any. 4. *Linear board is honest* (skip if project doesn't use Linear). Any Dev-Review ticket whose PR has merged was moved to Done or PM Acceptance per the classification rule. 5. *Git state is clean.* All changes committed + pushed to all remotes. Working tree clean. 6. *Valediction delivered.* Brief, warm closing with key accomplishments and reminders. The absence of =.ai/session-context.org= is the signal that the last session wrapped up cleanly. Its presence at session start means the previous session was interrupted. * The Workflow ** Step 1: Finalize the Summary Read through the =* Session Log= in =.ai/session-context.org=. Populate (or refine) the =* Summary= section: - *Active Goal* — one or two sentences describing the session's focus - *Decisions* — key choices made, with enough context to recall the /why/ - *Data Collected / Findings* — anything concrete (measurements, root causes, paths, discoveries) - *Files Modified* — what was changed, with one-line rationale per significant file - *Next Steps* — what should happen in the next session Don't repeat everything from the Log in the Summary. The Summary is distillation — pull out what's load-bearing. The Log stays in the file and is available if a future reader wants detail. *** KB promotion check (and the one-line instrumentation receipt) Before closing the Summary, ask: did this session learn anything worth promoting to the agent knowledge base? The bar is =knowledge-base.md='s inclusion criteria — durable facts with cross-project or cross-machine value (decisions and their why, environment gotchas, reference pointers, transferable lessons). Promote each qualifying fact as one =agents/= node per the rule's schema (work-classified projects skip the write per the boundary; the check still runs so the receipt below is honest). Then add one line at the end of the Summary, always, even when nothing moved: #+begin_example KB: promoted 2 / consulted yes #+end_example "promoted N" counts nodes written this session (0 most sessions); "consulted yes-no" records whether any KB query informed the session's work. The line is the input to the spec's 30-day success-metrics checkpoint — grepping session archives for =KB:= answers "are agents actually using this?" without any other instrumentation. A session that skips the line breaks the metric, so it's part of the Summary contract, not optional. ** Step 2: Pick a description + rename Read the Summary's Active Goal and the prominent entries in the Session Log. Pick a 4-6 word description that would make sense as a git-commit-message-series summary for the whole session. Good descriptions are concrete nouns/verbs: - =docs-ai-migration-and-ai-launcher= - =mybitch-usb-disconnect-diagnosis= - =ratio-system-health-check= - =orchestration-dashboard-bug-triage= Avoid vague ones: - =session-work= (useless) - =various-improvements= (useless) - =updates= (useless) Get current time and rename: #+begin_src bash mkdir -p .ai/sessions now=$(date +%Y-%m-%d-%H-%M) # Resolve the AI_AGENT_ID-aware source path (see protocols.org "Agent-scoped # path"); fall back to the singleton if the helper isn't present. sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org) # Under multi-agent, fold the agent id into the archive name so two agents # wrapping in the same minute don't collide. Single-agent: no segment. idseg="${AI_AGENT_ID:+${AI_AGENT_ID}-}" mv "$sc" ".ai/sessions/${now}-${idseg}DESCRIPTION.org" #+end_src Replace =DESCRIPTION= with your picked slug. (=AI_AGENT_ID= should be filename-safe; the recommended =host.project.runtime.shortid= shape already is.) ** Step 3: todo.org cleanup (hygiene + archive completed work) If the project has a =todo.org= at its root, run the cleanup script before committing. Two passes, both fast and idempotent: a hygiene pass and an archive pass. *** Roam inbox sweep (inbox-zero) Before the cleanup scripts, sweep the roam global inbox (=~/org/roam/inbox.org=) for items that belong to this project, so any imported tasks get linted and ride the wrap commit. Delegate to [[file:inbox-zero.org][inbox-zero.org]] for the claimed set. #+begin_src bash [ -f "$HOME/org/roam/inbox.org" ] && grep -cE '^\*\* ' "$HOME/org/roam/inbox.org" || true #+end_src Skip-fast when nothing matches: if the roam clone isn't on this machine, or no item is prefixed for this project, this is a silent no-op. When claimed items exist, run inbox-zero's Phase B–C (file each into =todo.org=, then remove them from the shared inbox in a separate roam commit). Report the total count and how many appeared related to this project, per inbox-zero's scan-summary rule. *** Hygiene pass It catches a recurring pattern: org sometimes leaves noise lines like =- State "X" from "X" [date]= when a state-change log lands outside a =:LOGBOOK:= drawer and the state didn't actually change. These lines carry no information and they break org's planning-line parser by wedging between the heading and =DEADLINE:=/=SCHEDULED:=, which kicks the entry out of agenda views. #+begin_src bash [ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el todo.org #+end_src The script is fast (under half a second on a 4000-line file) and idempotent — if there's nothing to fix, it reports zero changes and exits clean. What it does: 1. *Auto-deletes* bogus state-log lines (matched on identical from/to states). Any deletions show up in the wrap-up commit's diff, so they get reviewed before push. 2. *Reports* "orphan planning lines" — entries whose body has =DEADLINE:= or =SCHEDULED:= but =org-entry-get= can't read it (some other malformation kept it out of canonical position). The script doesn't auto-rewrite these because the right fix depends on whether real state-log history needs preserving — surface them and fix manually if they matter for the agenda. Run the report-only variant first if you want to see what would change without writing: #+begin_src bash emacs --batch -q -l .ai/scripts/todo-cleanup.el --check todo.org #+end_src *** Archive completed work #+begin_src bash [ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done todo.org #+end_src =--archive-done= moves every level-2 subtree whose TODO state is DONE or CANCELLED out of the project's "Open Work" section and into its "Resolved" section, subtree intact. The two sections are matched by a unique level-1 heading containing "Open Work" (case-insensitive) and one containing "Resolved" — if either is missing or ambiguous, the file is skipped with a message, no crash. Only direct level-2 children move; a DONE entry nested under an open parent stays put. Idempotent; any moves show up in the wrap-up commit's diff for review before push. Preview the moves without writing: #+begin_src bash emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org #+end_src *** Sync child priorities #+begin_src bash [ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --sync-child-priority todo.org #+end_src =--sync-child-priority= walks every heading with a priority cookie =[#A]=–=[#D]= and, for each of its direct child headings whose own priority cookie is /lower/ (later in the alphabet — D is below A), bumps the child to match the parent. Down-only: parents are never bumped up to match a higher-priority child. Children without a priority cookie are left alone, as are parents without one. The walk visits parents before descendants, so a multi-level chain (=[#A]= → =[#B]= → =[#D]=) collapses to the top priority in a single pass. Idempotent. Opt-out for deliberately-lower children: tag the heading =:no-sync:= (the literal six-character tag, including the hyphen). The script matches the tag literally on the heading line, so it works whether or not the surrounding emacs config has extended =org-tag-re= to allow hyphens. #+begin_example *** TODO [#D] Follow-up: VAD :no-sync: #+end_example Use this for =Follow-up:=, =Spike:=, =Stretch:= sub-tasks that are deliberately deprioritized below their parent — without the tag, the wrap-up would silently bump them back up. Preview the bumps without writing: #+begin_src bash emacs --batch -q -l .ai/scripts/todo-cleanup.el --check-child-priority todo.org #+end_src (=--check-child-priority= is the report-only alias for =--sync-child-priority --check=.) *** Lint org files (mechanical sweep, judgments deferred) #+begin_src bash if [ -n "$LINT_ORG_FOLLOWUPS" ]; then followups="$LINT_ORG_FOLLOWUPS" elif [ -d "./inbox" ]; then followups="./inbox/lint-followups.org" else followups=".ai/lint-followups.org" fi [ -f todo.org ] && emacs --batch -q -l .ai/scripts/lint-org.el \ --followups-file="$followups" todo.org #+end_src =lint-org= runs =org-lint= over =todo.org=, auto-applies four mechanical categories (=item-number= counters, bare =#+begin_src= → =#+begin_example=, multi-line planning-info merged onto one line, =**X.**= → =*X.*=), and appends every remaining judgment item (broken file links, invalid fuzzy links, verbatim-asterisk inside body prose, suspicious src-block languages) to the follow-ups file as a dated org section. Mechanical fixes show up in the wrap-up commit's diff for review before push. The follow-up path defaults to =./inbox/lint-followups.org= in the current project (where the next morning's daily-prep merges it in). If the project doesn't have an =inbox/= directory, the script falls back to =.ai/lint-followups.org= inside the current project. Override with =LINT_ORG_FOLLOWUPS== in the environment if needed — useful for routing all wrap-up output to a single shared inbox across projects. Each project's own =inbox/= is the right default because daily-prep reads that project's inbox at startup. Hardcoding a single project's path (formerly =~/projects/work/inbox/=) routed every project's wrap-up findings into the wrong inbox. Preview without writing — same flags as =--check= on the other scripts: #+begin_src bash [ -f todo.org ] && emacs --batch -q -l .ai/scripts/lint-org.el --check todo.org #+end_src The wrap-up never blocks on judgment items — they're deferred by design. For an interactive walk of the judgments mid-day, run =/lint-org todo.org=. *** Inbox sanity check (surface unprocessed handoffs) If the project has an =inbox/= directory, verify it holds nothing but =.gitkeep=, =lint-followups.org= (the lint-org pipeline file the next morning's daily-prep consumes), and any explicitly-deferred =PROCESSED-*= files before the wrap completes. An inbox that arrived at session start with handoffs from other projects, or that received handoffs mid-session, needs the =process-inbox.org= workflow to run and apply its value-gate dispositions. Wrapping with a dirty inbox silently defers the work to next session and accumulates handoff debt that the sender can't see. #+begin_src bash unprocessed=$(find inbox -maxdepth 1 -type f \ ! -name '.gitkeep' \ ! -name 'lint-followups.org' \ ! -name 'PROCESSED-*' \ 2>/dev/null | wc -l) if [ "$unprocessed" -gt 0 ]; then echo "wrap-up: inbox/ has $unprocessed unprocessed item(s). Run process-inbox.org before wrapping, or explicitly defer each item with a one-line reason in the valediction." find inbox -maxdepth 1 -type f \ ! -name '.gitkeep' \ ! -name 'lint-followups.org' \ ! -name 'PROCESSED-*' \ -printf ' %f\n' fi #+end_src If the count is zero or the project has no =inbox/= directory, the check is a silent no-op. If non-zero, the wrap is incomplete by default. The user resolves each item (process now, defer with reason in the valediction, or delete with rationale) before the validation checklist passes. The check exempts =lint-followups.org= explicitly because lint-org runs earlier in the same wrap-up workflow and writes its judgment items to that file in =inbox/= by design. The file is a pipeline artifact for the next morning's =daily-prep=, not a handoff that needs the value gate. This integrates with =process-inbox.org=, which stamps =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section on completion. Wrap-up doesn't double-stamp. It only ensures the inbox carries nothing but the expected pipeline artifacts at session end. *** Review-habit health check (surface a slipped daily task-review) The daily task-review habit walks the open top-level tasks on a rotating cycle, stamping =:LAST_REVIEWED:= as it goes (see =task-review.org=). This check is the watchdog for that habit. When tasks have gone too long unreviewed, the habit has slipped, and the wrap-up says so in one line — it does not re-list the tasks. =task-review-staleness.sh= counts top-level =[#A]= / =[#B]= / =[#C]= tasks (TODO/DOING/VERIFY) whose =:LAST_REVIEWED:= is missing or older than the threshold. Threshold 30 days is about 2.5 review cycles of slack at the default batch size — one missed week is fine, three weeks signals a problem. #+begin_src bash if [ -n "$LINT_ORG_FOLLOWUPS" ]; then followups="$LINT_ORG_FOLLOWUPS" elif [ -d "./inbox" ]; then followups="./inbox/lint-followups.org" else followups=".ai/lint-followups.org" fi if [ -f todo.org ]; then stale=$(.ai/scripts/task-review-staleness.sh todo.org 30 2>/dev/null || echo 0) if [ "$stale" -gt 0 ]; then printf "\n* %s — Task-review health: %s top-level [#A]/[#B]/[#C] tasks unreviewed for >30 days (daily review may have slipped)\n" \ "$(date '+%Y-%m-%d %a')" "$stale" >> "$followups" fi fi #+end_src A non-zero count writes one summary line and nothing else — the per-task walk is the review habit's job, not the wrap-up's. This supersedes the old date-coverage scan, which flagged every dateless =[#A]= / =[#B]= task on the wrong assumption that high-priority work needs a date. No-date is a valid resting state for research and watch-list tasks; staleness, not datelessness, is the real signal. ** Step 3.5: Linear ticket-state hygiene (skip if project doesn't use Linear) If the project uses Linear and has any tickets currently in *Dev Review* assigned to Craig, sweep them before the wrap-up commit. The check is fast and keeps the board honest — tickets stuck in Dev Review after their PR merges hide actual work-in-progress. #+begin_src mcp__linear__list_issues assignee="me" state="Dev Review" limit=50 #+end_src For each result, look up the linked PR (the =gitBranchName= field on the issue maps to a =headRefName= on the project's GitHub remote — use =gh pr list --author --state all --json number,state,headRefName,mergedAt,title=). *Assumption:* the =gh= lookup expects a GitHub-family host. It holds today because the only Linear-using project (DeepSat) lives on =deepsat.ghe.com=, where =gh= talks to the GHE API. A future Linear-using project on a non-GitHub host (GitLab, Gitea, Bitbucket) would need a provider-agnostic PR lookup here — update this step when that happens. If a Dev-Review ticket's PR is *merged*, propose a move: - *Done* — chores, refactors, test-coverage backfills, dead-code removal, e2e-flake fixes, anything with no PM-visible behavior change. PR titles prefixed =chore:=, =test:=, =refactor:=, =docs:= almost always belong here. - *PM Acceptance* — real behavior fixes or new features a PM (or end user) could verify by clicking through the app. PR titles prefixed =fix:=, =feat:= usually belong here unless the change is invisible to users. When in doubt, ask Craig per ticket. Don't auto-pick. After Craig confirms, move via =mcp__linear__save_issue= with =state="Done"= or =state="PM Acceptance"=. Several can run in parallel. Skip the step entirely if the project doesn't use Linear (e.g. personal projects, the rulesets repo). ** Step 4: Git commit + push *** Step 4.0: Commit template-sync churn first (consuming projects) The startup workflow's Phase A rsyncs template updates from rulesets into this project's =.ai/= (=protocols.org=, =workflows/=, =scripts/=) every session that rulesets has advanced. Nothing commits that churn, so without this step it accumulates across sessions and eventually blocks Phase A.0's auto-fast-forward (git refuses to ff a dirty tree). Commit it here, as its own =chore:= commit, before the session-work commit — so the sync stays separate from what the session actually shipped and the tree ends clean. The guard is conservative: only auto-commit a dirty synced path when it matches the rulesets canonical byte-for-byte (a modified/new file equals canonical, or a deletion pairs with a file retired upstream). If any synced path is dirty but /doesn't/ match canonical — a local hand-edit to a file that's supposed to be sync-managed — surface it and don't auto-commit. Anything outside the three synced paths is untouched here; the normal Step 4 commit and the worktree-leftover step handle it. #+begin_src bash # Skip in the rulesets repo itself: there .ai/ is a committed mirror of # claude-templates/.ai/, kept in sync by the pre-commit hook and committed # alongside template edits — not downstream sync churn. The presence of # claude-templates/.ai/ in this repo is the tell. if [ ! -d claude-templates/.ai ] && [ -d "$HOME/code/rulesets/claude-templates/.ai" ]; then canon="$HOME/code/rulesets/claude-templates/.ai" safe=1 commitlist=() while IFS= read -r line; do f="${line:3}" # strip the 2-char status + space rel="${f#.ai/}" if [ -e "$f" ] && [ -e "$canon/$rel" ] && diff -q "$f" "$canon/$rel" >/dev/null 2>&1; then commitlist+=("$f") # modified/new here, matches canonical elif [ ! -e "$f" ] && [ ! -e "$canon/$rel" ]; then commitlist+=("$f") # deleted here AND retired upstream else safe=0 # synced path dirty but != canonical fi done < <(git status --porcelain -- .ai/protocols.org .ai/workflows/ .ai/scripts/) if [ "$safe" -eq 1 ] && [ "${#commitlist[@]}" -gt 0 ]; then git add -- "${commitlist[@]}" git commit -q -m "chore: sync .ai tooling from templates" echo "wrap-up: committed ${#commitlist[@]} synced .ai file(s) as a template-sync chore." elif [ "$safe" -eq 0 ]; then echo "wrap-up: synced .ai paths are dirty but not all match rulesets canonical — NOT auto-committing. Resolve manually:" git status --porcelain -- .ai/protocols.org .ai/workflows/ .ai/scripts/ | sed 's/^/ /' fi fi #+end_src The commit isn't pushed here — the push step below pushes the current branch, which carries both this chore commit and the session-work commit. A crashed session that never reaches wrap-up leaves the churn for the next startup, which surfaces it (see startup.org Phase C) so it never silently accumulates. *** Review changes #+begin_src bash git status git diff --stat #+end_src Decide the scope of the wrap-up commit. Usually everything that changed during the session goes into one commit. If anything is intentionally not part of this session's work (pre-existing WIP, unrelated files), leave it out. *** Stage Add the renamed session file and all other session changes: #+begin_src bash git add .ai/sessions/ [other modified paths] #+end_src Do NOT blindly =git add .= — review what's being staged so unrelated dirty state isn't dragged in. *** Commit Commit message rules (also see protocols.org "Git Commit Requirements"): - Subject line: concise, describes what /shipped/. Use conventional prefixes (=docs:=, =refactor:=, =fix:=, =feat:=, =chore:=) — NEVER =session:=. - Body: 1-3 terse sentences describing what was accomplished. - NO Claude Code attribution. NO =Co-Authored-By=. NO references to =notes.org=, =session-context.org=, =.ai/sessions/=, "session wrap-up", or session timestamps. *Wrap-up commits skip the inline-approval gate.* The =commits.md= rule that requires writing the message to =/tmp/commit-.md=, printing inline, and waiting for an approve / request-changes / open-in-editor response does *not* apply to wrap-up commits. The wrap-up flow is meant to be quick — Craig has already authorized the wrap by triggering the workflow ("wrap it up"), and stopping again to approve a commit message disrupts the cadence. Still apply =/voice personal= silently before committing so the message reads cleanly. Just don't print and ask. Commit directly with the cleaned message. If a wrap-up commit needs Craig's eyes for a content reason (sensitive change, unusual scope, something he flagged earlier), surface it explicitly. Otherwise commit and move on. Example: #+begin_example docs: restructure docs/ to .ai/ and unify aix+hey into ai launcher Hidden .ai/ now holds Claude tooling; project-level docs/ reserved for user-facing docs. Single 'ai' launcher (fzf multi + smart tmux + git-aware fetch/pull) replaces the aix script and hey alias. #+end_example Use heredoc for multi-line: #+begin_src bash git commit -m "$(cat <<'EOF' subject line here body sentences here. EOF )" #+end_src *** Push to all remotes #+begin_src bash git remote -v #+end_src Push the current branch to every remote (some repos have multiple remotes — a primary host plus one or more mirrors, or different remotes for different audiences — and the loop keeps all of them current): #+begin_src bash current=$(git symbolic-ref --short HEAD) for r in $(git remote); do git push "$r" "$current"; done #+end_src Then push every other local branch with a tracking upstream to its tracking remote. This catches feature branches that advanced during the session but aren't the one being wrapped up — without it, work-in-progress branches stay local-only and are at risk if the machine dies before the next wrap-up. #+begin_src bash git for-each-ref --format='%(refname:short) %(upstream:remotename)' refs/heads/ | \ while read branch remote; do [ "$branch" = "$current" ] && continue if [ -z "$remote" ]; then echo " $branch: no tracking upstream — skipped (push manually with 'git push -u')" else git push "$remote" "$branch" fi done #+end_src Behavior: - *Tracked branches* → pushed to their upstream remote. - *Untracked branches* (no upstream set) → surfaced, not pushed. Craig sets the upstream manually with =git push -u = when he's ready. Auto-creating an upstream would commit to a remote choice the workflow can't make safely. - *Diverged or rejected pushes* → surface and stop. Don't force-push from this workflow; resolve manually. *** Resolve every worktree leftover #+begin_src bash git status --short #+end_src *Default policy: end every session with an empty =git status=.* The wrap is incomplete while anything remains dirty. There is no "leave it alone" default — every leftover gets an active resolution. The only way for a file to stay dirty across the wrap is the user explicitly saying "defer this one, leave it dirty." Surface each leftover with a concrete recommendation; the user has to actively opt out for the dirt to persist. This inverts the older "intentional carryover" default, which let pre-existing dirty state accumulate across sessions silently. Carryover that lives for days or weeks is almost always one of: a forgotten commit from a prior wrap, a stale change that should be discarded, or genuine in-flight work that needs an explicit stash/branch home. None of those should default to "leave it dirty." **** Three kinds of leftover | Pattern | What it is | Recommended action (apply unless user defers) | |---+---+---| | Generated, runtime, or lock files that no human edits — e.g., =.claude/scheduled_tasks.lock=, =.pytest_cache/=, build outputs, IDE state, editor swap files | *Runtime artifact* — created by tooling or the harness, not by the user, and shouldn't be tracked | Add the matching pattern to =.gitignore= (project-level, not =~/.gitignore_global=). For tracked files, =git rm --cached =. Stage =.gitignore= and any =rm --cached= changes in *one* follow-up commit (=chore: gitignore X=), push. Re-run =git status= to confirm clean. | | Modified or created during the session but not staged into the wrap-up commit | *Forgotten change* — real session work that should have been in the wrap commit but missed it | Stage and create a follow-up commit. Don't =--amend= the wrap-up commit once pushed (diverging history without a clear win). Push the follow-up to all remotes. | | Was dirty at session start and still dirty at session end — work this session deliberately didn't touch | *Pre-existing dirt that needs a decision* — could be a missed commit from a prior wrap, stale abandoned work, or real in-flight work without a home | Investigate (show diff + check the originating session). Recommend one of: (a) commit now if the work is complete, (b) stash with a descriptive message if it's genuine WIP, (c) =git checkout -- = / =git clean -f = if stale and unwanted, (d) move to a feature branch if it's longer-running, (e) user explicitly defers and accepts the dirt. Do not silently leave dirty. | **** Per-file flow For each leftover line in =git status --short=: 1. Identify which of the three kinds above it matches. 2. State what the file is (one line) and the recommended action. 3. Apply the action unless the user explicitly defers. 4. Re-run =git status --short= after each follow-up commit until empty (or until every remaining line is an explicit user-deferred entry). The pre-existing-dirt case (third row) is the one this rule most cares about. Treat each pre-existing-dirty file as a question that must get an answer this session, not as "carryover that's fine to inherit." A file that was dirty for a week before this session probably isn't going to get cleaner by waiting another week. Look at the diff, check the originating session's notes, and recommend a real resolution. **** When the user defers If the user does say "leave this one dirty for now" after seeing the recommendation, that is fine — log the deferral in the valediction so the next session knows it was an explicit choice, not a miss. Format: "Deferred (per Craig's decision today): =path/to/file= — ". Without that note, the next session can't distinguish "we agreed to defer" from "we forgot again." ** Step 5: Valediction Brief, warm closing. 3-4 sentences max. Include: - What was accomplished (specific, not generic) - What's ready for next session - Any critical reminders or deadlines Tone: warm but professional. No emoji unless Craig has explicitly requested. Acknowledge effort when session was long or difficult. Example: #+begin_example That's a wrap. Today we restructured the entire claude-templates ecosystem: docs/ → .ai/ across all 23 projects, unified aix + hey into a single 'ai' launcher with git-aware fetch/pull, and cleaned up 4 code projects on velox. Both machines fully in sync. Two things to pick up next: the chime README WIP (your inline notes from earlier) and archsetup's layout-navigate tests. Both are ratio-local uncommitted state. Good session. Talk tomorrow. #+end_example * Common Mistakes to Avoid 1. *Skipping Step 1 (Summary)* — the file becomes the record; an empty Summary makes it hard to scan at catch-up 2. *Vague description in filename* — =2026-04-20-updates.org= is useless next to =2026-04-20-13-45-docs-ai-migration.org= 3. *=git add .= without review* — drags in unrelated dirty state 4. *=session:= prefix in commit message* — explicitly forbidden; use real change categories 5. *Claude-tooling references in commit message* — describes tooling, not what shipped 6. *Forgetting to push to all remotes* — check =git remote -v=, push to each 7. *Leaving =.ai/session-context.org= in place* — its presence means "interrupted session", confuses next startup 8. *Long preachy valediction* — brief beats thorough 9. *Leaving runtime/generated files dirty without gitignoring them* — pollutes every future =git status= and erodes trust in "working tree clean" as a signal. Fix =.gitignore= during the wrap, not later. 10. *Treating "was dirty at session start, still dirty now" as fine by default* — that's how a forgotten commit from two sessions ago turns into "carryover" for two weeks. Every pre-existing dirty file needs an active resolution recommendation this session. Deferral is allowed only with an explicit user choice, logged in the valediction. * Validation Checklist Before considering wrap-up complete: - [ ] =.ai/session-context.org= =* Summary= section populated - [ ] The Summary ends with the =KB: promoted N / consulted yes-no= line (promotion check ran) - [ ] File renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= - [ ] =.ai/session-context.org= no longer exists - [ ] =todo-cleanup.el= ran — hygiene pass + =--archive-done= + =--sync-child-priority= (if =todo.org= exists at project root) - [ ] =lint-org.el= ran on =todo.org= — mechanical fixes applied, judgments appended to follow-ups file (if =todo.org= exists) - [ ] Any orphan-planning-line warnings reviewed (fix or accept) - [ ] Inbox carries nothing but expected pipeline artifacts (=.gitkeep=, =lint-followups.org=, =PROCESSED-*= prefixes), OR each remaining handoff has an explicit deferral logged in the valediction - [ ] Linear Dev-Review sweep ran; any merged-PR tickets moved to Done or PM Acceptance (skip if project doesn't use Linear) - [ ] Template-sync churn committed as its own =chore: sync .ai tooling from templates= (consuming projects only; skipped in rulesets), or surfaced if a synced path didn't match canonical - [ ] After wrap-up commit + push, =git status --short= is empty OR every remaining line has an explicit user-deferred decision logged in the valediction - [ ] Each leftover was investigated and the user saw a concrete resolution recommendation - [ ] Runtime artifacts added to =.gitignore=, follow-up commit pushed, =git status= re-verified - [ ] Forgotten changes committed in a follow-up and pushed - [ ] Pre-existing dirty files resolved (committed / stashed / discarded / moved to a feature branch) or explicitly deferred with a one-line reason in the valediction - [ ] Current branch pushed to ALL remotes (verified with =git remote -v=) - [ ] All other local branches with a tracking upstream pushed to their remote - [ ] Any untracked-upstream branches surfaced for manual =git push -u= - [ ] Commit message follows format (no =session:=, no Claude attribution) - [ ] Valediction delivered (brief, specific, warm)