aboutsummaryrefslogtreecommitdiff
path: root/working
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-23 08:25:40 -0500
committerCraig Jennings <c@cjennings.net>2026-07-23 08:25:40 -0500
commit01356fa036847f9d216dd92a006049105c2d5461 (patch)
tree4034b455c1a98a8b67e5ebb5ba8976e707055fc6 /working
parent33949c50b7d2b3ab01af4fe91119cc290fae9f01 (diff)
downloadrulesets-01356fa036847f9d216dd92a006049105c2d5461.tar.gz
rulesets-01356fa036847f9d216dd92a006049105c2d5461.zip
chore: file eight bug findings and stage four shared-asset proposals
Overnight hygiene sweep of this repo. Everything here is a finding or a staged proposal. No rule, workflow, script, or bundle file was modified. The one that matters: the python and typescript language bundles ship no pre-commit hook, so a project installing either gets no credential scan on commit. Both bundles predate the hook rollout that swept go and bash, and install-lang skips missing components without a word, so it went unnoticed for two months. Live on two projects, one of them work. Filed [#A]. Seven more findings landed. The largest is a non-atomic write in the cross-project inbox tool, which can strand an empty handoff in another project's inbox and block a turn there. The rest are lint noise and two gaps in the Signal channel. An eighth was filed and then retracted the same night, once a retest showed I had compared two different files and read the difference as a bug. Four proposals from other projects are staged under working/ with verified diffs, each behind a VERIFY task, waiting on a decision.
Diffstat (limited to 'working')
-rw-r--r--working/interaction-no-markup/interaction.md.proposed70
-rw-r--r--working/interaction-no-markup/proposal-from-org-drill.org5
-rw-r--r--working/interaction-no-markup/proposed.diff11
-rw-r--r--working/notes-template-lint/notes.org.proposed86
-rw-r--r--working/notes-template-lint/proposal-from-smoke.org5
-rw-r--r--working/notes-template-lint/proposed.diff38
-rw-r--r--working/sentry-living-document/feedback-from-archangel.org5
-rw-r--r--working/sentry-living-document/feedback-from-takuzu.org5
-rw-r--r--working/sentry-living-document/proposed.diff32
-rw-r--r--working/sentry-living-document/sentry.org.proposed219
-rw-r--r--working/wrapup-temp-cleanup/proposed.diff19
-rw-r--r--working/wrapup-temp-cleanup/wrap-it-up.org.proposed636
12 files changed, 1131 insertions, 0 deletions
diff --git a/working/interaction-no-markup/interaction.md.proposed b/working/interaction-no-markup/interaction.md.proposed
new file mode 100644
index 0000000..caa4d49
--- /dev/null
+++ b/working/interaction-no-markup/interaction.md.proposed
@@ -0,0 +1,70 @@
+# Interaction Style
+
+Applies to: `**/*`
+
+How the agent communicates with the user during a session — choice prompts, status updates, decision points.
+
+## No Popup Menus for Choices
+
+When the agent needs the user to pick between options, **do not** use the AskUserQuestion popup. Present the options inline in chat as a numbered list and ask the user to reply with a number.
+
+**Why:** The popup menu UI sits at the bottom of the chat window and obscures the chat content directly above it — exactly the area the user needs to read to make the choice. Inline numbered options keep the question, the surrounding context, and the proposed text all visible in the same scrollback.
+
+**How to apply:**
+
+For approve / changes / cancel flows (commit-message review, PR-description review, plan approval), draft inline:
+
+```
+1. Approve — commit now
+2. Request changes — tell me what to adjust
+3. Open in editor — emacsclient -n /tmp/...
+
+Pick a number.
+```
+
+For pick-one decisions, same shape: numbered list, one-line prompt at the end.
+
+For multi-select decisions, say so explicitly: "Pick any combination — reply with the numbers."
+
+**Render-merge guard.** GFM renderers merge adjacent ordered lists and renumber them — a message with a 1-3 content list followed by a 1-3 options list rendered as options 4-6 on the user's screen, and his pick ("4") didn't exist in the author's numbering (work session, 2026-06-10). Numerals belong to the options list only: any other enumeration in the same message uses dashes or prose, and a short prose lead-in line sits directly above the options so no renderer can merge them. When the user picks a number outside the offered range, suspect the render-merge and ask which item text they meant rather than treating it as an error.
+
+Reserve `AskUserQuestion` only when the user explicitly asks for the popup form ("use the popup for this one") or for genuinely free-form input where numbered options don't fit.
+
+This rule applies to all three approval gates in the `commits.md` publish flow (commit message, PR description, PR review reply): print the draft inline, then offer numbered approve / changes / edit options inline. Do not switch to the popup form for the gate even though the prior protocol referenced it.
+
+### Order options with the recommendation at item 1
+
+When the question has a clear recommended option, put it at item 1. The other paths stay visible so the user can override without having to type a custom answer, but the common case collapses to a single keystroke.
+
+Default to this pattern whenever the user needs to weigh a genuine choice — task-review actions, brainstorm decisions, judgment calls, approve/edit/cancel gates, "what should I do next" prompts. Skip it only when the question is free-form (open prose, names, dates), when the user has already issued a directive and a single confirmation suffices, or when there is no clearly-recommended option (in which case state that plainly: "I don't have a recommendation here — what's your read?").
+
+Include a "Skip" / "Defer" / "Tell me how" as the last option when the user's answer might be "none of these."
+
+The convention reinforces the popup-denial rule above by giving every inline choice list a single canonical shape, and it forces the model to actually pick a recommendation rather than offering a neutrally-ordered enumerate-and-defer list. A neutrally-ordered list is a covert way to push the decision back; recommendation-first puts skin in the game.
+
+**Enforcement:** a global `PreToolUse` hook (matcher `AskUserQuestion`) in `~/.claude/settings.json` hard-denies the popup and returns this rule as the reason — the prose alone proved too easy to forget. Because the deny is unconditional, the "use the popup for this one" exception above can't be honored in-turn; to get the popup, disable the hook via `/hooks` (or edit settings) first.
+
+## No Reverse-Video Highlighting in Chat Output
+
+In conversational output to the user, do not use Markdown bold (`**...**`) or inline-code spans (backtick `` `...` ``). The user's terminal renders both as reverse video, which is hard to read on the display.
+
+**Why:** The styling that looks like emphasis in most renderers comes through as inverted foreground/background here. Plain prose, plain identifiers, and plain key chords read cleanly; the "highlight" actively hurts legibility.
+
+**How to apply:**
+
+- Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s.
+- Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine.
+- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path.
+
+This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation.
+
+## Showing Craig Visuals
+
+Craig runs Claude Code inside Emacs EAT (through tmux). EAT renders SendUserFile and inline terminal images as an `[image] path.png` text line — the visual itself never appears. In one session ~20 renders went out that way and Craig approved UI he had never seen (takuzu, 2026-07-11). Never rely on SendUserFile or inline image display to show a visual. SendUserFile stays fine for *delivering* a file; it just doesn't display one.
+
+Two display lanes, by what the visual is for:
+
+- **Durable or interactive visuals** — HTML prototypes, full renders, anything Craig will study or click: open with `gui-open <abs-path>` (dotfiles-shipped; detaches through `systemd-run --user` so the agent shell can't reap it, resolves the Hyprland instance after a compositor restart, and confirms the window is actually visible before returning). It auto-detects image vs HTML; `--image` / `--browser` force. Place it off Craig's active workspace and name it, per `desktop-capture.md`.
+- **Quick inline glances** — a chart, a did-it-render check: sixel in the terminal. Encode with ImageMagick (`magick <img> sixel:<out>`; img2sixel silently emits zero bytes on some builds) and display in a separate tmux window (`tmux new-window -d -n <name>`, then `tmux send-keys -t <name> "clear; cat <out>" Enter`, tell Craig the window name) rather than the Claude Code pane, whose TUI repaints over anything drawn into it. With native tmux sixel active, the image lives in tmux's grid and survives window switches, scrolling, and resizing.
+
+**Capability gate.** Native sixel needs two config pieces: EAT answering the XTWINOPS cell-size query (patched eat.el, owned by .emacs.d) and `terminal-features 'xterm*:sixel'` in tmux.conf (owned by dotfiles). Check before relying on it: `tmux display -p '#{client_cell_width}'` — nonzero means go; 0 means the chain is missing a piece, so fall back to the browser lane. (Diagnosed 2026-07-13: tmux won't transmit sixel until it knows the client's cell pixel size, and stock EAT 0.9.4 silently ignores the CSI 14 t query tmux uses to ask.)
diff --git a/working/interaction-no-markup/proposal-from-org-drill.org b/working/interaction-no-markup/proposal-from-org-drill.org
new file mode 100644
index 0000000..09f0845
--- /dev/null
+++ b/working/interaction-no-markup/proposal-from-org-drill.org
@@ -0,0 +1,5 @@
+#+TITLE: Proposal: sharpen interaction.md's 'No Reverse-Video Highlig
+#+SOURCE: from org-drill
+#+DATE: 2026-07-21 00:06:48 -0500
+
+Proposal: sharpen interaction.md's 'No Reverse-Video Highlighting in Chat Output' rule. Craig's feedback in org-drill (2026-05-30, saved as a harness memory there): chat output must have zero markup — no code fences, no bold, no backtick spans, ever ('always always list it out without markup'). The current rule bans bold and inline-code spans but says fenced code blocks 'are acceptable when the user explicitly wants a block to copy'. Craig's directive tightens that: default to plain text always; no fences either. Suggest updating the rule text so every project inherits it instead of it living as one project's memory. Evidence: org-drill memory file no-markup-chat-output.md and the 2026-05-30 session log there.
diff --git a/working/interaction-no-markup/proposed.diff b/working/interaction-no-markup/proposed.diff
new file mode 100644
index 0000000..f5d1830
--- /dev/null
+++ b/working/interaction-no-markup/proposed.diff
@@ -0,0 +1,11 @@
+--- claude-rules/interaction.md 2026-07-19 15:41:37.382769288 -0500
++++ /tmp/im.md 2026-07-23 02:47:43.771850521 -0500
+@@ -54,7 +54,7 @@
+
+ - Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s.
+ - Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine.
+-- Fenced code blocks (triple backtick) are acceptable when the user explicitly wants a block to copy — they don't invert the way inline spans do. Default to plain text otherwise.
++- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path.
+
+ This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation.
+
diff --git a/working/notes-template-lint/notes.org.proposed b/working/notes-template-lint/notes.org.proposed
new file mode 100644
index 0000000..311a86f
--- /dev/null
+++ b/working/notes-template-lint/notes.org.proposed
@@ -0,0 +1,86 @@
+#+TITLE: Claude Code Notes - [Project Name]
+#+AUTHOR: Craig Jennings
+#+DATE: [Date]
+
+* About This File
+
+This file contains project-specific information for this project.
+
+- When to read this:
+- At the start of EVERY session (after reading protocols.org)
+- When needing project context or history
+- When checking reminders or pending decisions
+
+- What's in this file:
+- Project-specific context and goals
+- Pending decisions
+- Active reminders
+
+- Session history is NOT in this file. Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5.
+
+- For protocols and conventions, see [[file:protocols.org][protocols.org]].
+
+* Project-Specific Context
+
+This is where context regarding this project is placed.
+
+Examples of what goes here:
+- Project overview and goals
+- People and their relationships to the user
+- Contact information for companies and people
+- Current state of the project
+- Status report
+- Links to important documents
+- Technical architecture notes
+- Key decisions and rationale
+
+** If this is the first session
+
+Run the first-session workflow: [[file:workflows/first-session.org][first-session.org]]. It walks through git/.ai policy, project orientation, and initializing this notes.org. Remove this heading once done.
+
+* PENDING DECISIONS
+
+This section tracks decisions that need Craig's input before work can proceed.
+
+**Instructions:**
+- Add pending decisions as they arise during sessions
+- Format: =** [Topic/Feature Name]=
+- Include: What needs to be decided, options available, why it matters
+- Remove decisions once resolved (the resolution is captured in the Session Log of the session where it was resolved)
+
+- Example format:
+#+begin_example
+,** Feature Name or Topic
+
+Craig needs to decide on [specific question].
+
+Options:
+1. Option A - [brief description, pros/cons]
+2. Option B - [brief description, pros/cons]
+
+Why this matters: [impact on project]
+
+Implementation is ready - just need Craig's preference.
+#+end_example
+
+** Current Pending Decisions
+
+(None currently - will be added as they arise)
+
+* Active Reminders
+
+** Current Reminders
+
+(None currently - will be added as needed)
+
+** Instructions for This Section
+
+When Craig says "remind me" about something:
+1. Add it here with timestamp and description
+2. If it's a TODO, also add to =/home/cjennings/org/roam/inbox.org= scheduled for today
+3. Check this section at start of every session
+4. Remove reminders once addressed
+
+Format:
+- =[YYYY-MM-DD]= Description of what to remind Craig about
+
diff --git a/working/notes-template-lint/proposal-from-smoke.org b/working/notes-template-lint/proposal-from-smoke.org
new file mode 100644
index 0000000..26b458a
--- /dev/null
+++ b/working/notes-template-lint/proposal-from-smoke.org
@@ -0,0 +1,5 @@
+#+TITLE: Proposal: clean four lint-org flags in the notes.org templat
+#+SOURCE: from smoke
+#+DATE: 2026-07-21 06:49:22 -0500
+
+Proposal: clean four lint-org flags in the notes.org template (claude-templates/.ai/notes.org). A sentry pass over smoke's synced copy flagged: (1) two 'possibly misplaced heading' judgments on the boilerplate lines that open with org bold at column 0 ('**When to read this:**' and '**What's in this file:**' in the About This File section) — org can parse a line-initial ** as a level-2 heading; rephrasing (a list dash, or moving the bold off line start) would clear it everywhere. (2) two 'possible incomplete block' judgments around the Pending Decisions instructions' #+begin_example block that shows the entry format — likely the example content confusing block parsing; worth a look at whether the example should escape its own markers (e.g. a comma-escaped ,** or ,#+ inside the block). Both are cosmetic — no behavior breaks — but every project inherits the lint noise on each sweep, so one canonical fix beats N local ones. No local edit made in smoke; its copy stays template-faithful.
diff --git a/working/notes-template-lint/proposed.diff b/working/notes-template-lint/proposed.diff
new file mode 100644
index 0000000..627741f
--- /dev/null
+++ b/working/notes-template-lint/proposed.diff
@@ -0,0 +1,38 @@
+--- claude-templates/.ai/notes.org 2026-07-09 13:57:29.814830969 -0500
++++ /tmp/nt.org 2026-07-23 02:48:03.165968767 -0500
+@@ -6,19 +6,19 @@
+
+ This file contains project-specific information for this project.
+
+-**When to read this:**
++- When to read this:
+ - At the start of EVERY session (after reading protocols.org)
+ - When needing project context or history
+ - When checking reminders or pending decisions
+
+-**What's in this file:**
++- What's in this file:
+ - Project-specific context and goals
+ - Pending decisions
+ - Active reminders
+
+-**Session history is NOT in this file.** Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5.
++- Session history is NOT in this file. Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5.
+
+-**For protocols and conventions, see:** [[file:protocols.org][protocols.org]]
++- For protocols and conventions, see [[file:protocols.org][protocols.org]].
+
+ * Project-Specific Context
+
+@@ -48,9 +48,9 @@
+ - Include: What needs to be decided, options available, why it matters
+ - Remove decisions once resolved (the resolution is captured in the Session Log of the session where it was resolved)
+
+-**Example format:**
++- Example format:
+ #+begin_example
+-** Feature Name or Topic
++,** Feature Name or Topic
+
+ Craig needs to decide on [specific question].
+
diff --git a/working/sentry-living-document/feedback-from-archangel.org b/working/sentry-living-document/feedback-from-archangel.org
new file mode 100644
index 0000000..b59ce20
--- /dev/null
+++ b/working/sentry-living-document/feedback-from-archangel.org
@@ -0,0 +1,5 @@
+#+TITLE: Sentry dogfood feedback (first live run, archangel, night of
+#+SOURCE: from archangel
+#+DATE: 2026-07-21 07:22:17 -0500
+
+Sentry dogfood feedback (first live run, archangel, night of 2026-07-20→21): ran clean end to end — 7 fires (1 working + 6 quiet heartbeats), morning teardown + 8-item approval-queue walk all worked as designed. Two findings for the Living Document: (1) todo-cleanup --archive-done can write a TRACKED file from an 'org-only' pass — on first archive it creates archive/task-archive.org and appends a .gitignore entry, so the pass produced a real commit and correctly triggered the fire-end conditional suite; worth a note in the pass list that todo-cleanup's first archive touches .gitignore. (2) In a project that gitignores .ai/, the whole spine is the mirror-only case — quiet fires produce zero commits, so 'git log main..sentry/*' understates the night's activity; the anchor's heartbeat list is the only record. Both are notes, not defects.
diff --git a/working/sentry-living-document/feedback-from-takuzu.org b/working/sentry-living-document/feedback-from-takuzu.org
new file mode 100644
index 0000000..fe3ddba
--- /dev/null
+++ b/working/sentry-living-document/feedback-from-takuzu.org
@@ -0,0 +1,5 @@
+#+TITLE: Sentry dogfood feedback from its first live overnight run (t
+#+SOURCE: from takuzu
+#+DATE: 2026-07-21 06:43:55 -0500
+
+Sentry dogfood feedback from its first live overnight run (takuzu, 2026-07-20/21, 8 hourly fires): the engine worked as designed end to end — entry gates, per-fire lock acquire/release, branch isolation, digest-vs-heartbeat split, approval queue, morning squash-merge teardown. Findings for the Living Document: (1) a Craig-granted rotating-angle bug-hunt pass found 3 real fixes in the first 4 fires then went honestly quiet — worth considering as an official opt-in pass; (2) randomized property sweeps (generate-and-verify in both engines) made good quiet-fire work beyond the frozen parity corpus; (3) lint-org's todo-format checkers (level2-done-without-closed, level-2-dated-header) fire on spec files' Decisions sections under docs/specs/ — consider scoping those checkers away from spec files or blessing CLOSED dates in spec-create; (4) the per-fire full task-audit pass is too heavy hourly — a mechanical-subset audit with judgment items queued once per night fit better. No engine defects observed; skip semantics and the quiet-fire heartbeat behaved per spec.
diff --git a/working/sentry-living-document/proposed.diff b/working/sentry-living-document/proposed.diff
new file mode 100644
index 0000000..90c0db4
--- /dev/null
+++ b/working/sentry-living-document/proposed.diff
@@ -0,0 +1,32 @@
+--- claude-templates/.ai/workflows/sentry.org 2026-07-23 02:39:42.349838885 -0500
++++ /tmp/st.org 2026-07-23 02:48:24.104591775 -0500
+@@ -131,9 +131,9 @@
+
+ 3. *Triage intake — mail and messenger sources excluded.* Run =triage-intake.org=, loading only its non-mail, non-messenger source plugins (calendar, PR/ticketing). The mail and messenger plugins — cmail, any Gmail variant, Telegram, Signal, chat DMs — are never loaded by a sentry fire: Craig ruled 2026-07-21 that sentry doesn't check email or messengers. A manual "triage intake" still scans everything. Probe: the project has at least one *active* triage source that survives that exclusion — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources, or whose only declared sources are mail or messengers, probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended.
+
+-4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=.
++4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. Note that =--archive-done= is not purely an org-file pass on its first run in a project: it creates =archive/task-archive.org= and appends a =.gitignore= entry, so it produces a real tracked-file commit and correctly trips the fire-end conditional suite. (archangel, first live run 2026-07-21.)
+
+-5. *Task audit* — =task-audit.org=. Probe: a root =todo.org=. Priority regrades and consolidations queue for review; factual staleness fixes that are unambiguous execute.
++5. *Task audit* — the *mechanical subset* of =task-audit.org= hourly (staleness counts, structural checks, cookie recomputation); the judgment half (priority regrades, consolidations, merge candidates) runs *once per night* and queues its findings rather than repeating them every fire. Probe: a root =todo.org=. A full audit every hour is too heavy and re-surfaces the same judgment calls all night. (takuzu, first live run 2026-07-21.) Factual staleness fixes that are unambiguous still execute.
+
+ 6. *Working-files hygiene* — flag =working/<slug>/= directories whose backing task is closed (a filing candidate per =working-files.md=). Probe: a =working/= directory exists. The filing itself queues (it's a judgment move).
+
+@@ -145,7 +145,7 @@
+
+ 10. *Prep + symlink freshness* — stale daily-prep docs, broken symlinks. Probe: the prep dir / symlinks exist (work and home only, in practice).
+
+-11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.)
++11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. Randomized property sweeps (generate-and-verify against an engine's own invariants) are good quiet-fire work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few fires find the standing defects; a quiet bug hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four fires, then quiet.) This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.)
+
+ (KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.)
+
+@@ -178,6 +178,8 @@
+
+ A bad night is discarded by deleting one branch — nothing reached main, nothing was pushed.
+
++In a project that gitignores =.ai/=, the whole spine is untracked, so quiet fires produce no commits at all and =git log main..sentry/<date>-<host>= understates the night's activity. There the anchor's heartbeat list is the only record of what fired. Read the anchor, not just the log. (archangel, first live run 2026-07-21.)
++
+ * Stop Sentry
+
+ Trigger: "stop sentry" (and synonyms above). Sentry owns its own shutdown:
diff --git a/working/sentry-living-document/sentry.org.proposed b/working/sentry-living-document/sentry.org.proposed
new file mode 100644
index 0000000..62eac12
--- /dev/null
+++ b/working/sentry-living-document/sentry.org.proposed
@@ -0,0 +1,219 @@
+#+TITLE: Sentry — Overnight Hygiene Supervisor
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-19
+
+* Overview
+
+Sentry is an interval loop that keeps a project's hygiene current while Craig is away. Each fire walks a fixed list of hygiene passes — roam pull, inbox zero, triage (no mail or messengers), todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness, bug finding — and commits each pass's writing to a throwaway daily branch. Nothing pushes. In the morning Craig reviews the branch, squash-merges what he wants, and deletes it.
+
+The design goal is a project that greets the morning already tidy, with every judgment call and every destructive action parked in an approval queue rather than executed unattended. Sentry does the mechanical sweeping; Craig does the deciding.
+
+This file is the engine. It owns the entry gates, the branch mechanics, the lock model, the per-fire pass runner, the digest and approval queue, the skip semantics, and the stop-sentry shutdown. The =agent-lock= helper (=.ai/scripts/agent-lock=) provides the locks. The passes reuse existing workflows (=inbox.org=, =triage-intake.org=, =clean-todo.org=, =task-audit.org=) under sentry's unattended contract.
+
+* When to Use This Workflow
+
+Craig arms sentry at the end of a session, with the machine left running, to have overnight hygiene done by morning.
+
+Triggers:
+
+- "start sentry", "run sentry", "arm sentry", "sentry mode"
+- "let sentry watch this overnight", "keep this tidy overnight"
+- "start sentry hourly", "start sentry every <interval>" (sets the loop interval)
+
+Stop trigger (see Stop Sentry below):
+
+- "stop sentry", "stand down sentry", "sentry off"
+
+Sentry is deliberately *not* auto-armed. Running it in a project is a per-project grant (the =:COMMIT_AUTONOMY:= marker) plus a deliberate launch with Craig at the terminal for the entry gates.
+
+* Prerequisite — the autonomy ticket
+
+Sentry commits unattended. =commits.md= gates commits on Craig's approval, so sentry needs standing, per-project authorization to run at all. Before anything else, read the project's =.ai/notes.org= Workflow State block for:
+
+: :COMMIT_AUTONOMY: yes
+
+If the marker is absent or not =yes=, decline to start and name the marker:
+
+: Sentry needs ":COMMIT_AUTONOMY: yes" in .ai/notes.org Workflow State to run — it commits unattended. Add it to grant, or run the hygiene passes by hand.
+
+No half-running mode: a project without the grant doesn't run sentry's read-only passes either. The grant is one line away, so this is a deliberate opt-in, not a barrier.
+
+* Entry — interactive, with Craig present
+
+Craig types the sentry trigger, so the first moves run with him at the terminal. Do them in order; each gate that fails stops entry until Craig answers.
+
+1. *Autonomy ticket* — the prerequisite above. Absent → decline and stop.
+
+2. *Dirty-tree gate.* =git diff --quiet HEAD= (tracked modifications only; untracked and gitignored files never block — an inbox drop or scratch file is not in-progress work). If the tracked tree is dirty, describe what's dirty and offer, inline-numbered per =interaction.md=:
+
+ 1. Finish the job — commit the in-progress work first (recommended if it's a coherent unit)
+ 2. Stash it — =git stash= and start sentry on a clean tree
+ 3. Roll back named changes — discard specific files (names them)
+
+ Wait for an answer. Sentry can't start unattended from a dirty state; that's the point.
+
+3. *Green-suite gate.* Run the project's full suite (=make test=, or the project's equivalent — detect it). Read the output. If anything is red, describe the failures and offer to investigate before arming. The loop starts only on a green baseline, because every unattended fire measures itself against "did I break this?" and a pre-existing red poisons that check.
+
+4. *Prior sentry branch.* =git branch --list 'sentry/*'=. An unmerged =sentry/*= branch from a previous night means the morning review didn't happen. Surface it and offer to squash-merge or delete it now (Craig is present); don't stack a second sentry branch on the first.
+
+5. *Reconcile the project branch.* Fetch and fast-forward-only against upstream — the same reconcile =startup= runs:
+
+ : git fetch --all --prune
+ : git rev-list --left-right --count @{u}...HEAD
+
+ Zero-behind → continue. Behind-only and clean → =git merge --ff-only @{u}=. Diverged → surface to Craig (he's present); don't auto-resolve.
+
+6. *Create the daily branch.* From HEAD:
+
+ : git switch -c "sentry/$(date +%F)-$(uname -n)"
+
+ The host suffix (=uname -n=) stops a same-date collision between the two daily drivers. The working tree now sits on this branch overnight — the launch hands the repo to sentry until the morning merge. Reclaiming it mid-night means stopping sentry first (see Stop Sentry). Note the Emacs buffer-revert caveat to Craig if he has the repo open: files change on disk under him overnight, so buffers want reverting after the morning merge (see =emacs.md=).
+
+7. *Arm the loop.* Start =/loop= at the interval (default hourly; Craig's "every <interval>" phrase overrides) with the per-fire body being one sentry fire (the Pass Runner below). Confirm the arming in one line: interval, branch name, project.
+
+* The lock model
+
+Two locks, both served by =.ai/scripts/agent-lock= (names only; the helper owns the paths, which live on tmpfs under =$XDG_RUNTIME_DIR/agent-locks/=, host-local and cleared on reboot).
+
+*Single-runner lock* (=sentry-<project>=, where =<project>= is the repo-root basename: =basename "$(git rev-parse --show-toplevel)"= — the same derivation =wrap-it-up.org='s guard uses, so the two agree on the lock name). Each fire acquires it at fire start and releases it at fire end, and refreshes it between passes (the heartbeat, so a live fire's lock never ages past one pass). If =/loop= fires again while a previous fire still holds it, the new fire's acquire fails and the fire skips with one digest line — no two fires run at once. The bounded wait is short (a few seconds); a live fire means defer, not queue.
+
+*Roam-write lock* (=roam-write=). A pass that edits a file under =~/org/roam= acquires it, runs =capture-guard --wait= (the human-capture layer stays underneath), edits the working tree, triggers =systemctl --user start roam-sync.service=, and releases. The lock spans only edit-plus-trigger. Sentry never runs =git= against =~/org/roam= — roam-sync stays the repo's only committer (the 2026-06-24 one-git-owner rule). Pass 1's =pull --ff-only= is the sole, read-only exception.
+
+Every reclaim of a stale lock surfaces in the digest — the helper prints the reclaim note, and the fire records it. A reclaim during a genuinely slow pass is possible, so it's never silent.
+
+* The Pass Runner — one contract per pass
+
+Each fire, after acquiring the single-runner lock and verifying branch state (below), walks the pass list in order. Every pass follows the same four-step contract:
+
+1. *Probe* — a cheap existence check for the pass's target (named per pass below). Absent → the pass is one skip line in the digest and nothing more. This is what makes the pass list portable: passes self-activate where their target exists and stay silent elsewhere, with zero per-project configuration.
+
+2. *Work* — run the pass under the unattended contract. Quick, solo, already-agreed mechanical actions execute. Anything destructive or requiring judgment does *not* execute — it appends to the morning-approval queue (what, why, the exact command or edit that fires on approval). A pass runs fully or not at all; there is no reduced-form pass.
+
+3. *Session-context entry* — a pass that does or queues work appends its digest line to the =session-context.org= Session Log (path resolved via =.ai/scripts/session-context-path=) before its commit, so a crash between them still leaves the trail. Per-pass lines for an all-quiet fire (every pass probe-skipped or no-op) are not written one by one — the fire collapses to a single heartbeat at fire-end (below), so an idle fire doesn't spray one skip line per pass.
+
+4. *Commit* — if the pass wrote to disk, commit it: =chore(sentry): <pass> — <what changed>=. One commit per writing pass. A probe-skip or a no-op pass writes nothing and commits nothing.
+
+Between passes, refresh the single-runner lock (=agent-lock refresh sentry-<project>=) — the heartbeat.
+
+** Branch-state verification (fire start, before the passes)
+
+After acquiring the lock, confirm the fire is safe to run:
+
+- *On the right branch* — HEAD is =sentry/<today>-<host>=. If the loop was armed on a prior day and crossed midnight, the branch keeps the arming date; that's fine, morning teardown handles it. If HEAD is somehow *not* a sentry branch (an interrupted stop, a manual checkout), skip the whole fire with a digest line rather than committing onto main.
+- *Clean of foreign changes* — =git diff --quiet HEAD= excluding the spine set (=session-context.org= / =session-context.d/=, resolved via =session-context-path=). Sentry's own spine writes must not trip this; a genuinely unexpected dirty tree (something outside the spine changed and wasn't committed by a prior pass) poisons the fire — skip it with a digest line, the next fire retries.
+
+* Unattended safety — skip, never degrade
+
+With no one at the terminal, any unsafe state makes the affected scope skip with one digest line, and the next fire retries. Unsafe states and their scope:
+
+- *Unexpected dirty tree* (non-spine) → skip the whole fire.
+- *Lost or un-acquirable single-runner lock* → skip the fire (another fire holds it, or the helper is missing).
+- *A pass's own precondition unmet* (its probe fails, or a dependency is dirty) → skip that pass only.
+- *Red suite at fire-end* (see below) → the commits stay on the branch, flagged in the digest for morning review; the fire doesn't roll back.
+
+Skips are never silent and never partial. Inside a *working* fire, a pass line means the pass fully ran and a skip line names why it didn't. An *all-quiet* fire is not a silent skip either: its single =sentry at HH:MM: nothing= heartbeat is the explicit record that every pass found nothing, standing in for a wall of identical skip lines. The anti-silence rule targets a pass that hides work it should have surfaced; a quiet fire has surfaced that there was none.
+
+** Multi-day stall notification
+
+An unmerged prior =sentry/*= branch at fire start (the morning review never happened) skips the fire. After the *second consecutive* fire skipped for this reason, send one persistent desktop notification naming the project and branch:
+
+: sentry stalled: <branch> unmerged — merge or delete to resume
+
+Then repeat at most daily. Persistent notify matches the paging convention — it stays on screen until dismissed. A multi-day stall never stays silent.
+
+* The pass list (v1)
+
+In order. Each names its detection probe. A pass whose probe fails is one skip line.
+
+1. *Roam pull* — =git -C ~/org/roam pull --ff-only=. Probe: =~/org/roam= is a git clone. Skipped when the roam tree is dirty (roam-sync owns that case) or the clone is absent. Read-only and ff-only — the one narrow exception to "don't touch roam git," so later passes read a fresh tree.
+
+2. *Inbox zero* — run =inbox.org= roam mode under the no-approvals contract: quick+solo+agreed items execute, shared-asset and convention proposals park (prepared diff, =VERIFY= task, sender reply) in the approval queue. Edits to =~/org/roam/inbox.org= take the roam-write lock + =capture-guard=. Probe: the roam clone or a project =inbox/= exists. Tidying the shared roam inbox is allowed from *any* project session, work included — it's housekeeping on a shared resource, not a durable KB-node write, so the work-denylist doesn't gate it (=knowledge-base.md=). Never park it as a cross-project boundary crossing.
+
+3. *Triage intake — mail and messenger sources excluded.* Run =triage-intake.org=, loading only its non-mail, non-messenger source plugins (calendar, PR/ticketing). The mail and messenger plugins — cmail, any Gmail variant, Telegram, Signal, chat DMs — are never loaded by a sentry fire: Craig ruled 2026-07-21 that sentry doesn't check email or messengers. A manual "triage intake" still scans everything. Probe: the project has at least one *active* triage source that survives that exclusion — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources, or whose only declared sources are mail or messengers, probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended.
+
+4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. Note that =--archive-done= is not purely an org-file pass on its first run in a project: it creates =archive/task-archive.org= and appends a =.gitignore= entry, so it produces a real tracked-file commit and correctly trips the fire-end conditional suite. (archangel, first live run 2026-07-21.)
+
+5. *Task audit* — the *mechanical subset* of =task-audit.org= hourly (staleness counts, structural checks, cookie recomputation); the judgment half (priority regrades, consolidations, merge candidates) runs *once per night* and queues its findings rather than repeating them every fire. Probe: a root =todo.org=. A full audit every hour is too heavy and re-surfaces the same judgment calls all night. (takuzu, first live run 2026-07-21.) Factual staleness fixes that are unambiguous still execute.
+
+6. *Working-files hygiene* — flag =working/<slug>/= directories whose backing task is closed (a filing candidate per =working-files.md=). Probe: a =working/= directory exists. The filing itself queues (it's a judgment move).
+
+7. *Spec status board* — the =docs-lifecycle= grep for spec keywords, surfacing any =DOING= spec whose bound build parent is closed. Probe: =docs/specs/= exists.
+
+8. *Link integrity* — broken =file:= links in the project's org files, via =lint-org.el=. Probe: =lint-org.el= present. Report-only into the digest; no unattended rewrites.
+
+9. *Git health* — uncommitted drift, unpushed commits on other branches, stale branches, main-behind-origin. Probe: =.git=. Report into the digest.
+
+10. *Prep + symlink freshness* — stale daily-prep docs, broken symlinks. Probe: the prep dir / symlinks exist (work and home only, in practice).
+
+11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. Randomized property sweeps (generate-and-verify against an engine's own invariants) are good quiet-fire work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few fires find the standing defects; a quiet bug hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four fires, then quiet.) This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.)
+
+(KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.)
+
+* Fire-end — conditional suite, then the digest commit
+
+After the passes:
+
+1. *Conditional suite run.* If any pass this fire modified files *outside* the org/spine set (a code-touching pass, rare but possible via fixtures), run the full suite once. A green run confirms the fire's commits are safe; a red run flags the digest for morning review — the commits stay on the branch (nothing is pushed, so the morning gate catches it). No per-pass suite runs: the entry run is the green baseline, and hourly per-commit runs would turn a seconds-long fire into minutes all night. Fires that only touched org/spine files skip this.
+
+2. *Heartbeat or digest, then commit.* Decide quiet vs working. A *quiet* fire — every pass probe-skipped or no-op, nothing added to the approval queue — writes a single heartbeat line to the Session Log, =sentry at HH:MM: nothing= (HH:MM local, from =date=), and no per-pass digest block. A *working* fire — any pass ran, wrote, or queued — writes its full per-pass digest block. Then commit any accumulated spine writes in one sweep: =chore(sentry): digest — <date> <time> fire= for a working fire, =chore(sentry): heartbeat — <date> <time>= for a quiet one, so even a quiet fire leaves a clean tree for the next branch-state check (where the spine is untracked, the mirror-only case, there is nothing to commit and the heartbeat line stays in the working-tree anchor). This is the silent-until-signal policy (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=): an all-quiet night collapses from a wall of no-op digests to a list of one-line heartbeats, while a fire that actually did or queued something still writes the full record.
+
+3. *Release the single-runner lock.*
+
+* The digest and the approval queue
+
+*Digest.* A *working* fire appends its block to the =session-context.org= Session Log (the spine the fire already writes), so it survives a crash, rides the session archive, and is on screen in the running session. One block per working fire: the timestamp, then one line per pass (ran + what, or skipped + why), plus any lock reclaim notes. A *quiet* fire (nothing done or queued) writes no block — just the one heartbeat line =sentry at HH:MM: nothing= (the silent-until-signal policy). The per-pass block is a working-fire artifact; it still carries one line per pass so a real skip inside a working fire is never hidden.
+
+*Approval queue.* Destructive and judgment actions accumulate under one heading in the same file — =* Sentry approval queue (<date>)= — newest last. Each item carries three things: *what* (the action), *why* (what triggered it), and the *exact command or edit* that fires on approval. The morning review is Craig reading this heading top to bottom and running or discarding each item.
+
+* Morning teardown — Craig's, documented not automated
+
+Sentry never merges its own branch. In the morning Craig:
+
+1. Reviews the digest and the approval queue in =session-context.org=.
+2. Runs or discards each approval-queue item.
+3. Reviews the branch: =git log main..sentry/<date>-<host>= and the diff.
+4. Squash-merges what he wants (=git switch main && git merge --squash sentry/<date>-<host>=, then one clean commit) or cherry-picks selectively.
+5. Deletes the branch: =git branch -D sentry/<date>-<host>=.
+6. Reverts any Emacs buffers still showing the pre-merge on-disk state (=emacs.md= buffer-revert caveat).
+
+A bad night is discarded by deleting one branch — nothing reached main, nothing was pushed.
+
+In a project that gitignores =.ai/=, the whole spine is untracked, so quiet fires produce no commits at all and =git log main..sentry/<date>-<host>= understates the night's activity. There the anchor's heartbeat list is the only record of what fired. Read the anchor, not just the log. (archangel, first live run 2026-07-21.)
+
+* Stop Sentry
+
+Trigger: "stop sentry" (and synonyms above). Sentry owns its own shutdown:
+
+1. *Cancel the loop* — stop the =/loop= (=ScheduleWakeup= stop / the loop's stop path). No further fires.
+2. *Release the single-runner lock* if this context holds it.
+3. *Branch disposition* — offer, inline-numbered:
+ 1. Squash-merge the day's branch into main now (walk the morning teardown steps 3-5 interactively)
+ 2. Leave it named for later review (=sentry/<date>-<host>= stays; review at leisure)
+4. *Approval queue* — offer to walk the queued items now, or carry them (they stay under the heading for whenever Craig reviews).
+
+Stopping sentry is the only way to reclaim the working tree mid-night. The entry gate fronts the handoff; stop-sentry ends it.
+
+* Wrap-up interaction
+
+=wrap-it-up.org= refuses while sentry is live: it detects the single-runner lock (=agent-lock status sentry-<project>= → held) and stops with "sentry is active — say 'stop sentry' first." The shutdown logic lives here, not in wrap-up; wrap-up carries only the one guard.
+
+* Common Mistakes
+
+1. *Running without the =:COMMIT_AUTONOMY:= grant* — sentry commits unattended; the marker is the entry ticket, and its absence is a hard stop, not a degrade.
+2. *Starting from a dirty or red tree* — the entry gates exist because an unattended fire can't tell Craig's in-progress work from a regression. Answer the gate; don't bypass it.
+3. *Committing onto main* — every writing pass commits to the daily =sentry/*= branch. A fire that finds HEAD off the sentry branch skips rather than commits.
+4. *Running a =git= write against =~/org/roam=* — roam-sync is the only committer. Sentry edits the tree under the roam-write lock and triggers the sync; it never commits or pushes roam.
+5. *A per-pass suite run* — the suite runs at entry (baseline) and conditionally at fire-end (only when a pass touched non-org files). Hourly per-commit runs all night is the anti-pattern the suite policy exists to prevent.
+6. *Executing a judgment or destructive action unattended* — those queue for the morning with their exact command. The pass did its detection; Craig makes the call.
+7. *A silent skip* — inside a working fire, every skip writes a digest line naming why; a missing pass with no line reads as "ran clean" when it didn't. The one exception is not a violation: an all-quiet fire collapses to a single =sentry at HH:MM: nothing= heartbeat instead of one skip line per pass — the heartbeat is the explicit "nothing to do" record, per the silent-until-signal policy.
+8. *Degrading a pass to a reduced form* — a pass runs fully or skips. No half-passes.
+9. *Letting an unmerged branch stall silently* — after two consecutive unmerged-branch skips, the persistent desktop notify fires. Don't suppress it.
+10. *Merging sentry's branch automatically* — the morning teardown is Craig's. Sentry creates and commits; it never merges or deletes its own branch.
+
+* Living Document
+
+Sentry ships with eleven mechanical passes and a deferred KB pass. The pass list, the interval default, and the queue-vs-execute line for each pass are the knobs most likely to move with dogfooding. Fold in what the live trial surfaces — a pass that queues too eagerly, a probe that misfires, a digest line that wants more detail. Refine as the signal arrives.
+
+* History
+
+Built 2026-07-19 from the sentry spec (=docs/specs/2026-07-14-sentry-workflow-spec.org=, ID f6c51f27-d7a2-4b63-9ff9-5ba005a66dfb) — 10 decisions and 12 review findings resolved before the build. Phase 1 shipped the =agent-lock= helper (commit =a8b6cf4=); this file is Phase 2, the engine. Phase 3 reconciles the roam writers (=inbox.org=, =knowledge-base.md=) to acquire the roam-write lock and adds the =wrap-it-up.org= guard.
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)