aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows/wrap-it-up.org
blob: 2d79795e6d3d03e3a2dddd4b3cee06dc3601d417 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
#+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=<path>= 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 <github-login> --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-<slug>.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 <remote> <branch>= 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 <path>=. 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 -- <path>= / =git clean -f <path>= 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= — <one-line reason>". 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)