aboutsummaryrefslogtreecommitdiff
path: root/working/wrapup-temp-cleanup
diff options
context:
space:
mode:
Diffstat (limited to 'working/wrapup-temp-cleanup')
-rw-r--r--working/wrapup-temp-cleanup/proposed.diff19
-rw-r--r--working/wrapup-temp-cleanup/wrap-it-up.org.proposed636
2 files changed, 655 insertions, 0 deletions
diff --git a/working/wrapup-temp-cleanup/proposed.diff b/working/wrapup-temp-cleanup/proposed.diff
new file mode 100644
index 0000000..5c2375c
--- /dev/null
+++ b/working/wrapup-temp-cleanup/proposed.diff
@@ -0,0 +1,19 @@
+--- claude-templates/.ai/workflows/wrap-it-up.org 2026-07-19 21:07:49.848788027 -0500
++++ /tmp/wu.org 2026-07-23 02:48:49.759571128 -0500
+@@ -189,6 +189,16 @@
+ emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org
+ #+end_src
+
++*** Clear temp/
++
++#+begin_src bash
++[ -d temp ] && find temp -mindepth 1 -delete && echo "temp/ cleared"
++#+end_src
++
++=temp/= holds throwaway artifacts — discarded prototypes, scratch output, intermediate data (see =working-files.md=). It's gitignored in every project, so nothing here rides a commit and nothing is recoverable from git once deleted. Clearing it at wrap is what keeps ephemeral work from silting up across sessions, and it's the counterpart to =working/=, which is tracked and *never* cleared here.
++
++Two guards. Confirm before deleting if =temp/= holds anything a reasonable reader would call in-progress rather than throwaway — misfiled work belongs in =working/=, so move it there instead of deleting it. And skip the step entirely in a project where =temp/= is not gitignored, since that means the project is using the directory for something else.
++
+ *** Sync child priorities
+
+ #+begin_src bash
diff --git a/working/wrapup-temp-cleanup/wrap-it-up.org.proposed b/working/wrapup-temp-cleanup/wrap-it-up.org.proposed
new file mode 100644
index 0000000..103e225
--- /dev/null
+++ b/working/wrapup-temp-cleanup/wrap-it-up.org.proposed
@@ -0,0 +1,636 @@
+#+TITLE: Session Wrap-Up Workflow
+#+AUTHOR: Craig Jennings
+#+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. A bare wrap also tears the session down (kills the ai-term buffer + tmux session, restoring geometry); a qualified wrap keeps the buffer, and a shutdown wrap powers the machine off. The teardown variants are set by the trigger phrase (see Teardown mode below) and act only at the very end, in Step 6.
+
+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, ending with =session wrapped.= on its own line as the signoff marker.
+
+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.
+
+* Teardown mode (set from the trigger phrase)
+
+The wrap itself — Steps 1 through 5 — is identical in every mode. The trigger phrase only decides what Step 6 does once commit + push and the valediction are done. Resolve the mode from the phrase before starting:
+
+- *Teardown* (the default) — bare "wrap it up", "that's a wrap", "let's call it a wrap". The full wrap, then Step 6 kills the ai-term buffer + the =aiv-<project>= tmux session (which takes =claude= with it) and restores the saved window geometry. This is Craig's typical end-of-day case.
+- *No-teardown* — "wrap it up with summary" or "wrap it up and summarize". The full wrap, but Step 6 leaves the buffer and session intact so the summary stays readable. The explicit qualifier is what opts out of teardown.
+- *Shutdown* — "wrap it up and shutdown". The full wrap, then Step 6 gates on this being the only live ai-term session and powers the machine off. Shutdown supersedes teardown (killing the buffer is moot if the box is going down).
+
+Why teardown waits for Step 6 and runs through a hook, never inline: teardown kills the very tmux session =claude= runs in, so an inline kill would cut the valediction off before it renders. Step 6 instead drops a sentinel after everything else is verified, and the =Stop= hook (=ai-wrap-teardown.sh=) does the actual teardown when this response ends — by which point the valediction has already been delivered.
+
+This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-quit=, =cj/ai-term-live-count=, =cj/ai-term-shutdown-countdown=) and on the =Stop= hook being wired in =settings.json= (=hooks/settings-snippet.json=). If =emacsclient= or the daemon is unreachable, the sentinel is cleared and the session simply stays up — teardown degrades to a no-op, never a wedge.
+
+* The Workflow
+
+** Step 0: Refuse if sentry is live
+
+Before anything else, check whether sentry is running in this project. Sentry holds the working tree on its =sentry/<date>-<host>= branch and commits unattended; wrapping underneath it would archive the session anchor and tear down the buffer while the loop is still firing into it. If sentry's single-runner lock is held, stop and point at the shutdown path:
+
+#+begin_src bash
+proj="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")"
+if [ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock status "sentry-$proj" | grep -q '^held'; then
+ echo "sentry is active — say 'stop sentry' first"
+ exit 1
+fi
+#+end_src
+
+The stop-sentry operation (defined in =sentry.org=) owns the shutdown: it cancels the loop, disposes of the branch, and walks the approval queue. Wrap-up carries only this one guard; a =stale= lock (a crashed fire) doesn't block — only a live =held= lock does.
+
+** Step 1: Finalize the Summary
+
+*** Work the Before-Close Queue (before the Summary)
+
+If the session anchor (=.ai/session-context.org=) carries a =* Before-Close Queue= heading with items, work them now, oldest-first, before writing the Summary, so any resulting edits ride this wrap's commit and get described in it. The queue is the "put X on the list" shorthand (see =protocols.org=, Colloquialisms and Expansions): session-scoped work Craig deferred to wrap time.
+
+Per item: do it if it's clear and bounded, or promote it to a =todo.org= task if it turns out to need its own session. Never drop an item silently. Remove each line as it's handled; if one can't be finished, surface it in the valediction (Step 5) and either leave a follow-up task or state why it's dropped.
+
+If there's no =* Before-Close Queue= heading, or it's empty, this step is a silent no-op.
+
+*** Early KB reflection (capture while fresh, before the Summary)
+
+Before distilling the Summary, while the session is still fresh, ask: what did this session learn worth remembering, for yourself or a future agent? Reflect and stage any candidate durable facts — a decision and its why, an environment gotcha, a reference pointer, a transferable lesson. Self-answer silently; this adds no interactive turn (Craig already authorized the wrap). The candidates flow straight into the KB promotion check below, which does the actual writing and the receipt — this is the capture half, that is the commit half, one pipeline, one receipt. Reflecting here rather than reconstructing learnings after the Summary is the point: the early ask is what keeps the receipt from defaulting to "promoted 0" out of fatigue.
+
+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 and unique per run; the recommended =host.project.runtime.<epoch>= shape is both. The epoch on the tail keeps a re-run of the same logical agent from resolving to a prior run's leftover anchor. See protocols.org "Agent-scoped path".)
+
+** 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 roam mode)
+
+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.org][inbox.org]] roam mode 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 roam mode's Phase B–D (file each into =todo.org=, then remove them from the shared inbox and let =roam-sync= commit + push the edit). Report the total count and how many appeared related to this project, per roam mode'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
+
+*** Convert done sub-tasks to dated entries
+
+#+begin_src bash
+[ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks todo.org
+#+end_src
+
+=--convert-subtasks= rewrites every heading at level 3 or deeper whose TODO state is DONE/CANCELLED/FAILED into a dated event-log entry (=<stars> YYYY-MM-DD Day @ HH:MM:SS -ZZZZ <text>=), dropping the keyword, priority cookie, and tags, and removing the now-redundant =CLOSED:= line. This enforces the =todo-format.md= depth rule that a completed *sub-task* (a heading under a parent task) becomes dated history, not a lingering DONE keyword — a shape an interactive org close (=org-log-done= → DONE + CLOSED) never applies and =--archive-done= (level-2 only) never reaches. The timestamp comes from each entry's own =CLOSED= cookie; a date-only close yields =00:00:00=. Heading text is kept verbatim. Idempotent (an already-dated heading has no keyword to match), and a done sub-task with no parseable =CLOSED= is flagged and left alone rather than stamped with a fabricated date.
+
+Run this *before* =--archive-done= so that when a completed level-2 parent is archived, its sub-tasks already carry their dated form. Any rewrites show up in the wrap-up commit's diff for review before push.
+
+Preview without writing:
+
+#+begin_src bash
+emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks --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
+
+*** Clear temp/
+
+#+begin_src bash
+[ -d temp ] && find temp -mindepth 1 -delete && echo "temp/ cleared"
+#+end_src
+
+=temp/= holds throwaway artifacts — discarded prototypes, scratch output, intermediate data (see =working-files.md=). It's gitignored in every project, so nothing here rides a commit and nothing is recoverable from git once deleted. Clearing it at wrap is what keeps ephemeral work from silting up across sessions, and it's the counterpart to =working/=, which is tracked and *never* cleared here.
+
+Two guards. Confirm before deleting if =temp/= holds anything a reasonable reader would call in-progress rather than throwaway — misfiled work belongs in =working/=, so move it there instead of deleting it. And skip the step entirely in a project where =temp/= is not gitignored, since that means the project is using the directory for something else.
+
+*** 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 \
+ --fix --followups-file="$followups" todo.org
+#+end_src
+
+The =--fix= flag is required for the writes: lint-org's CLI default is
+report-only (a linter reports, it doesn't write), and this wrap-up pass is
+the deliberate exception that applies fixes — its diff rides the wrap-up
+commit for review.
+
+=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 =inbox.org= process mode 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 inbox.org process mode 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 =inbox.org= process mode, 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.
+
+*** Cross-project router (optional — route filed keepers to their home projects)
+
+Runs directly after the inbox sanity check. The split between the two: the sanity check *gates* the wrap (a dirty inbox blocks until resolved); the router is *optional* (skipping it never blocks anything — the candidates just stay local until a future wrap). Spec: =docs/specs/wrapup-routing-spec.org= (D7/D8/D9).
+
+The candidate set is exactly the local tasks carrying a =:ROUTE_CANDIDATE:= property — keepers that inbox process mode filed this session whose inferred home is another project. Never scan the standing backlog.
+
+#+begin_src bash
+.ai/scripts/route-batch --list
+#+end_src
+
+*Empty set = zero interaction.* =--list= prints nothing when there are no candidates; continue the wrap silently — no prompt, no "0 items" line.
+
+When candidates exist, surface the batch as one line per task — the task heading, the destination project, the delivery mode (=inbox-send= file handoff), and the engine's confidence — then offer exactly two options: *go* (route the whole batch) or *skip* (leave everything local). Derive each confidence label by running the engine on the task's heading + body (=python3 .ai/scripts/route_recommend.py --item "..." --exclude "$(basename "$PWD")"=); label weak matches visibly ("weak — verify the destination") so a low-confidence route gets a human glance before the keystroke.
+
+On *go*:
+
+#+begin_src bash
+.ai/scripts/route-batch --go
+#+end_src
+
+Per candidate, the helper writes the task's subtree (children ride along; =:ROUTE_CANDIDATE:= stripped, headings promoted to top level) to a one-task handoff, delivers it via =inbox-send <destination> --file= (so the =from-<this-project>= provenance is stamped and the destination's inbox process mode dispositions it as a single item), and only after a successful send removes the subtree from the local =todo.org= — a single-file local edit the wrap is already committing. A failed send leaves that task in place and exits non-zero; report it and continue the wrap. Never write the destination's =todo.org= directly; its own inbox processing files the task per its conventions.
+
+On *skip*, leave every candidate in place, marker included — they resurface next wrap.
+
+Mis-routes are recoverable: the receiving project rejects via inbox process mode's reject-from-another-project flow, which returns the item to this project's inbox with the rationale. That reject path is why removing the local source on send is safe.
+
+*** 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.
+
+End on a clear signoff: the *last* line of the valediction is always =session wrapped.= on its own line (lowercase, with the period, nothing after it). It's the unmistakable end-of-session marker, so don't trail it with another sentence. This is the last user-facing output — Step 6's teardown is silent.
+
+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.
+
+session wrapped.
+#+end_example
+
+** Step 6: Session teardown (mode-dependent)
+
+The last action of the wrap, and only after Step 4's commit + push is verified and the Step 5 valediction is composed. The teardown itself happens when this response ends (via the =Stop= hook), so the valediction always renders first. Act by the mode resolved up front:
+
+*** No-teardown mode
+
+Do nothing. The buffer, the =aiv-<project>= tmux session, and =claude= all stay up so the summary stays readable. The wrap is complete.
+
+*** Teardown mode (default)
+
+Confirm commit + push succeeded (Exit Criteria 5 — never tear down over unpushed work), then drop the sentinel:
+
+#+begin_src bash
+touch "/tmp/ai-wrap-teardown-$(basename "$PWD")"
+#+end_src
+
+That is the whole step. Don't run any =tmux kill-session=, =emacsclient=, or buffer kill inline — the =Stop= hook reads the sentinel when this response ends and runs =cj/ai-term-quit=, which kills the =aiv-<project>= session (taking =claude= with it), kills the vterm buffer, and restores geometry. The basename of =$PWD= is the key the hook matches, so the sentinel names the session it tears down.
+
+*** Shutdown mode
+
+Confirm commit + push succeeded, then evaluate the safety gate *before* committing to the shutdown — never power the box off out from under another live session:
+
+#+begin_src bash
+emacsclient -e '(cj/ai-term-live-count)'
+#+end_src
+
+- *Count > 1* — another ai-term session is alive. ABORT the shutdown. List the other live =aiv-*= sessions, drop *no* sentinel, and tell Craig in the valediction that it fell back to a normal wrap (no poweroff, no teardown). This gate is the load-bearing safety of the whole feature.
+- *Count = 1* — this session is the only one. Drop the shutdown sentinel:
+
+ #+begin_src bash
+ touch "/tmp/ai-wrap-shutdown-$(basename "$PWD")"
+ #+end_src
+
+ The =Stop= hook fires =cj/ai-term-shutdown-countdown= when this response ends: it re-checks the gate, runs an abort-able 10→1 countdown in the Emacs echo area (=C-g= cancels), then =sudo shutdown now=. Shutdown supersedes teardown — do *not* also drop the teardown sentinel.
+
+If =emacsclient= isn't resolvable or the daemon is down, the gate can't run — abort the shutdown, fall back to a normal wrap, and say so. Don't power off on an unverifiable gate.
+
+* 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 + =--convert-subtasks= + =--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=
+- [ ] Step 6 teardown matches the trigger phrase: no-teardown leaves the buffer; teardown drops only =/tmp/ai-wrap-teardown-<project>=; shutdown gates on =cj/ai-term-live-count= = 1 and drops only =/tmp/ai-wrap-shutdown-<project>=
+- [ ] No teardown/shutdown sentinel was dropped before commit + push was verified
+- [ ] Shutdown aborted (fell back to normal wrap, logged in the valediction) when another =aiv-*= session was live or the gate couldn't run
+- [ ] Commit message follows format (no =session:=, no Claude attribution)
+- [ ] Valediction delivered (brief, specific, warm)