aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/.ai/workflows
diff options
context:
space:
mode:
Diffstat (limited to 'claude-templates/.ai/workflows')
-rw-r--r--claude-templates/.ai/workflows/INDEX.org44
-rw-r--r--claude-templates/.ai/workflows/add-calendar-event.org2
-rw-r--r--claude-templates/.ai/workflows/broadcast.org6
-rw-r--r--claude-templates/.ai/workflows/clean-todo.org21
-rw-r--r--claude-templates/.ai/workflows/code-quality.org90
-rw-r--r--claude-templates/.ai/workflows/create-workflow.org8
-rw-r--r--claude-templates/.ai/workflows/cross-agent-comms.org334
-rw-r--r--claude-templates/.ai/workflows/daily-prep.org7
-rw-r--r--claude-templates/.ai/workflows/delete-calendar-event.org2
-rw-r--r--claude-templates/.ai/workflows/edit-calendar-event.org2
-rw-r--r--claude-templates/.ai/workflows/email-assembly.org2
-rw-r--r--claude-templates/.ai/workflows/extract-email.org2
-rw-r--r--claude-templates/.ai/workflows/find-email.org2
-rw-r--r--claude-templates/.ai/workflows/first-session.org2
-rw-r--r--claude-templates/.ai/workflows/flashcard-review.org2
-rw-r--r--claude-templates/.ai/workflows/helper-mode.org4
-rw-r--r--claude-templates/.ai/workflows/inbox-zero.org97
-rw-r--r--claude-templates/.ai/workflows/inbox.org524
-rw-r--r--claude-templates/.ai/workflows/journal-entry.org2
-rw-r--r--claude-templates/.ai/workflows/meeting-prep.org2
-rw-r--r--claude-templates/.ai/workflows/meeting-prep.pre-wire.org2
-rw-r--r--claude-templates/.ai/workflows/monitor-inbox.org122
-rw-r--r--claude-templates/.ai/workflows/no-approvals.org12
-rw-r--r--claude-templates/.ai/workflows/open-tasks.org34
-rw-r--r--claude-templates/.ai/workflows/page-me.org53
-rw-r--r--claude-templates/.ai/workflows/process-inbox.org220
-rw-r--r--claude-templates/.ai/workflows/process-meeting-transcript.org26
-rw-r--r--claude-templates/.ai/workflows/read-calendar-events.org2
-rw-r--r--claude-templates/.ai/workflows/readability-audit.org242
-rw-r--r--claude-templates/.ai/workflows/rename-artifact.org2
-rw-r--r--claude-templates/.ai/workflows/send-email.org2
-rw-r--r--claude-templates/.ai/workflows/sentry.org227
-rw-r--r--claude-templates/.ai/workflows/session-harvest.org2
-rw-r--r--claude-templates/.ai/workflows/spec-create.org22
-rw-r--r--claude-templates/.ai/workflows/spec-response.org67
-rw-r--r--claude-templates/.ai/workflows/spec-review.org113
-rw-r--r--claude-templates/.ai/workflows/startup.org115
-rw-r--r--claude-templates/.ai/workflows/status-check.org2
-rw-r--r--claude-templates/.ai/workflows/summarize-emails.org2
-rw-r--r--claude-templates/.ai/workflows/suspend.org143
-rw-r--r--claude-templates/.ai/workflows/sync-email.org2
-rw-r--r--claude-templates/.ai/workflows/task-audit.org40
-rw-r--r--claude-templates/.ai/workflows/task-review.org12
-rw-r--r--claude-templates/.ai/workflows/triage-intake.cmail.org2
-rw-r--r--claude-templates/.ai/workflows/triage-intake.github-prs.org2
-rw-r--r--claude-templates/.ai/workflows/triage-intake.org237
-rw-r--r--claude-templates/.ai/workflows/triage-intake.personal-calendar.org2
-rw-r--r--claude-templates/.ai/workflows/triage-intake.personal-gmail.org23
-rw-r--r--claude-templates/.ai/workflows/triage-intake.telegram.org143
-rw-r--r--claude-templates/.ai/workflows/work-the-backlog.org266
-rw-r--r--claude-templates/.ai/workflows/wrap-it-up.org215
51 files changed, 2406 insertions, 1103 deletions
diff --git a/claude-templates/.ai/workflows/INDEX.org b/claude-templates/.ai/workflows/INDEX.org
index 42119b4..f18d953 100644
--- a/claude-templates/.ai/workflows/INDEX.org
+++ b/claude-templates/.ai/workflows/INDEX.org
@@ -1,5 +1,5 @@
#+TITLE: Workflow Index
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-04-25
* Purpose
@@ -18,8 +18,12 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e
- =helper-mode.org= — role contract for a helper instance (a second Claude in the same project as a live primary). No manual trigger; the spawn paths route to it, "you are a helper" is the manual fallback.
- =first-session.org= — initialize =.ai/= for a brand-new project.
- Triggers: "this is a new project", "let's set this project up". Auto-runs if =.ai/sessions/= is empty.
-- =wrap-it-up.org= — end-of-session: write summary, archive, commit, push.
+- =wrap-it-up.org= — end-of-session: write summary, archive, commit, push, then a phrase-dependent Step 6 teardown. Bare "wrap it up" tears the session down (kills the ai-term buffer + =aiv-<project>= tmux session via a =Stop=-hook sentinel, after the valediction flushes); a "with summary" / "and summarize" wrap keeps the buffer; "and shutdown" gates on being the only live ai-term session, then powers the machine off via an abort-able Emacs countdown.
- Triggers: "wrap it up", "that's a wrap", "let's call it a wrap"
+ - No-teardown triggers: "wrap it up with summary", "wrap it up and summarize"
+ - Shutdown trigger: "wrap it up and shutdown"
+- =suspend.org= — capture-only mid-session pause for an abrupt departure: append a resume-weighted =SUSPENDED= entry to the Session Log, note uncommitted work, and LEAVE =.ai/session-context.org= in place so the next startup resumes from it. The capture-only counterpart to =wrap-it-up= (which archives + tears down) and to =flush= (=/flush=, which prompts =/clear= and resumes the same session). Provides only the capture half; startup's interrupted-session path is the resume half.
+ - Triggers: "suspend the session", "suspend", "I need to go", "stick a pin in everything"
- =retrospective.org= — post-mortem after a tough session.
- Triggers: "let's do a retrospective", "retrospective time"
@@ -44,12 +48,19 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e
- Triggers: "let's do a journal entry", "create a journal entry"
- =clean-todo.org= — tidy =todo.org=: hygiene pass + =--archive-done=, then summarize. Wrap-up does this automatically; this is the manual entry point.
- Triggers: "clean up todo.org", "clean-todo", "tidy the todo file", "archive the done items in todo.org", "run the todo cleanup"
-- =process-inbox.org= — evaluate each inbox item against a three-question value gate (advances an existing TODO / improves the project / serves the mission), then implement, fold, file, defer, or reject per source (Craig / project handoff / script). Auto-invoked by startup when inbox is non-empty. Source-aware rejection flow: handoff rejections write a response back via =inbox-send= naming the failed gate question and any reconsideration condition.
- - Triggers: "process inbox", "process the inbox", "handle the inbox", "what's in inbox", "what's in the inbox", "let's clear the inbox", "let's process the inbox items"
-- =monitor-inbox.org= — the cadence + act-vs-file + reply layer over process-inbox: "monitor the inbox" runs a pass now then loops process-inbox every 15 min; gates on a clean tree + green suite at both ends; in no-approvals mode auto-executes only agreed + quick + solo items (else files or parks); also the ambient =inbox-status= task-boundary check and the reply-to-sender discipline.
- - Triggers: "monitor the inbox", "watch the inbox", "respond to the handoffs", "handle the handoffs"
-- =inbox-zero.org= — route the *global roam inbox* (=~/org/roam/inbox.org=) to owning projects by =<project>:= heading prefix. Distinct from =process-inbox.org= (the project's own =inbox/= dir). The current session claims only its own prefixed items, files them into =todo.org=, removes them from the shared inbox, and leaves foreign/unowned items. Every scan reports the total item count plus how many appear related to this project. v1 is single-destination (prefix-claim only); domain-aware whole-inbox routing is deferred. Called read-only from startup (count + offer) and as a wrap-up Step 3 sub-step.
- - Triggers: "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox"
+- =inbox.org= — one engine for the project's inbox surfaces, with the shared value gate / skeptical review / disposition ladder / reply discipline / capture-guard / priority-scheme check in one place, plus thin per-surface modes. *Process mode* evaluates each project-local =inbox/= item against the three-question value gate, then implements / folds / files / defers / rejects per source (auto-invoked by startup when inbox is non-empty). *Monitor mode* runs process mode now then loops it every 15 min, gating on a clean tree + green suite and adding the act-vs-file + no-approvals-execute + reply discipline. *Roam mode* routes the global roam inbox (=~/org/roam/inbox.org=) to owning projects by =<project>:= prefix (read-only nudge at startup, sweep at wrap-up). *Auto inbox zero* runs roam mode on an interactive =/loop= at a Craig-chosen interval. Distinct from =triage-intake.org= (external accounts), which stays separate.
+ - Process-mode triggers: "process inbox", "process the inbox", "handle the inbox", "what's in inbox", "what's in the inbox", "let's clear the inbox", "let's process the inbox items"
+ - Monitor-mode triggers: "monitor the inbox", "watch the inbox", "respond to the handoffs", "handle the handoffs"
+ - Roam-mode triggers: "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox"
+ - Auto-mode trigger: "auto inbox zero" (match before "inbox zero")
+
+- =work-the-backlog.org= — the autonomous task-execution loop, the single home for working a batch of marked tasks unattended: takes an ordered task set (explicit list or tag query) + session mode (=file-only= default / =autonomous-commit= + paging) + a hard run cap; each candidate passes the mechanical eligibility gate (status =TODO= + =:solo:= per the project's scheme header) and the four-item defer checklist, then is implemented to the full quality bar (TDD, =/review-code=, =/voice=) as its own logical commits. Fed by the inbox auto-loop's chain step (yes-gated, file-only, cap 1) and the no-approvals speedrun preset (pre-flight Q&A → autonomous-commit + always-push + end-of-set page over an explicit ordered list).
+ - Speedrun triggers: "speedrun", "no approvals speedrun", "speedrun these: <task set>" — any phrase containing "speedrun" routes here (the preset), never to =no-approvals.org=
+ - Manual triggers: "work the backlog", "work the backlog with <task set>" (file-only defaults)
+ - Synthesis trigger: "synthesize backlog metrics" — read the per-project metrics logs, compute trends + the corrections signal, write one =:agent:metrics:= KB node (personal projects only)
+- =sentry.org= — the overnight hygiene supervisor: an interval loop (default hourly) that walks a fixed pass list (roam pull, inbox zero, triage, todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness), commits each writing pass to a throwaway =sentry/<date>-<host>= branch (never pushed), and parks every judgment call and destructive action in a morning-approval queue. Gated on =:COMMIT_AUTONOMY: yes= plus interactive entry gates (clean tree, green suite) with Craig present. Locks via =agent-lock=; morning teardown (review, squash-merge, delete) is Craig's, never automated.
+ - Triggers: "start sentry", "run sentry", "arm sentry", "sentry mode", "start sentry every <interval>"
+ - Stop trigger: "stop sentry", "stand down sentry", "sentry off"
** Calendar
@@ -81,11 +92,18 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e
- =spec-create.org= — author a design/feature spec before non-trivial work (more than ~6 hours, or real trade-offs): a when-to-spec gate, then problem-first framing, design + alternatives + inline mini-ADR decisions, implementation phases + acceptance criteria + a readiness-dimensions menu, a terseness pass, and a hand-off self-check against the review rubric. The *author* side that starts the trio; its output feeds =spec-review.org=.
- Triggers: "let's write a spec", "spec this out", "create a spec for X", "spec-create workflow"
-- =spec-review.org= — review a design/feature spec for implementation-readiness: run the readiness gate, read the code first, evaluate across dimensions, assign a rubric (Ready / Ready-with-caveats / Not-ready / Needs-research), and write a =<spec>-review.org= file when not ready. The *reviewer* side; its output feeds =spec-response.org=.
+- =spec-review.org= — review a design/feature spec for implementation-readiness: run the readiness gate, read the code first, evaluate across dimensions, assign a rubric (Ready / Ready-with-caveats / Not-ready / Needs-research), and record findings as =TODO= tasks in the spec's =* Review findings= section when not ready. The *reviewer* side; its output feeds =spec-response.org=.
- Triggers: "review the spec", "is this spec implementation-ready?", "spec-review workflow", "review the design"
-- =spec-response.org= — fold an external spec review back in: decide accept / modify / reject for every recommendation, weave accepts into the spec body, document modifies and rejects in a "Review dispositions" section, reconcile cross-spec tensions, iterate to implementation-ready. The *author* side; consumes the =<spec>-review.org= file =spec-review.org= produces.
+- =spec-response.org= — fold a spec review back in: decide accept / modify / reject for every finding, weave accepts into the spec body, complete each finding task in place (the reason recorded on modifies and rejects), reconcile cross-spec tensions, iterate to implementation-ready. The *author* side; consumes the =* Review findings= =spec-review.org= produces.
- Triggers: "respond to the review", "process the spec reviews", "spec-response workflow", "fold in the review"
+** Code quality
+
+- =code-quality.org= — one trigger that sequences every behavior-preserving quality pass over a scope of existing code: =/refactor= (complexity, duplication, dead-code, simplification) then =readability-audit= (comments, headers, names, organization), then surfaces the =:refactor:= tasks readability filed and any deferred =/refactor= findings. A thin orchestrator — each pass keeps its own gate. Excludes =/simplify= (that's for the current diff, not existing code).
+ - Triggers: "code quality sweep", "quality sweep", "run every quality pass on <scope>", "give me every pass on <scope>"
+- =readability-audit.org= — make code readable to a future maintainer: audit file-top commentary, inline comments (why-not-what), names (intention-revealing), and organization (co-location / stepdown / cohesion). The cheap comment- and name-only fixes (dimensions A/B/C) land inline, verified by a green suite; the structural findings (dimension D — split a module, rename a public symbol) are *filed* as =:refactor:= tasks, not done here. Language-agnostic. Feeds =/refactor= (which executes the filed structural work); distinct from =/refactor='s metric scans and =/simplify='s diff cleanup.
+ - Triggers: "let's run the readability-audit workflow", "audit the comments and commentary in <area>", "clean up the structure/organization of <module>", "readability audit"
+
** Tools and meta
- =process-meeting-transcript.org= — record → transcript → labeled archive.
@@ -95,8 +113,8 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e
- Situational triggers: "broadcast the <event> to all projects", "broadcast that <situation>", "let every project know I'll be away ..."
- =flashcard-review.org= — review an org-drill flashcard file, restructure cards to question-form headings (no answer hints), audit content accuracy against project source-of-truth via subagent, rewrite source preserving SRS state, regenerate the Anki =.apkg= to =~/sync/phone/anki/=. Person cards use "Who is X? Tell me about their Y."; talking-points cards stay as-is. Script behavior: =flashcard-to-anki.py= strips =:PROPERTIES:= drawers + =SCHEDULED:= / =DEADLINE:= planning lines from Anki output.
- Triggers: "review the flashcards", "update the flashcards", "review the drill deck", "update the drill deck", "refresh the Anki cards", "let's run the flashcard-review workflow"
-- =page-me.org= — set a timed notification.
- - Triggers: anything containing the word "page" used as a verb ("page me", "page me in 10 minutes", "page me at 3pm")
+- =page-me.org= — set a timed notification. "page me" desktop =notify=, "text me" phone via =agent-text=, "text and page me" both.
+ - Triggers: anything containing the word "page" used as a verb ("page me", "page me in 10 minutes", "page me at 3pm", "page my phone")
- =status-check.org= — proactive long-running-job updates.
- Triggers: "keep me posted on this", "provide status checks on this job", "let me know when it's done", "monitor this for me". Auto: any job estimated 10+ min.
- =create-workflow.org= — define a new workflow.
@@ -107,7 +125,7 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e
- Triggers: "session harvest", "harvest the sessions", "let's run the session-harvest workflow", "monthly harvest", "mine the sessions"
- =no-approvals.org= — drop the interaction-level approval gates for a pre-agreed batch while keeping engineering-discipline gates (=/review-code=, =/voice personal=, tests, session-log updates, subagent reviews, destructive-action consent). Mode stays on until Craig turns it off, a real question arises, the queue empties, or the conversation switches topics.
- Triggers: "no-approvals mode", "no approvals", "no-approval", "no need for approval gates", "stop asking, just keep going", "I'll check back in when you're done or stuck", "do all =<selector>= with no-approval"
-- =cross-agent-comms.org= — protocol for cross-project agent coordination via =inbox/from-agents/= (file-based IPC, GPG-signed, supports cross-machine over Tailscale). Auto: when =cross-agent-watch= detects a new inbound message, or when an agent decides to initiate a cross-project conversation. Operational scripts (=cross-agent-send=, =-recv=, =-watch=, =-status=, =-discover=, =-halt=, =-resume=) and their READMEs live at =.ai/scripts/cross-agent-comms/=.
+ - Exception: any phrase containing "speedrun" routes to =work-the-backlog.org='s no-approvals speedrun preset instead
* Living Document
diff --git a/claude-templates/.ai/workflows/add-calendar-event.org b/claude-templates/.ai/workflows/add-calendar-event.org
index 2650fb7..5dd6c42 100644
--- a/claude-templates/.ai/workflows/add-calendar-event.org
+++ b/claude-templates/.ai/workflows/add-calendar-event.org
@@ -1,5 +1,5 @@
#+TITLE: Add Calendar Event Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-01
* Overview
diff --git a/claude-templates/.ai/workflows/broadcast.org b/claude-templates/.ai/workflows/broadcast.org
index 1be07d2..cc14f00 100644
--- a/claude-templates/.ai/workflows/broadcast.org
+++ b/claude-templates/.ai/workflows/broadcast.org
@@ -1,5 +1,5 @@
#+TITLE: Broadcast Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-29
* Overview
@@ -159,11 +159,11 @@ broadcast, not a task and not tailored to this project.
- Ask Craig any follow-up questions then — this message is deliberately general.
#+end_example
-The "For the receiving agent" block is fixed text — it travels with every situational broadcast so the message is self-describing. A receiving project's =process-inbox= reads it and acts on those instructions without needing any special-casing; the value gate accepts it as situational awareness that improves how the project works.
+The "For the receiving agent" block is fixed text — it travels with every situational broadcast so the message is self-describing. A receiving project's =inbox.org= process mode reads it and acts on those instructions without needing any special-casing; the value gate accepts it as situational awareness that improves how the project works.
** Receiving behavior (what a project does with an incoming situational broadcast)
-When =process-inbox= encounters a =Broadcast:= item, the disposition is *record-and-hold*, not file-as-task:
+When =inbox.org= process mode encounters a =Broadcast:= item, the disposition is *record-and-hold*, not file-as-task:
1. Add a dated entry to =notes.org= Active Reminders capturing the situation and its end date (if any).
2. If the event bears on an open task, note the connection in that task's body.
diff --git a/claude-templates/.ai/workflows/clean-todo.org b/claude-templates/.ai/workflows/clean-todo.org
index dd33056..48d3084 100644
--- a/claude-templates/.ai/workflows/clean-todo.org
+++ b/claude-templates/.ai/workflows/clean-todo.org
@@ -1,5 +1,5 @@
#+TITLE: Clean-Todo Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-11
* Overview
@@ -27,7 +27,17 @@ Deletes bogus =- State "X" from "X" [date]= log lines (state didn't actually cha
To preview without writing, run =--check= first: =emacs --batch -q -l .ai/scripts/todo-cleanup.el --check todo.org=.
-** Step 2: Archive completed work
+** Step 2: Convert done sub-tasks to dated entries
+
+#+begin_src bash
+emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks todo.org
+#+end_src
+
+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 =CLOSED:= line. Enforces the depth rule that a completed sub-task becomes dated history — a shape interactive org closes and =--archive-done= (level-2 only) leave unapplied. Timestamp comes from each entry's =CLOSED= cookie; heading text kept verbatim; idempotent; a done sub-task with no parseable =CLOSED= is flagged and left alone. Run before archiving so a parent's sub-tasks are already dated when it moves. Capture the output.
+
+To preview without writing: =emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks --check todo.org=.
+
+** Step 3: Archive completed work
#+begin_src bash
emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done todo.org
@@ -37,10 +47,11 @@ Moves every level-2 subtree whose TODO state is DONE or CANCELLED out of the "Op
To preview the moves without writing: =emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org=.
-** Step 3: Summarize
+** Step 4: Summarize
-Report to Craig from the two captured outputs:
+Report to Craig from the three captured outputs:
- Hygiene: how many bogus state-log lines were deleted; any orphan-planning warnings (file:line + heading), or "none".
+- Convert: how many done sub-tasks were rewritten to dated entries (heading + line), any flagged for no =CLOSED= date, or "nothing to convert".
- Archive: how many subtrees moved and which (heading + line), or "nothing to move" / the skip reason if a section was missing or ambiguous.
- If the file changed, note that =todo.org= now has an uncommitted edit — review =git diff -- todo.org= and commit it (in this repo's commit style) if it looks right. If nothing changed, say so and stop.
@@ -49,7 +60,7 @@ Don't auto-commit. The summary is the review point; Craig decides whether the di
* Principles
- *Both passes apply, not just preview.* The workflow is invoked because cleanup is wanted. Use the =--check= variants only when Craig asks for a dry run.
-- *Two passes, two invocations.* =--archive-done= is its own mode and does not run the hygiene pass; run both.
+- *Separate modes, separate invocations.* =--convert-subtasks=, =--archive-done=, and the hygiene pass are each their own mode and don't run the others; run all three.
- *Never auto-commit todo.org.* Surface the diff and let Craig commit it. The cleanup is a working-tree change, fully reversible until committed.
- *Trust the script.* It's fast and idempotent; if there's nothing to do, it reports zero and exits clean. No pre-checks.
diff --git a/claude-templates/.ai/workflows/code-quality.org b/claude-templates/.ai/workflows/code-quality.org
new file mode 100644
index 0000000..3c4ed8f
--- /dev/null
+++ b/claude-templates/.ai/workflows/code-quality.org
@@ -0,0 +1,90 @@
+#+TITLE: Code-Quality Sweep Workflow
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-28
+
+* Overview
+
+One trigger that runs every behavior-preserving quality pass over a scope of
+*existing* code, in order, then surfaces what got filed for later. It's a thin
+orchestrator — each pass keeps its own discipline and its own confirm gate; this
+workflow only sequences them and collects the residue.
+
+*Behavior-preserving rests on a test net.* The passes below claim to preserve
+behavior, but a refactor on untested code is a guess, not a preservation. Where
+the scope has no tests, bring it under a characterization net first
+(Normal/Boundary/Error per unit, per the =testing-standards= skill's "Adding Tests to Existing
+Untested Code") — that net is what turns "behavior-preserving" from an assertion
+into something the green suite actually verifies across each pass.
+
+The passes it chains:
+
+1. =/refactor= — structural and logic cleanup on measurable metrics (complexity,
+ duplication, dead-code) plus the simplification lens.
+2. =readability-audit= ([[file:readability-audit.org][readability-audit.org]]) — prose and human-reader clarity
+ (comments, file headers, names, organization).
+
+It deliberately does *not* run =/simplify=: that works the current uncommitted
+diff, not existing committed code, so it belongs to the moment you've just made a
+change, not to a sweep of code already in the tree (see "The /simplify boundary"
+below).
+
+* When to Use This Workflow
+
+- "code quality sweep" / "quality sweep"
+- "run every quality pass on <scope>" / "full quality pass on <scope>"
+- "give me every pass on <file/module/tree>"
+
+Do NOT use it for:
+- *In-flight diff cleanup* — that's =/simplify= on the change you just made.
+- *Bug hunting* — these passes are behavior-preserving; for defects use =debug=
+ or =/review-code=.
+- *Performing the structural refactors it files* — those become =:refactor:=
+ tasks; work them later via =/refactor rename= / =/refactor simplification= or
+ =/start-work=.
+
+* Steps
+
+** 1. Scope
+
+Pick the target: one file, a named module set, or the whole tree (honor
+=.aiignore=). The same scope is passed to both passes so they cover the same
+code.
+
+** 2. /refactor <scope>
+
+Run =/refactor= on the scope. Its default full scan covers complexity,
+duplication, dead-code, and simplification. It presents findings and applies
+only what's approved (its own gate) — structure and logic first, so the
+readability pass audits the cleaned-up code.
+
+** 3. readability-audit on <scope>
+
+Run the readability-audit workflow on the same scope. Its cheap comment- and
+name-only fixes (dimensions A/B/C) land inline and are verified by a green
+suite; its structural findings (dimension D — split a module, rename a public
+symbol) are *filed* as =:refactor:= tasks rather than done here.
+
+** 4. Surface the residue
+
+Collect and report what the sweep left behind for later work:
+
+- The =:refactor:= tasks readability-audit filed (the structural backlog).
+- Any =/refactor= findings deferred rather than applied in step 2.
+
+That residue is the "do this next" list the sweep produces; it's not a failure
+to finish, it's the structural work that needs its own design and test pass.
+
+* The /simplify boundary
+
+=/simplify= and this sweep don't overlap: =/simplify= cleans the *current diff*
+and applies its fixes directly, so reach for it right after making a change,
+before committing. This sweep works *existing committed code* and runs the
+scan-and-present passes. One trigger can't sensibly do both — a diff you're
+holding and a tree you're auditing are different inputs.
+
+* Verification
+
+Each pass owns its verification (=/refactor= runs the suite after applying;
+readability-audit verifies inline fixes against a green suite). The umbrella
+adds nothing beyond sequencing, so when both passes report green, the sweep is
+clean — confirm that before reporting done rather than assuming it.
diff --git a/claude-templates/.ai/workflows/create-workflow.org b/claude-templates/.ai/workflows/create-workflow.org
index 6060df1..393fce5 100644
--- a/claude-templates/.ai/workflows/create-workflow.org
+++ b/claude-templates/.ai/workflows/create-workflow.org
@@ -195,7 +195,7 @@ After the Q&A, ask together:
Decide on a name for this workflow.
*Naming convention:* Action-oriented (verb form)
-- Examples: "refactor", "inbox-zero", "create-workflow", "review-code"
+- Examples: "refactor", "clean-todo", "create-workflow", "review-code"
- Why: Shorter, natural when saying "let's do a [name] workflow"
- Filename: =.ai/workflows/[name].org=
@@ -240,10 +240,10 @@ Update =notes.org=:
Example entry:
#+begin_src org
-,** inbox-zero
-File: =.ai/workflows/inbox-zero.org=
+,** journal-entry
+File: =.ai/workflows/journal-entry.org=
-Workflow for processing inbox to zero:
+Workflow for capturing a daily journal entry:
1. [Brief workflow summary]
2. [Key steps]
diff --git a/claude-templates/.ai/workflows/cross-agent-comms.org b/claude-templates/.ai/workflows/cross-agent-comms.org
deleted file mode 100644
index 430b4b0..0000000
--- a/claude-templates/.ai/workflows/cross-agent-comms.org
+++ /dev/null
@@ -1,334 +0,0 @@
-#+TITLE: Cross-Agent Communication Workflow (v5)
-#+AUTHOR: Craig Jennings & Claude (homelab + career sessions)
-#+DATE: 2026-04-27
-#+VERSION: 5
-
-* Status
-
-Draft. Iterating between the homelab and career sessions through a multi-round design discussion. Awaiting Craig's review for promotion to =~/code/rulesets/claude-templates/.ai/workflows/=.
-
-v5 changes from v4:
-- *Script absorption.* Seven operational scripts (=cross-agent-send=, =cross-agent-recv=, =cross-agent-watch=, =cross-agent-status=, =cross-agent-discover=, =cross-agent-halt=, =cross-agent-resume=) now own most implementation detail. Their READMEs are the operational source of truth. The spec stays declarative.
-- *Failsafe halt.* Layered HALT-file mechanism stops all cross-agent activity on a machine within ~5 min, without visiting individual sessions or restarting Claude Code. =cross-agent-halt= and =cross-agent-resume= are the convenience entry points; every other component checks the HALT file independently.
-- *Identity.* Messages are GPG-signed by sender and verified by receiver. Combined with POSIX permissions on =from-agents/= and Tailscale-level network auth, identity becomes a three-layer story.
-- *Atomic writes.* Writers MUST use temp-file + rename. =cross-agent-send= handles this; the spec just states the contract.
-- *Dedup.* Sequence-collision dedup is now binary SHA-256 equality, not a fuzzy ">90% match" threshold.
-- *Cold-start handling.* Layered: =cross-agent-watch= (push notifications via =inotifywait=) is the primary mechanism; startup-workflow check and user-direct-injection are coverage layers.
-- *Spec stays roughly the same length but does more protocol work.* Operational detail (rsync retry numbers, inotifywait recipes, peers.toml schema, GPG flags, dedup mechanics) moved to the script READMEs. The spec adds new protocol elements (identity layer, atomic-writes contract, SHA-256 dedup, =escalate= type, =RELEASE_STATUS= values, =REQUIRES_TOOLS= optional field) in the freed space. Total documentation surface (spec + seven READMEs ≈ 1000 lines) is larger than v4's 259 lines, but the spec and the READMEs serve different audiences — protocol-thinkers and CLI-users — and a reader of just the spec can comprehend the protocol without consulting any README.
-
-* When to use
-
-When two Claude sessions in different projects (same machine or different machines on the same Tailscale tailnet) need to coordinate on a shared task that one session can't complete alone — typically because one has tooling, context, or MCP access the other doesn't.
-
-Examples that fit:
-- Session A asks session B to apply a workflow patch in B's project, then verify it.
-- Session A runs a long task and needs session B to monitor results in B's domain.
-- Two sessions co-design a workflow.
-
-Examples that don't fit:
-- A simple file handoff that doesn't require iteration.
-- A task one session can do alone.
-- Cross-tailnet or cross-organization. The protocol is local-tailnet-scoped.
-
-* Protocol
-
-** File location
-
-Each project has =inbox/from-agents/= as its agent-comms mailbox. Create the directory if it doesn't exist; set permissions =chmod 700= and ownership to the user.
-
-- Sender writes to receiver's =inbox/from-agents/=.
-- Receiver polls (or watches) =inbox/from-agents/=, *not* the parent =inbox/=.
-- The parent =inbox/= stays reserved for human-triage items.
-- Out-of-band artifacts (PDFs, datasets) live at =inbox/from-agents/artifacts/=. Reference by relative path in the message body.
-
-The user does NOT write directly to =from-agents/=. To inject input into a running conversation, the user tells one of the agents in that agent's session; the agent writes the input as a normal message attributed to the user.
-
-** File naming
-
-=YYYYMMDDTHHMMSSZ-from-<sender>-<short-conv-id>.org=
-
-- Timestamp is UTC ISO 8601 compact. The trailing =Z= is mandatory.
-- =from-<sender>= prefix.
-- =<short-conv-id>= is a stable kebab-case slug across the back-and-forth. Reusable across time; ordering relies on filename timestamps.
-
-Frontmatter =#+TIMESTAMP= carries the same instant in local time with explicit offset. The two MUST refer to the same instant.
-
-The implementation (=cross-agent-send=) generates the canonical filename from the message's frontmatter (=CONVERSATION_ID=, current UTC time) and the sender's project context. Senders supply only the message body file; the script handles naming. Senders MUST NOT pre-name files in this format and pass them through; the script overwrites with its own canonical name to ensure consistency and enable the sender-side max-seen sequence-collision-reduction scan.
-
-GPG signatures live in a sibling file =YYYYMMDDTHHMMSSZ-from-<sender>-<short-conv-id>.org.asc=. Receivers verify before processing. See =* Writes are atomic= for the two-file delivery ordering rule.
-
-** Frontmatter
-
-Required:
-
-#+begin_example
-#+TITLE: <human-readable subject>
-#+CONVERSATION_ID: <stable across the thread>
-#+MESSAGE_TYPE: <see types below>
-#+SEQUENCE: <integer hint>
-#+TIMESTAMP: <ISO 8601 with explicit offset>
-#+PROTOCOL_VERSION: 5
-#+end_example
-
-Optional:
-
-#+begin_example
-#+REQUIRES_TOOLS: <comma-separated tool/MCP slugs, e.g. gmail-mcp, slack-mcp>
-#+RELEASE_STATUS: <see release-statuses; valid only on MESSAGE_TYPE: release>
-#+WORKFLOW_VERSION: <sender's version of cross-agent-comms.org; informational only in v5 — no enforcement>
-#+end_example
-
-Receiver sanity-checks frontmatter before acting. Missing or malformed frontmatter → surface to user, don't proceed. Mismatched =PROTOCOL_VERSION= → receiver writes a =query= asking the originator to upgrade.
-
-** Identity
-
-Messages are GPG-signed by the sender. Receivers verify the detached signature before processing the message body.
-
-The implementation (=cross-agent-send=) signs automatically with the sender's configured key (the user's primary GPG key by default; configurable via =--key= flag or environment). Receivers verify automatically against the keys in their GPG keyring.
-
-Identity is a three-layer story:
-
-1. *Tailscale layer.* Only tailnet members can reach the rsync-over-SSH endpoint at all.
-2. *POSIX layer.* =chmod 700= on =from-agents/= means only processes running as the directory's owner can write.
-3. *GPG layer.* Sender's signature on each message proves the message originated from a process holding the key.
-
-Three independent layers. Per-user GPG (using existing keys) gives a correctness check more than a security boundary — unsigned messages are almost certainly bugs, not attackers. That's still load-bearing.
-
-** Writes are atomic
-
-Writers MUST use a temp-file + rename pattern (=mktemp= + =mv= within the same filesystem) so receivers never see partial files. The implementation script (=cross-agent-send=) handles this.
-
-Receivers ignore =.tmp.*= files, processing only the final renamed name.
-
-*Two-file ordering.* When a message has a sibling GPG signature file (=.org.asc=), the writer MUST rename the =.asc= to its final name *before* renaming the =.org=. Two =mv= operations are not atomic together — without this ordering, a receiver could read the =.org= in the window between the two renames and fail GPG verify because the =.asc= hasn't landed yet. The rule: receiver only acts on =.org= files, and a =.org= without a corresponding =.asc= means the signature is genuinely missing (not still in flight).
-
-** Sequence numbering
-
-=#+SEQUENCE= is a *hint*, not a strict counter. Canonical order is =#+TIMESTAMP=. Sequences may collide under rapid back-and-forth (both sides write what they think is sequence N near-simultaneously). Treat collision as a normal protocol event.
-
-*Receiver-side dedup rule.* When a new file shares =CONVERSATION_ID= + =SEQUENCE= with an already-processed message, compare SHA-256 hashes. Identical hashes → silent dedup, treat as a retry. Different hashes → process both, ordered by =#+TIMESTAMP=.
-
-*Sender-side collision-reduction (best-effort).* Before picking sequence, scan the receiver's =from-agents/= for the highest existing sequence in this conversation across both sender prefixes. Use =max(seen) + 1=.
-
-** Message types
-
-- *request* — a side asks for work, input, or a decision. Sequence 1 is always =request=.
-- *progress* — work-in-progress checkpoint. "Here's where I am, no action needed from you, more coming." Originator's poll loop should NOT page the user on progress messages.
-- *query* — either side asks a clarifying question that blocks further work. Originator's poll loop SHOULD surface this immediately. Originator answers and work continues.
-- *pushback* — receiver formally disagrees with the request and has *not* started the work. Carries reasoning. Distinct from =query= because the originator's response path differs.
-- *complete* — receiver signals the requested work is done. Triggers verification.
-- *release* — terminal type. Originator writes after verifying =complete=. Carries =RELEASE_STATUS= to disambiguate the closure mode.
-- *escalate* — punts the conversation to the user for adjudication. Both sides pause polling on =escalate=; the user resolves.
-
-Reply expectation is implied by type: =request=, =query=, =pushback=, =escalate= expect a reply; =progress=, =complete=, =release= don't.
-
-** Conversation lifecycle
-
-A conversation is a directed loop between an originator (issued sequence 1) and a receiver:
-
-1. Originator writes =request= (sequence 1). Begins polling for replies.
-2. *Optional acknowledgment.* Receiver may write a =progress= at sequence 2 to acknowledge receipt and set expectations. Required if work will take >5 minutes (so the originator's poll loop doesn't waste wakes).
-3. *Optional echo-back.* For ambiguous or large requests, receiver writes a =progress= that restates work items and announces "starting now unless you push back within N minutes."
-4. Receiver works. May write =progress= updates. =query= mid-work if blocked. =pushback= if the request is wrong.
-5. Receiver writes =complete=. Begins polling for =release=.
-6. Originator reads, *verifies the deliverable directly*. For subjective deliverables, verification is the originator's editorial accept.
-7. If verified: =release= with =RELEASE_STATUS: complete=. If problems: new =request= (next sequence number).
-8. Receiver sees =release=, stops polling.
-
-The verification step is load-bearing. =complete= is a *claim*; =release= is *verification*.
-
-** Pushback path
-
-On receiving a =pushback=, the originator chooses:
-
-1. *Revise* — new =request= with adjusted scope.
-2. *Insist* — new =request= addressing the pushback's reasoning, standing by direction.
-3. *Withdraw* — =release= with =RELEASE_STATUS: withdrawn-after-pushback=.
-
-*Deadlock cap.* After two pushback-insist exchanges, the next message MUST be =MESSAGE_TYPE: escalate=. Both agents pause polling; the user resolves.
-
-** =RELEASE_STATUS= values
-
-| Status | Meaning |
-|---+---|
-| =complete= | Goal achieved, originator verified |
-| =cancelled= | Originator changed their mind mid-conversation |
-| =withdrawn-after-pushback= | Originator chose option 3 on receiver's =pushback= |
-| =abandoned-after-escalation= | User adjudicated and chose to close the conversation |
-| =abandoned-after-timeout= | Receiver auto-closed after originator never returned to verify |
-
-** Async fallback
-
-If the originator session ends between =request= and =complete=, the receiver's =complete= goes unverified. Receiver behavior:
-
-- Polls for =release= up to ~24 hours of cycles (implementation default).
-- After timeout, writes a final =progress= message ("treating as terminal-without-verification; originator never returned to release") and stops polling. Receiver does NOT write =release= itself — that would contradict the lifecycle rule that =release= is the originator's terminal action.
-- Next time the originator project starts, the unreleased =complete= is surfaced as a startup item. The user can issue a late =release= (with whichever =RELEASE_STATUS= fits) or open a fresh conversation to revisit. =RELEASE_STATUS: abandoned-after-timeout= is used at that point if the user wants to formally close the orphaned thread.
-
-** Escalation
-
-A side writes =escalate= when:
-- Pushback-insist deadlock cap reached.
-- Conversation has stalled (no productive movement in N exchanges).
-- A reply-expecting message has gone unanswered past timeout.
-
-Body summarizes both sides' positions in 60 seconds of reading. Both agents pause polling; the user resolves.
-
-* Implementation notes
-
-This sub-section describes how to operate the protocol. Operational detail lives in the seven scripts' READMEs.
-
-** Recommended scripts
-
-| Script | Replaces user action | README |
-|---+---+---|
-| =cross-agent-send <dest> <msg>= | Filename generation, GPG sign, atomic write, peer lookup, rsync push, retry+backoff, failure surfacing — seven mechanical sender-side steps. Frontmatter and message body are still author-supplied. | =cross-agent-send.md= |
-| =cross-agent-recv <msg>= | Frontmatter sanity-check, =PROTOCOL_VERSION= verify, GPG verify, SHA-256 dedup, =REQUIRES_TOOLS= check — five mechanical receiver-side steps. Output is a structured decision (=process= / =dedup= / =query= / =reject=) the agent acts on. | =cross-agent-recv.md= |
-| =cross-agent-watch= | Manually checking inboxes; "did I get a message?" | =cross-agent-watch.md= |
-| =cross-agent-status= | Walking each project to count pending messages | =cross-agent-status.md= |
-| =cross-agent-discover= | Remembering project topology and reachability | =cross-agent-discover.md= |
-| =cross-agent-halt [reason] [--tailnet]= | Visiting each session to stop polling, restarting Claude Code, or hand-killing processes when comms go runaway. =--tailnet= propagates HALT to all peers. | =cross-agent-halt.md= |
-| =cross-agent-resume [--tailnet]= | Manually clearing the HALT state and restarting the watcher. Per-session polling does NOT auto-resume — the user re-engages each session explicitly. | =cross-agent-resume.md= |
-
-The scripts are tools the user runs from any terminal. They do not depend on agent context — =cross-agent-status= run from a fresh shell works.
-
-A reader can comprehend this protocol from this spec alone. Script READMEs add operational detail that makes the protocol practical to use, but understanding the protocol's semantics requires only this document.
-
-** Polling
-
-Default cadence: 270 seconds (≈4.5 min). Sits just under the 5-minute prompt-cache TTL.
-
-If a side needs to slow down (heads-down work, idle wait), it writes a =progress= message saying so in prose. The other side adapts. There are no named polling modes.
-
-After ~12 empty polls in a row, the poll loop surfaces the silence to the user.
-
-A future runtime with native filesystem-event support could replace polling for active sessions; =cross-agent-watch= already provides event-driven notifications outside active sessions.
-
-** User multi-tasking
-
-- *Deferral.* If the user's last message in the agent's session was less than 60 seconds ago AND a poll fires, queue the inbox check until either the user sends another message OR 5 minutes pass without further input.
-- *Surfacing.* On the next user-facing response: "While we were working on X, a cross-agent message landed from <project>. It's a =<type>= — want me to handle it now or after we finish?"
-- *Mid-question.* Answer the user first.
-- *Project switch.* If the user moves to the receiver project mid-conversation, the receiver agent surfaces the in-flight thread on first user prompt.
-- *Conversation state.* Always include in any response that mentions a cross-agent thread: "<conv-id> at sequence N, awaiting <event>."
-
-** Failure modes
-
-The seven scripts surface most failures with concrete error messages. Spec-level failure modes:
-
-- *Malformed frontmatter on a received file.* Surface to user; do not act.
-- *Mismatched =PROTOCOL_VERSION=.* Receiver writes =query= asking originator to upgrade.
-- *Missing or invalid GPG signature.* Receiver surfaces "unsigned/unverified message"; refuses to act.
-- *Sequence collision* with non-matching SHA-256. Process both, ordered by timestamp.
-- *Required tool unavailable.* Receiver checks =REQUIRES_TOOLS= during frontmatter-sanity-check (before any work begins). On a missing tool, receiver writes =query= asking the originator to reframe the request to avoid the unavailable tool. Originator may revise (new =request=) or withdraw (=release= with =RELEASE_STATUS: cancelled=). =query= is the right type rather than =pushback= because missing-tool is a capability gap, not disagreement.
-- *Runaway resource usage.* User invokes =cross-agent-halt= globally (or =cross-agent-halt --tailnet= for cross-machine). HALT file stops all components within one polling cycle (~5 min). See =* Halt mechanism= for the layered checks.
-- *User halts mid-conversation.* Both sides write a final =progress= note ("HALT fired; pausing"); polling stops within one cadence; conversations resume on explicit per-session re-engage after HALT clears.
-- *HALT file accidentally created* (typo, errant =touch=). =cross-agent-status= prominently flags HALT active; user clears with =cross-agent-resume=. Cost: no messages send during the typo window.
-- *HALT file unreadable* (perms wrong, partial write). Each component fails-closed (treats as halted) and reports "HALT file present but unreadable; treat as halted." Safer than fail-open.
-
-Operational failures (rsync push fails, watcher dies, peer unreachable) live in the script READMEs' failure-mode tables.
-
-* Halt mechanism
-
-A failsafe to stop all cross-agent activity on a machine without visiting individual sessions or restarting Claude Code. Designed for the runaway-polling case: an agent has spun up conversations with N other agents, polling is eating CPU, and the user needs to stop everything *now*.
-
-** The HALT file
-
-Path: =~/.config/cross-agent-comms/HALT=.
-
-Existence triggers halt across all components on the machine. The file's body may carry an optional human-readable reason (reviewed by the user later when deciding to resume).
-
-User commands:
-
-#+begin_example
-$ touch ~/.config/cross-agent-comms/HALT # halt
-$ rm ~/.config/cross-agent-comms/HALT # resume
-#+end_example
-
-Or via convenience scripts (=cross-agent-halt= / =cross-agent-resume=) that also handle the watcher service and cross-machine propagation.
-
-** Layered checks (the failsafe property)
-
-Every component MUST check the HALT file. The "any one component stops the system independently" property is what makes this failsafe — the system doesn't depend on a single point doing the right thing.
-
-| Component | Check timing | Behavior on HALT |
-|---+---+---|
-| =cross-agent-send= | At start of send + between =.asc= and =.org= rsync + between retry iterations | Refuse to start new send; complete current step then exit. Worst case: one in-flight send finishes within a few seconds. |
-| =cross-agent-recv= | Before any verify or dedup | Leave inbound message in place — do NOT dedup, reject, or move. Resume picks it up via cold-start handling. |
-| =cross-agent-watch= | At iteration start | Suppress notifications; log only. Continues running, no-op until HALT clears. |
-| =cross-agent-status= | At start | Print prominent "⚠ HALT ACTIVE" banner before normal output. Read-only, continues. |
-| =cross-agent-discover= | At start | Print HALT banner; continue read-only enumeration. |
-| Agent polling loop | First action on every wake | Write a final =progress= note to any active conversation ("HALT fired; pausing"), do NOT reschedule, surface "halt active" to user. Polling decays within one cadence (~5 min). |
-| Agent user-facing responses | Every response while HALT is set | Append "(HALT active; cross-agent comms paused)" to the response. On HALT clear, the next response says "(HALT cleared; cross-agent comms ready to resume — say so to re-engage polling)." Persistent, not just first-response — keeps awareness alive. |
-| Conversation initiator | Before writing sequence 1 of any new conversation | Refuse and surface to user. |
-| Startup workflow | Phase A on session start | If HALT exists, surface immediately and skip cross-agent inbox checks. |
-
-The agent polling-loop check is the load-bearing one for "stops eating CPU." Wake-ups already scheduled fire, but each wake on-HALT is a no-op + reschedule-prevention. Within one polling cadence (~5 min) all polling stops.
-
-*Fail-closed on unreadable HALT.* If the HALT file exists but is unreadable (wrong permissions, partial write), components MUST treat as halted. Safer than fail-open.
-
-** Resume asymmetry (deliberate)
-
-Halt is automatic everywhere. Resume requires explicit user intent per-session.
-
-When the user removes HALT (or runs =cross-agent-resume=), components stop refusing to act, but agent polling does NOT auto-resume. The user must open each session and tell that agent to resume polling for its conversations.
-
-The asymmetry exists because:
-
-1. Auto-resume could silently invert intentional kills. If the user halted because a session was misbehaving, removing HALT shouldn't quietly revive it.
-2. Per-session resume forces the user to look at each session and confirm the situation is resolved before re-engaging.
-
-** Cross-machine halt
-
-=cross-agent-halt --tailnet= iterates =peers.toml= and SSH-touches HALT on each peer. Same shape for resume.
-
-Reports per-peer status with non-zero exit on partial halt:
-
-#+begin_example
-$ cross-agent-halt --tailnet
-Halting velox.local ✓ (HALT file written)
-Halting bastion.local ✗ (ssh exit 255: no route to host)
-Halting locally ✓ (HALT file written)
-
-PARTIAL HALT: 2/3 machines halted. bastion.local needs manual halt.
-Exit 1.
-#+end_example
-
-Scripting can detect partial halt via the exit code. Same pattern for =--tailnet= on resume.
-
-* Limitations
-
-- *Local-tailnet only.* Filesystem IPC + rsync over SSH. Cross-tailnet or cross-organization is out of scope.
-- *Identity has three layers (Tailscale + POSIX + GPG)* but no message-content encryption. Confidentiality is not the goal; signing is correctness, not secrecy.
-- *Single-receiver per conversation.* Fan-out to multiple receivers requires manually orchestrating multiple parallel conversations.
-- *Polling is best-effort.* A wake may be delayed by an in-flight tool call until the runtime is idle. =cross-agent-watch= mitigates by offering event-driven notifications.
-- *Project-extension drift.* If two projects' =.ai/project-workflows/= modify shared workflow definitions in incompatible ways, cross-agent assumptions can diverge silently. The optional =#+WORKFLOW_VERSION= advisory field is informational only in v5 — no implementation reads or acts on it. A future version may add enforcement on mismatch (e.g. receiver writes =query= asking which side is stale). Today, alignment is verified manually before high-stakes conversations.
-
-* Persistence after release
-
-Conversation files persist by default. The conversation log is the audit trail.
-
-Manual archival is fine if the inbox grows unmanageable. Suggested cadence: once the conversation has been =release='d AND the work it produced has shipped, archive both projects' message files into =.ai/sessions/cross-agent/= as a flat directory — no per-conversation subdirectories. Rename each archived file to lead with the conversation-id so messages from the same conversation cluster on =ls=: =<conv-id>-<TIMESTAMP>-from-<sender>.org= (and the matching =.asc= sibling, if present). Inbox filenames lead with the timestamp because chronological arrival is what matters in =from-agents/=; archives invert that because grouping by conversation is what matters when reading history. Keep the =.asc= signatures alongside the =.org= files in archive — they're small and document the GPG verification chain.
-
-Old messages don't affect protocol behavior (=cross-agent-status='s pending semantics correctly ignore released messages) but the =from-agents/= directory grows indefinitely without manual archival. =cross-agent-status= performance degrades noticeably when a project's =from-agents/= exceeds a few hundred files. =cross-agent-init= (deferred to v6) would include an archival sub-command.
-
-* Open questions
-
-- *=cross-agent-init= and =cross-agent-compose= helper scripts.* =-init= would be one-command project bootstrap (creates =inbox/from-agents/= with =chmod 700=, installs the =cross-agent-watch= systemd path unit, validates peer config, runs a discovery probe). =-compose= would be interactive frontmatter authoring (prompts for required fields, produces a draft message file). Both deferred to v6. Current onboarding requires manual =mkdir= + systemd setup per =cross-agent-watch.md='s install recipe; current message authoring requires writing the file by hand or via a small in-agent template.
-- *Hard conversation timeout.* The async-fallback timeout is implementation-default ~24 hours. Right number depends on use case; tighten as patterns emerge.
-- *=paused= polling state.* Today there's no clean signal for "pause without ending." Add when first user complaint surfaces.
-- *Multi-LLM context.* If we ever bring in a non-Claude agent, the protocol's natural-language framing may need formalization.
-
-* Examples
-
-** =prep-fixup= conversation (2026-04-26 → 2026-04-27)
-
-Eleven exchanges between homelab and career produced the v4 spec by iterative critique-and-simplification. Three real-time sequence collisions during the conversation drove the sequence-as-hint rule that landed in v4 and persists in v5.
-
-Files at =~/projects/{homelab,career}/inbox/from-agents/= named =*-prep-fixup.org=. Worth re-reading when designing future cross-agent flows.
-
-** =comms-cold-start-discovery= conversation (2026-04-27)
-
-The follow-up that produced this v5 spec. Cold-start, watcher tooling, agent discovery, GPG identity, sha256 dedup, atomic writes, POSIX perms, script absorption, and process-vs-text simplification. Tonight's first cold-start in real time (career session went dormant after =prep-fixup= release; Craig's user-injection re-engaged it) is the worked demonstration of the v5 user-injection rule.
-
-Files at =~/projects/{homelab,career}/inbox/from-agents/= named =*-comms-cold-start-discovery.org=.
diff --git a/claude-templates/.ai/workflows/daily-prep.org b/claude-templates/.ai/workflows/daily-prep.org
index b6989e7..3f21214 100644
--- a/claude-templates/.ai/workflows/daily-prep.org
+++ b/claude-templates/.ai/workflows/daily-prep.org
@@ -1,5 +1,5 @@
#+TITLE: Daily Prep Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-11
* Overview
@@ -74,7 +74,7 @@ The separate =* Standup Briefs= and =* Upcoming Deadlines= sections are *retired
Items that frame the day. Four standing items are *always* present, plus the look-ahead:
1. *Meeting-density framing.* One line on the day's shape, e.g. "Meeting-dense morning: 09:00 team discussion, 10:00 standup, 11:00 general standup. Real focus time only opens at noon."
-2. *Calendar events from BOTH calendars* (work + personal): birthdays, holidays, anniversaries, vacations, trips, big events. "Your trip begins Friday." "It's <person>'s birthday today."
+2. *Calendar events from BOTH calendars* (work + personal): birthdays, holidays, anniversaries, vacations, trips, big events. "Your trip begins Friday." "It's <person>'s birthday today." Fold in the =upcoming_birthdays.py= block from Phase A (source 8) for contact birthdays the calendar doesn't carry; keep its callout entries (within 7 days) so a gift or plan gets prompted.
3. *Reminders due, imminently due, or past trigger* — from notes.org Active Reminders plus scheduled/deadline tasks. A reminder tied to a rescheduled meeting reports against the new date.
4. *Requested metrics* — a slot for any metric Craig has asked to track in the daily prep. Render the slot only when a metric is active; none are active by default. (Metric design is a separate discussion — don't invent metrics.)
5. *5-Day Look-Ahead* — one day per line, format =Fri 12:= / =Mon 15:=, including clear days marked =clear=. Built by Phase 1's forward scan with the invite quick-read and decline gate applied.
@@ -192,6 +192,7 @@ Pull every source in a *single batch of parallel tool calls*:
5. Pull the project tracker's view of Craig's plate (assigned issues, items in review, blocked items) where the project has one.
6. List + read the *previous* prep doc. Glob =daily-prep/*-daily-prep.org=, sort by date, take the file *before* the one the root =daily-prep.org= symlink resolves to. If the symlink doesn't resolve yet, take the most recent file. The standup lookback anchors on this file's date.
7. Read the most recent =.ai/sessions/= summary (for the standup brief's lookback).
+8. Run =.ai/scripts/upcoming_birthdays.py= (reads =~/sync/org/contacts.org=). Its block feeds the Heads-Up birthday line — contacts carry birthdays the calendar doesn't, and anything inside 7 days comes back flagged so a gift/plan gets prompted.
This fetch *is* the live calendar read for build time. In Update mode, re-run the calendar fetches — never reuse the build-time snapshot.
@@ -239,7 +240,7 @@ Assemble priorities from both sources; no mid-flow confirmation (the gate handle
*** Sub-step 3b: Triage external sources (delegate to triage-intake.org)
-Don't scan email / Slack / tracker / PRs inline. Run the =triage-intake.org= engine (if the mode's freshness check already ran one this hour, use its synthesis). It classifies everything new against the four-bucket model, writes every Action item into =todo.org= as its own =:quick:reactive:= task, and executes the routine actions on confirmation. Source coverage comes from its Phase 0 plugin load — both =.ai/workflows/triage-intake.*.org= (general) and =.ai/project-workflows/triage-intake.*.org= (project-specific). A missing source is a missing plugin, not a daily-prep regression.
+Don't scan email / Slack / tracker / PRs inline. Run the =triage-intake.org= engine (if the mode's freshness check already ran one this hour, use its synthesis). It surfaces the three-section digest (==TASKS== / ==FYI== / ==MISC==) and then closes by default per its Phase D — filing every TASKS item into =todo.org= as its own =:quick:reactive:= task and running the routine mail hygiene without itemized confirmation. Source coverage comes from its Phase 0 plugin load — both =.ai/workflows/triage-intake.*.org= (general) and =.ai/project-workflows/triage-intake.*.org= (project-specific). A missing source is a missing plugin, not a daily-prep regression.
*** Sub-step 3c: Build the entries
diff --git a/claude-templates/.ai/workflows/delete-calendar-event.org b/claude-templates/.ai/workflows/delete-calendar-event.org
index 5bb92a1..7de0086 100644
--- a/claude-templates/.ai/workflows/delete-calendar-event.org
+++ b/claude-templates/.ai/workflows/delete-calendar-event.org
@@ -1,5 +1,5 @@
#+TITLE: Delete Calendar Event Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-01
* Overview
diff --git a/claude-templates/.ai/workflows/edit-calendar-event.org b/claude-templates/.ai/workflows/edit-calendar-event.org
index 662f0b4..27a9dd3 100644
--- a/claude-templates/.ai/workflows/edit-calendar-event.org
+++ b/claude-templates/.ai/workflows/edit-calendar-event.org
@@ -1,5 +1,5 @@
#+TITLE: Edit Calendar Event Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-01
* Overview
diff --git a/claude-templates/.ai/workflows/email-assembly.org b/claude-templates/.ai/workflows/email-assembly.org
index 003459c..699dbc0 100644
--- a/claude-templates/.ai/workflows/email-assembly.org
+++ b/claude-templates/.ai/workflows/email-assembly.org
@@ -1,5 +1,5 @@
#+TITLE: Email Assembly Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-01-29
* Overview
diff --git a/claude-templates/.ai/workflows/extract-email.org b/claude-templates/.ai/workflows/extract-email.org
index 3a70bea..c68bafe 100644
--- a/claude-templates/.ai/workflows/extract-email.org
+++ b/claude-templates/.ai/workflows/extract-email.org
@@ -1,5 +1,5 @@
#+TITLE: Extract Email Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-06
* Overview
diff --git a/claude-templates/.ai/workflows/find-email.org b/claude-templates/.ai/workflows/find-email.org
index 0ef9615..d71ed3e 100644
--- a/claude-templates/.ai/workflows/find-email.org
+++ b/claude-templates/.ai/workflows/find-email.org
@@ -1,5 +1,5 @@
#+TITLE: Find Email Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-01
* Overview
diff --git a/claude-templates/.ai/workflows/first-session.org b/claude-templates/.ai/workflows/first-session.org
index 60118a2..147026f 100644
--- a/claude-templates/.ai/workflows/first-session.org
+++ b/claude-templates/.ai/workflows/first-session.org
@@ -1,5 +1,5 @@
#+TITLE: First Session Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
Run this workflow on the first Claude Code session for a new
project. It establishes the git/.ai policy, orients Claude to the
diff --git a/claude-templates/.ai/workflows/flashcard-review.org b/claude-templates/.ai/workflows/flashcard-review.org
index 31027b3..09af348 100644
--- a/claude-templates/.ai/workflows/flashcard-review.org
+++ b/claude-templates/.ai/workflows/flashcard-review.org
@@ -1,5 +1,5 @@
#+TITLE: Drill Deck Review Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-30
* Overview
diff --git a/claude-templates/.ai/workflows/helper-mode.org b/claude-templates/.ai/workflows/helper-mode.org
index 8ead37b..a6acfa7 100644
--- a/claude-templates/.ai/workflows/helper-mode.org
+++ b/claude-templates/.ai/workflows/helper-mode.org
@@ -1,5 +1,5 @@
#+TITLE: Helper Mode Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-15
* Overview
@@ -65,7 +65,7 @@ The git ban is concurrency-scoped. /Helper wrap-up/ below lifts it for exactly o
** Escalation
-Anything the contract blocks routes through the cross-agent message form (=machine.project.agent-id=), or just gets reported to Craig. The helper leaves its tree changes for the primary's next commit, or describes them in a targeted message.
+Anything the contract blocks gets reported to Craig, or — for a cross-project handoff — routed through =inbox-send= to the owning project's =inbox/=. The helper leaves its tree changes for the primary's next commit, or describes them in a note to Craig.
* Data-Integrity Rules
diff --git a/claude-templates/.ai/workflows/inbox-zero.org b/claude-templates/.ai/workflows/inbox-zero.org
deleted file mode 100644
index aa7c273..0000000
--- a/claude-templates/.ai/workflows/inbox-zero.org
+++ /dev/null
@@ -1,97 +0,0 @@
-#+TITLE: Inbox Zero Workflow
-#+AUTHOR: Craig Jennings & Claude
-#+DATE: 2026-06-13
-
-* Overview
-
-The roam global inbox (=~/org/roam/inbox.org=) is Craig's cross-project GTD capture: one shared file every project can see. This workflow routes each inbox item to the project that owns it. The current session claims only the items belonging to THIS project, files them into the project's =todo.org=, and removes them from the shared inbox. Everything it doesn't own stays. The aspiration is inbox zero: over time, every item lands in its owning project.
-
-This is NOT =process-inbox.org=. That workflow handles the project's own =inbox/= directory (handoffs from other projects, scripts, Craig). This one handles the single global =roam/inbox.org= and the cross-project routing a shared file creates.
-
-This is also distinct from the wrap-up inbox/transcript routing feature (which moves session-filed keepers between projects). This routes the shared roam capture file by ownership prefix.
-
-** Scope: single-destination (v1)
-
-This version routes each item to its one owning project, identified by an explicit =<project>:= heading prefix. The multi-project domain-aware mode, which would guess the owner of every unprefixed item and empty the whole inbox in one run, is deferred (see "Deferred: domain-aware routing" at the end). v1 claims only what's prefixed for the current project, surfaces the rest, and never guesses.
-
-** Three callers
-
-Reused from three callers so the steps live in one place:
-- *Startup* (read-only nudge) — count the items, identify which appear related to this project, surface both numbers, offer processing as one of the startup options. Never auto-files.
-- *Wrap-up* (Step 3 sub-step) — sweep items that belong here before the cleanup scripts, so imported tasks lint and ride the wrap commit.
-- *On demand* — "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox".
-
-Each project touches the roam inbox at least twice a session this way: once at startup, once at wrap-up.
-
-* The ownership rule (the coordination primitive)
-
-The inbox is shared, so the workflow must never let two projects fight over an item or let one project grab another's. Ownership is by explicit prefix:
-
-- =<project>: ...= heading → owned by that project. The current project claims only items prefixed with its own identifier.
-- Prefixed for *another* project → leave untouched (cross-project boundary, =protocols.org=).
-- *No prefix* → unowned. Never auto-claim. Surface as candidates a human can claim or prefix.
-
-The prefix partition is what makes concurrent triage across projects safe: each project only ever removes its own items, so two sessions editing the inbox touch disjoint lines.
-
-** Resolving this project's identifier (v1)
-
-Use the project root basename plus its common aliases (=.emacs.d= ↔ =emacs=, and the obvious ones: =rulesets=, =work=, =home=). A project may override the inferred set with an =:INBOX_PREFIX:= line in =notes.org='s *Workflow State* section when the basename is fragile (a dot in the name, an alias the inference misses). The explicit override is optional in v1; the durable multi-project resolution is part of the deferred domain-aware mode.
-
-* Phase A — Identify, count, and match
-
-1. Resolve the current project's identifier and aliases (above).
-2. Read =~/org/roam/inbox.org=. If absent, silent no-op (the file lives only on machines with the roam clone).
-3. Bucket every item under the inbox heading:
- - *claimed* — prefixed for this project
- - *foreign* — prefixed for another project → leave
- - *unowned* — no project prefix
-4. *Summarize the scan* (Craig's requirement, every scan): report the total item count in the inbox, then the count that appears related to this project. "Appears related" is the union of claimed items (exact prefix) and any unowned item whose topic plainly concerns this project's domain (a content judgment, surfaced as a candidate, never auto-claimed). Foreign-prefixed items are not "related" — they belong to their owner.
-5. If both claimed and related-unowned are empty, report the total and stop (the common case for most wraps).
-
-* Phase B — File each claimed item into todo.org
-
-Apply =process-inbox.org='s discipline against the project's =todo.org=; don't reinvent it:
-
-1. *Status check first.* Already done, or already a task in =todo.org=? → drop it, or fold into the existing task (dated sub-entry per =todo-format.md=). Don't duplicate.
-2. *Rewrite* to terse-heading + body per =todo-format.md=.
-3. *Priority + tags from THIS project's scheme* — the legend at the top of its =todo.org=, tags from that scheme's allowed set only. The project expresses someday-maybe with =[#D]=; there's no special someday-maybe routing.
-4. *File* under the project's Open Work section.
-
-* Phase C — Reconcile the shared inbox
-
-The roam inbox lives in a git repo (=~/org/roam=, auto-synced by the =roam-sync= timer). Edit it carefully:
-
-1. *Pull first* (=git -C ~/org/roam pull --ff-only=). If it can't fast-forward (dirty tree, divergence), surface and stop. Don't auto-stash, auto-merge, or force. Resolve before removing items.
-2. *Remove only the claimed items.* Never touch foreign or unowned items.
-3. *Commit the roam repo as its own commit* (separate from any project wrap commit): =chore(inbox): route <project> tasks to <project>/todo.org=. Push, or leave for the =roam-sync= timer. Surface a blocked push; don't force.
-
-* Phase D — Surface
-
-Report: moved (with their new priorities and tags), folded, dropped-as-done. Then the residue: foreign items (left for their owners, count only) and unowned items (count plus the headings that appear related to this project, for manual claim or prefix). Same "summarize what we kept" shape.
-
-* Skip conditions
-
-- No =~/org/roam/inbox.org= → silent no-op.
-- No claimed and no related-unowned items → report the total, stop.
-- Roam pull blocked → surface, stop before editing.
-
-* Caller integration
-
-** Startup (read-only nudge)
-
-Phase A of =startup.org= reads =~/org/roam/inbox.org= and produces the scan summary; Phase C surfaces one line: "Roam inbox: N items total, M appear related to this project — say 'inbox zero' to file them." Offered as one of the priority options. Startup never auto-files; it counts and offers.
-
-** Wrap-up (Step 3 sub-step)
-
-A sub-step at the start of wrap-up Step 3 (before the cleanup scripts, so imported tasks get linted and ride the wrap commit) delegates here for the claimed set. Skip-fast when nothing matches.
-
-* Deferred: domain-aware routing (future work, multi-project)
-
-v1 handles the single-destination case via the prefix rule. The multi-project parts are deferred until the need is real:
-
-1. *Domain-aware empty-it-all mode.* If rulesets held a description of each project's domain, one run could guess the owner of every item (prefixed or not) and empty the whole inbox at once, delivering each item to its owning project's =inbox/= via =inbox-send= (where that project's =process-inbox= gate still decides whether to file it). This turns "inbox zero" from a per-project aspiration into a single command. Open: where the domain map lives (central registry vs each project's =notes.org=), how confident a guess must be before auto-routing vs surfacing, and whether a low-confidence item stays put.
-2. *Explicit per-project =:INBOX_PREFIX:= as the durable resolver*, replacing basename inference.
-3. *Unowned-item lifecycle* once domain-aware routing exists (no item stays unrouted indefinitely).
-4. *Concurrent push contention* on the shared roam repo: the pull-before-edit + ff-only + surface-on-conflict floor may want a retry-once-after-pull.
-
-Take these up when the single-destination version is in use and the multi-project pain is concrete.
diff --git a/claude-templates/.ai/workflows/inbox.org b/claude-templates/.ai/workflows/inbox.org
new file mode 100644
index 0000000..6faa20f
--- /dev/null
+++ b/claude-templates/.ai/workflows/inbox.org
@@ -0,0 +1,524 @@
+#+TITLE: Inbox Workflow (Engine)
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-23
+
+* Overview
+
+One engine for the project's inbox surfaces. Inbox items are *ideas to evaluate*, not orders to execute — each is a proposal that earns a place in =todo.org= or git history only when it passes the value gate. The engine holds the shared disposition machinery once: the three-question value gate, the skeptical review, the disposition ladder, the reply-to-sender discipline, the capture-guard before a roam write, and the priority-scheme check. Each *mode* is a thin section that names which surface it reads, how it enters and exits, and which core steps it runs.
+
+Two surfaces feed a project, and there is a recurring check over the second:
+
+1. *Project-local =inbox/= dir* — handoffs from other projects (via =inbox-send=), from scripts, and from Craig (typed directives saved as files). Handled by *process mode*; watched on a cadence by *monitor mode*.
+2. *Global roam inbox* (=~/org/roam/inbox.org=) — Craig's cross-project GTD capture, one shared file every project can see. Handled by *roam mode*, which claims only the items this project owns. *Auto inbox zero* runs roam mode on a recurring interactive loop.
+
+A *third* surface — external accounts (email / calendar / PRs) — is a different domain and stays in its own engine: =triage-intake.org= and its source plugins are *not* part of this engine. "Deal with my inbox dirs" is here; "what's new across my accounts" is there.
+
+*Two altitudes.* For the user, the trigger phrase picks the mode and the phrases are unchanged (see When to Use). For the implementer, this is one file: the core sections are written once, and each mode references them by name ("run the value gate (core §1) on each item") rather than restating them.
+
+* When to Use This Workflow
+
+The trigger phrase selects the mode. Every phrase below still works; it now routes to a mode of this engine.
+
+** Process mode — the local =inbox/= dir
+
+- "process inbox" / "process the inbox"
+- "handle the inbox"
+- "what's in inbox" / "what's in the inbox"
+- "let's clear the inbox" / "let's process the inbox items"
+
+Auto-invocation: startup Phase C delegates here when the local inbox is non-empty — don't ask, just run it.
+
+** Monitor mode — process mode on a cadence
+
+- "monitor the inbox" / "watch the inbox" — *the defined meaning:* one process pass now, then loop every 15 minutes (see the Monitor mode Cadence section). The phrase *is* the loop, not an opt-in extra.
+- "respond to the handoffs" / "handle the handoffs" — a single pass now, no loop.
+
+Ambient (always on, even with no loop running): the =inbox-status= task-boundary check (Monitor mode).
+
+** Roam mode — the global roam inbox
+
+- "inbox zero" / "empty the inbox" / "process the roam inbox" / "triage my roam inbox"
+
+Called read-only from startup (count + offer) and as a wrap-up Step 3 sub-step.
+
+** Auto inbox zero — recurring interactive roam check
+
+- "auto inbox zero"
+
+Match this before "inbox zero" — the auto phrase contains the roam phrase as a substring, so the longer match wins. Starts a recurring =/loop=-driven roam-mode pass; see the Auto inbox zero mode.
+
+** Boundary
+
+Do *not* invoke this engine for an inbox item that is clearly out-of-scope for the project — that is a cross-project routing problem, handled per the cross-project boundary rule in =protocols.org=. And do not invoke it for external-account triage ("what's new in email/cal/PRs") — that is =triage-intake.org=.
+
+* Core §1 — The value gate
+
+Every inbox item (local or roam) passes through three questions. One *yes* is enough to accept.
+
+1. *Does it advance an existing TODO?* Look up by topic in =todo.org='s open work. If the item extends a filed task, fold it in. If it implements a filed task, do the work.
+2. *Does it improve how the project works?* Architecture cleanup, workflow refinement, tooling, rule hygiene, drift detection — anything that makes the project itself more effective.
+3. *Does it serve the project's stated mission?* Read =notes.org= *Project-Specific Context* if the mission isn't obvious from the working directory and current task. The item should advance that mission, not orbit it.
+
+Three *no*s means reject. The rejection isn't lazy — an idea that doesn't help any current task, doesn't improve the system, and doesn't serve the mission is genuine noise, and accepting it inflates =todo.org= without payoff.
+
+* Core §2 — The skeptical review
+
+The value gate decides whether an item is worth taking. This review decides whether what it proposes is *right*, *complete*, and *as simple as it should be*. Run it on every task and file that arrives — not only shared-asset change proposals. Pure FYIs and replies that ask for nothing skip it.
+
+Approach the file with curiosity and skepticism. Work through, in writing — the core pass on every item:
+
+1. Is the request actually right — does it do what it claims, and is the claim correct for this project?
+2. Is it complete, or does it leave a gap — an unhandled case, a missing step, an untested path?
+3. Should it be simpler?
+4. Can it be enhanced to be more effective than as proposed?
+5. Does it conflict with any existing instruction — workflows, skills, rules, protocols, CLAUDE.md?
+
+When the item proposes a change to *shared assets* — template workflows, rules, skills, scripts, anything synced to consuming projects — or to a substantive convention, add the cross-project battery. It arrived from one project's context; you're evaluating it for all of them:
+
+6. Does this make sense for *all* consuming projects, or just the sender's situation?
+7. How does it change a common activity Craig performs — better, worse, or differently than the sender assumed?
+8. Plus at least three more questions specific to this change — what breaks for artifacts already using the old shape, what tooling interacts with it, what's underspecified, what the sender's worked example doesn't exercise.
+
+Output: a short summary of the thinking and a recommendation (do it / do it with named changes / file / reject). For shared-asset and convention changes the recommendation is surfaced to Craig for approval before applying; for ordinary tasks and files it feeds the act-vs-file and no-approvals-execute decision (Monitor mode).
+
+** In a no-approvals session: shared-asset changes defer and stage
+
+Shared-asset and convention changes still don't self-apply when Craig has put the session in no-approvals mode — they need his decision, so they fail the *solo* test in Monitor mode's executing-in-no-approvals criteria. Ordinary tasks and files that pass the review and are quick + solo execute under that criteria instead; this defer-and-stage path is for the shared-asset and convention changes that don't qualify. Run the review, prepare the edits in =working/<task-slug>/= (a patch file or the worked-out diff), file a =[#B]= VERIFY carrying the decision package, and reply to the sender that it's parked. The sender's local stopgap (per =cross-project.md='s propagation process) means the delay costs nothing — the canonical update is about durability, not speed.
+
+Wording-only fixes — no consuming project acts differently — may proceed even then, logged in the session log.
+
+The VERIFY shape (top-level, =[#B]= so startup's A/B surfacing catches it; no =SCHEDULED= unless the proposal names a real deadline):
+
+#+begin_example
+** VERIFY [#B] Parked: <proposal topic> (from <sender>)
+What arrived: <one line — what the handoff proposes>.
+Recommendation: <accept as-is / accept with changes / reject> — <2-3 line
+skeptical-review summary: what's right, what to change, what was checked>.
+Prepared diff: [[file:working/<slug>/proposed.diff]] — apply is mechanical on
+your go.
+Say "approve the parked <topic>" (or adjust / reject) and it gets applied.
+#+end_example
+
+The full question-battery answers live in the session log and the =working/= dir, not the task body — the body carries the conclusion, with the trail one link away.
+
+* Core §3 — The disposition ladder
+
+Every item that clears the value gate gets one disposition. The first six are the per-item outcomes; *park* is the no-approvals shared-asset path from core §2.
+
+** Implement now
+Small, scoped, clear, no design call required. The work is the disposition. Do the work, commit per the project's commit flow, delete the inbox file. The commit message references the inbox item by filename so the provenance lands in =git log=.
+
+** Fold into existing TODO
+The item extends a task already filed. Update the parent TODO's body with a dated reconciliation sub-entry per =todo-format.md= (=*** YYYY-MM-DD Day @ HH:MM:SS -ZZZZ <what landed>=). Move substantive content to =docs/design/<date>-<topic>.<ext>= if it's worth keeping; reference from the TODO body. Delete the inbox file.
+
+** File as TODO
+Substantive but waits, or needs design/triage before implementation. Add the TODO under =* <Project> Open Work= with priority + tags per the priority-scheme check (core §6). Body summarizes the proposal and links the inbox content if it's been moved to =docs/design/=. Delete the inbox file (or move it to =docs/design/= first if the content survives).
+
+*Route-candidate marking (feeds the wrap-up router).* After filing, check whether the keeper's inferred home is a different project:
+
+#+begin_src bash
+python3 .ai/scripts/route_recommend.py --item "<the keeper's heading + body text>" --exclude "$(basename "$PWD")"
+#+end_src
+
+On a =<destination>\tstrong= or =<destination>\tweak= result, stamp the new TODO's property drawer with =:ROUTE_CANDIDATE: <destination>= (create the drawer if the task has none). A =none= result stamps nothing, and a local keeper stays unstamped. The marker is the wrap-up router's entire candidate set — =wrap-it-up.org= Step 3 surfaces exactly the =:ROUTE_CANDIDATE:=-tagged tasks and offers to deliver each to its destination's inbox, never scanning the standing backlog. Stamping is cheap and reversible (the router's skip leaves the task in place; a wrong marker is one property line to delete), so prefer stamping on any plausible match — the human reviews the batch at wrap time.
+
+*Blocking-dependency handoff.* A special shape: another project sends a note that *this* project's work is blocking one of theirs ("your task X is blocked on us — we need Y"). File or link the owning task, tag it =:blocker:=, and name the requesting project in the body (see the cross-project dependency convention in =todo-format.md=). The =:blocker:= tag makes =open-tasks.org= surface that task *first*, since clearing it unblocks the other project. Dedup against an existing task rather than filing a duplicate. When the work later lands, drop =:blocker:= and notify the waiting project (=inbox-send <their-project> --text "Delivered: <what> — you're unblocked."=) so it can lift its own =:blocked:=.
+
+** Defer
+Rename in place to =inbox/PROCESSED-<original-filename>= and add a brief comment line at the top: =# Deferred YYYY-MM-DD: <condition>=. Don't accumulate deferred items indefinitely — sweep them on a future process pass when the condition is met or the deferral has aged out.
+
+** Reject — by source
+- *From Craig* — push back honestly in chat. State why you won't implement; offer the conditions under which you would, if any. The inbox file stays until Craig confirms — override re-enters as accept, acknowledgment deletes the file. Don't theatre the pushback: if you don't genuinely think Craig is wrong, just do the work.
+- *From another project (handoff)* — write a response file at =/tmp/inbox-response-<topic>.org=: a heading naming the original handoff and date, one paragraph on the rejection rationale (*which* value-gate question failed and why), one paragraph on the condition under which you'd reconsider (or "never, this misreads the project's mission" if that's the truth). Deliver via =inbox-send <sender> --file /tmp/inbox-response-<topic>.org=. Delete the local inbox file after the response lands. Silent rejection on a handoff trains the sender to escalate around the channel — always close the loop.
+- *From a script or automated system* — just delete. No notification.
+
+** Park (skeptical review in a no-approvals session)
+Move the proposal file into =working/<task-slug>/= alongside the prepared diff, file the =[#B]= VERIFY per core §2, reply to the sender that it's parked for Craig's review, and delete the inbox file. On Craig's approval the apply is mechanical: apply the prepared edits, run the normal verify-and-publish flow, close the parked =**= VERIFY per =todo-format.md= (a top-level VERIFY resolves to =DONE= + =CLOSED:=, not a dated header), and send the acceptance reply. On rejection, the reject-from-another-project flow above runs unchanged.
+
+* Core §4 — Reply-to-sender discipline
+
+A handoff came from another project's agent (or the user). Close the loop:
+
+- *Accepted and acted on* — send a confirmation to the sender via =inbox-send <sender> --text "..."=, naming what landed and the commit, so they're not left guessing (they can't see this project's git log). =inbox-send= excludes the current project as a target, so a self-sourced item is handled in-session, not sent.
+- *Accepted and filed* — a short confirmation that it's filed and where, so the sender knows it wasn't dropped.
+- *Rejected* — always state the why (which value-gate question failed), per the reject-by-source ladder (core §3).
+
+Cross-project boundary: never act on a file under another project's =.ai/= scope from here — route it back as a handoff (see =cross-project.md=).
+
+* Core §5 — Capture-guard before a roam write
+
+Before *any* read-modify-write of =~/org/roam/inbox.org=, run the capture-guard. This runs first because the Phase D edit rewrites the file on disk, and editing underneath a live capture wedges it just as a stray hand edit would.
+
+*Wait-and-retry, not bounce.* Use the poll mode so a *transient* capture clears itself instead of immediately kicking the work back to the caller:
+
+#+begin_src bash
+.ai/scripts/capture-guard --wait "$HOME/org/roam/inbox.org"
+#+end_src
+
+An org capture is usually only a few seconds of mid-finalize state, so =--wait= (default 30s, re-checking every ~10s) returns the instant it clears and reports blocked only if it's *still* open at the deadline. The common case — nothing capturing — returns instantly without sleeping. This is the fix for the guard bouncing a caller over a capture that would have cleared on its own a moment later. (The bare single-shot form — no =--wait= — stays available for a caller that genuinely must not block.)
+
+- *Exit 0* → no live capture, or it cleared during the wait (or no reachable Emacs). Proceed with the edit.
+- *Exit 1* → an indirect org-capture buffer is *still* cloned from the roam inbox after the wait (the script prints the offending buffer name). Editing underneath it would leave the capture pointing at stale state and unable to finalize with =C-c C-c= (see =emacs.md=). Only now does the per-caller fallback fire:
+ - *On-demand / interactive run* → stop and surface: "You have a live org-capture session open against the roam inbox (=<buffer>=) — finalize it (=C-c C-c=) or abort it (=C-c C-k=) and I'll continue." Re-run the guard and resume once it returns clean.
+ - *Auto inbox zero (=/loop=) cycle* → don't surface or wait further; defer the roam reconcile to the next cycle, which is itself the retry at loop cadence. The items were already filed in Phase C, so the next cycle's Phase C status-check drops the duplicates and its Phase D removes them. Note one line: "roam reconcile deferred — a capture is still open; next cycle catches it."
+ - *Wrap-up sub-step* → don't block the wrap. Skip the roam reconcile for this run and surface one line: "Skipped roam-inbox reconcile — a live org-capture is open against it; claimed items stay and get caught next run." The items were already filed into =todo.org= in roam mode Phase C, so the next roam run's Phase C status-check drops the duplicates and its Phase D removes them — the skip self-heals.
+
+*The roam-write lock (around the Phase D edit).* Capture-guard protects against a live *human* capture; the roam-write lock protects against a concurrent *agent* writer (a sentry inbox pass, a KB promotion) editing =~/org/roam= at the same time. Acquire it after the capture-guard clears and release it after the edit-and-trigger, so the two guards nest — capture-guard underneath, the agent lock around the write:
+
+#+begin_src bash
+if [ -x .ai/scripts/agent-lock ]; then
+ .ai/scripts/agent-lock acquire roam-write --wait || { echo "roam-write busy; deferring roam reconcile" >&2; exit 1; }
+fi
+# capture-guard (above), then the Phase D read-modify-write of ~/org/roam/inbox.org,
+# then trigger the sync — roam-sync stays the only committer:
+systemctl --user start roam-sync.service
+[ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock release roam-write
+#+end_src
+
+Degrade gracefully when =agent-lock= isn't installed (an older checkout mid-sync): the write proceeds unlocked, today's behavior. A *present* helper reporting the lock busy after its bounded wait defers the roam reconcile (the auto-loop and wrap-up paths already defer-and-retry per the fallback list above); an *absent* helper never blocks it.
+
+* Core §6 — Priority-scheme check
+
+This gates filing whenever there are accept-and-file items. Check whether =todo.org= has a top-of-file priority scheme (an explicit legend defining =[#A]= through =[#D]= semantics and mandatory/optional tag conventions — a =* <Project> Priority Scheme= section or similar).
+
+- *Scheme present* — file new TODOs per the scheme. Every TODO gets a priority cookie matching the legend's rules, the mandatory type tag, and any applicable effort/autonomy tags.
+- *Scheme absent* — surface one sentence: "This project has no priority scheme. We should adopt one before filing the new TODOs from this inbox pass — want me to propose one based on the rulesets scheme?" If Craig says yes, do that first (the =/research-priority-scheme= research subagent pattern in rulesets is the reference). If Craig says no, file the TODOs without grading but flag in the commit message that they're un-prioritized pending a scheme.
+
+The point is to avoid adding ungraded =TODO= entries to a project that's never agreed on what =[#A]= means.
+
+* Mode: process
+
+Reads the project-local =inbox/= dir. Entry: a trigger phrase, or startup Phase C on a non-empty inbox. Exit: inbox empty (excluding =.gitkeep= and intentional =PROCESSED-*=), session log updated, =:LAST_INBOX_PROCESS:= stamped.
+
+** Phase A — Inventory (one parallel batch)
+
+Issue these reads in one parallel batch:
+
+1. List =inbox/= excluding =.gitkeep= and =PROCESSED-*= prefixes (use =\ls -la inbox/= per the protocols.org exa-alias note).
+2. Read =notes.org= *Project-Specific Context* if mission isn't already loaded in the session.
+3. Read =todo.org='s top-of-file priority scheme if present.
+
+For each inbox file, parse the filename for sender. Two common patterns:
+
+- =YYYY-MM-DD-HHMM-from-<sender>-<topic>.<ext>= — from another project via =inbox-send=.
+- =<topic>.org= — typically from Craig directly, or from a script.
+
+Note the file type. =.eml= files need the extract script (not raw =Read=):
+
+#+begin_src bash
+# View mode
+python3 .ai/scripts/eml-view-and-extract-attachments.py inbox/<file>.eml
+
+# Pipeline mode (extract attachments to a directory)
+python3 .ai/scripts/eml-view-and-extract-attachments.py inbox/<file>.eml --output-dir assets/<target>/
+#+end_src
+
+Everything else, read directly.
+
+** Phase B — Evaluate each item
+
+For each inbox file:
+
+1. *Read it.* Full read for substantive proposals (org files with TODO entries, design notes, multi-section docs); skim short FYIs and one-liner asks.
+2. *Identify the shape.* Instruction, question, proposal, FYI, or handoff — shapes guide disposition.
+3. *Apply the value gate* (core §1). One yes → candidate accept. Three nos → candidate reject.
+4. *Run the skeptical review* (core §2) on the item before classifying — the core pass on every accepted task and file, plus the cross-project battery when it proposes a shared-asset or convention change. Its summary + recommendation rides along to Phase C; in a no-approvals session it gates whether the item self-applies (quick + solo + agreed, per Monitor mode) or, for shared-asset and convention changes, defers and stages.
+5. *Within accept, classify* by the disposition ladder (core §3): implement now / fold into existing TODO / file as TODO.
+6. *Within reject, classify by source* (core §3): from Craig / from another project / from a script.
+
+** Phase B.1 — Priority-scheme check
+
+Run core §6. This gates Phase C filing when there are accept-and-file items.
+
+** Phase C — Surface dispositions
+
+Numbered options inline per =interaction.md= (no popup). Recommendation at item 1.
+
+Batch trivial items (one-line rejections of script noise, obvious file-as-TODO accepts where the scheme is already settled) into a single confirm-all prompt. Walk substantive items one at a time so the decision is visible.
+
+Per-item template:
+
+#+begin_example
+<filename> from <sender>: <one-line summary>
+Value-gate read: <yes/no on each of the three questions, one phrase each>
+Disposition recommendation: <implement / fold into <TODO> / file [#X] :tags: / reject>
+
+1. <recommendation as item 1>
+2. <alternative>
+3. Defer — leave in inbox under PROCESSED-<topic>.<ext> until <condition>
+4. Something else
+#+end_example
+
+For items that went through the skeptical review, the surfaced disposition includes its summary + recommendation, and approval here is what authorizes the apply. In a no-approvals session those items are reported as parked (the =[#B]= VERIFY) rather than surfaced for live approval.
+
+For pure FYIs that need no action, surface as a single line and recommend delete-with-acknowledgment.
+
+** Phase D — Apply
+
+Apply each disposition per the ladder (core §3). The flow is autonomous past Craig's Phase C approval.
+
+** Phase E — Close out
+
+Verify =inbox/= is empty (excluding =.gitkeep= and any intentional =PROCESSED-*= files). Run =\ls -la inbox/= and confirm.
+
+Update the session log per =protocols.org= with one short paragraph: count processed, count accepted (implement/fold/file split), count rejected (Craig/handoff/script split), and the commit SHA if a commit landed.
+
+Stamp =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section if it exists, so future workflows that gate on freshness can read it. Same format as =:LAST_AUDIT:= (=YYYY-MM-DD=).
+
+* Mode: monitor
+
+Process mode on a cadence. This is the *when, how-often, and act-vs-file* layer; the per-item disposition mechanics are the core sections, run via process mode — not restated here. Monitor decides *that* an item gets handled and *how I respond*; the core decides *what disposition* each item gets.
+
+The gap it closes: handoffs that arrive mid-session used to sit unseen until the user asked or the next startup ran. A handoff the sender can't see being handled trains them to escalate around the inbox channel.
+
+** Preconditions — before starting
+
+Never begin monitoring on a dirty worktree or a failing test suite. A dirty tree means the auto-commit at the end of an executed item sweeps up unrelated changes; a red suite means you can't tell whether the monitor broke something. At the start:
+
+1. =git status --porcelain --untracked-files=no= is empty (no tracked modifications). Untracked and gitignored files never block — an inbox drop is exactly what this mode processes, and a scratch file is none of its business (the template-freshness policy in =startup.org= Phase A.0). The tracked-only gate is safe because the per-item commit stages its files explicitly (=commits.md=: only intended changes staged) — never =git add -A=, which would sweep untracked files and is the failure this gate guards against.
+2. A full test run is all green (=make test= here, or the project's full-suite command).
+
+If *dirty*: offer to commit the pending changes in discrete, logical batches before starting. If *red*: offer to investigate the failures first. Surface the blocker with inline numbered options per =interaction.md= and wait — monitoring does not start until the tree is clean and the suite is green.
+
+** Cadence — how often to check
+
+*"Monitor the inbox" = run now, then loop every 15 minutes.* Do one process pass over any pending handoffs immediately, then start the loop:
+
+#+begin_src
+/loop 15m check the inbox with inbox-status and run inbox.org process mode over any pending handoffs
+#+end_src
+
+Each firing runs the cheap =inbox-status= check first and only does a full process pass when items are pending. The loop is the monitoring; it runs until Craig stops it or the session ends. Honor the Preconditions gate before the first pass and the Close-out gate when the loop stops.
+
+*Ambient task-boundary check (always on, even without a loop).* After finishing a unit of work, before reporting back or asking "what's next," run the cheap status check:
+
+#+begin_src bash
+.ai/scripts/inbox-status -q
+#+end_src
+
+Exit 1 means handoffs are pending — list them (drop =-q=) and run process mode. Exit 0 means clean; say nothing. This is one =find=; it costs nothing to run often, and it's the fix for handoffs piling up unseen during long sessions.
+
+*Startup and wrap-up already cover their ends.* Startup Phase C processes a non-empty inbox; the wrap-up sanity check refuses to wrap with unprocessed handoffs. The task-boundary cadence fills the middle.
+
+*Mid-task arrivals.* If a handoff lands while you're mid-task and it's urgent (blocks the current work, or is time-sensitive), surface it right away. Otherwise batch it to the next task boundary so the current work isn't thrashed.
+
+** The act-vs-file decision
+
+Every accepted handoff (one that clears the value gate) is then either acted on now or filed as a task.
+
+*Act immediately — and just do it, no asking — when all of these hold:*
+- *Clear* — the action is unambiguous; no design decision or option-choice is needed.
+- *Bounded* — small, finishable this session, ideally a tight file set.
+- *Low-risk and verifiable now* — not a risky change to load-bearing infra (or trivially revertible), and testable/lintable this session.
+- *In-scope and safe* — within this project, not destructive or outward-facing without confirmation, not across a project boundary.
+- *Cheaper than deferring* — doing it now costs less than filing plus re-triaging later.
+
+When you decide to act, queue the work and do it. Don't ask first.
+
+*Exception:* a proposal to change a shared asset (template workflow, rule, skill, synced script) or a substantive convention never qualifies for silent act-now, however clear and bounded it looks — it routes through the skeptical review (core §2), which carries its own approval (or, in a no-approvals session, park) step.
+
+*File a task when any of these hold:*
+- It needs a judgment call, a design decision, or an option the user would pick.
+- It's large, multi-session, or sprawls across many files.
+- It's blocked (a dependency, an external thing, the user is away).
+- It's risky enough to want the user's eyes before it lands.
+- It's off the session's active goal and acting now would derail it (file and keep going, unless it's urgent).
+
+When you decide to file, *ask first* — inline numbered options per =interaction.md=, with *filing as option 1 (the recommendation)* and *"do it now" as option 2*:
+
+#+begin_example
+<handoff> wants <X>. My read: file it (needs <reason>).
+
+1. File as a TODO ([#?] :tags:) — Recommended
+2. Do it now instead
+3. Something else
+
+Pick a number.
+#+end_example
+
+*Always ask if you're unsure* which side of the line an item falls on. Decisiveness on clear act-now items is the point of the rule; the ask is for genuine ambiguity and for filing.
+
+** Executing in no-approvals mode
+
+When Craig has put the session in no-approvals mode, an accepted item may be implemented automatically — but only when all three of these hold:
+
+1. *Agreed* — you've run the value gate and the full skeptical review and concluded the change should be done, not merely that it's harmless.
+2. *Quick* — the whole implementation, including verification, is under ~15 minutes.
+3. *Solo* — you can carry it end to end without a decision from Craig. Manual verification you perform yourself is fine; needing Craig to choose an option, approve a design, or resolve an ambiguity is not.
+
+All three → implement it, verify, then commit and push at the end of that item (the Step 0 reconcile and pre-push check from the =publish= skill still run). Miss any one and it doesn't self-apply: a shared-asset or convention change needs Craig's decision, so it fails *solo* and routes to the defer-and-stage park (core §2 / core §3); an oversized item fails *quick* and gets filed.
+
+** Replying to handoffs
+
+Close the loop per the reply-to-sender discipline (core §4): confirm what landed (accepted-and-acted), confirm where it's filed (accepted-and-filed), or state the why (rejected).
+
+** The inbox-status script
+
+=.ai/scripts/inbox-status= lists unprocessed handoffs and exits nonzero when any are pending. Exclusions match the wrap-up sanity check (=.gitkeep=, =lint-followups.org=, =PROCESSED-*=). Exit 0 = clean, 1 = pending, 2 = no inbox/ or bad usage. Use =-q= for the count-only form the cadence check calls.
+
+** Close out — before finishing
+
+End the way it started: clean worktree, green suite. Before stopping the loop or reporting the pass done:
+
+1. Commit or revert every tracked modification left in the worktree — no tracked change remains uncommitted. Untracked files (unprocessed inbox drops, scratch) are not the monitor's to sweep.
+2. Run the full test suite once more and confirm all green.
+
+If either can't be satisfied — a half-done item, a failure introduced during the pass — surface it rather than leaving it. The next monitor run assumes a clean, green starting state (the Preconditions gate).
+
+* Mode: roam
+
+Reads the *global roam inbox* (=~/org/roam/inbox.org=), Craig's cross-project GTD capture: one shared file every project can see. This mode routes each roam item to the project that owns it. The current session claims only the items belonging to THIS project, files them into the project's =todo.org=, and removes them from the shared inbox. Everything it doesn't own stays.
+
+*Allowed from any project, work included.* Tidying the shared roam inbox is housekeeping on a shared resource, not a cross-project boundary crossing and not a durable KB-node write, so the =knowledge-base.md= work-denylist doesn't gate it (a sentry inbox-zero pass mis-parked the whole inbox as a boundary crossing from the work project on 2026-07-19 — the error this note closes). Reading roam and tidying its inbox are fine from work; only promoting a durable =agents/= node stays work-denylisted.
+
+The aspiration is inbox zero: after this mode runs, the current project's local handoff inbox has been processed (Phase A delegates to process mode) and the shared roam inbox no longer contains items explicitly owned by this project.
+
+This is distinct from the wrap-up inbox/transcript routing feature (which moves session-filed keepers between projects). This routes the shared roam capture file by ownership prefix.
+
+** Scope: single-destination (v1)
+
+Routes each item to its one owning project, identified by an explicit =<project>:= heading prefix. The multi-project domain-aware mode (guess the owner of every unprefixed item and empty the whole inbox in one run) is deferred — see "Deferred: domain-aware routing" at the end. v1 claims only what's prefixed for the current project, surfaces the rest, and never guesses.
+
+** Callers
+
+The steps live here so three callers reuse them:
+- *Startup* (read-only nudge) — count the items, identify which appear related to this project, surface both numbers, offer processing as one of the startup options. Never auto-files.
+- *Wrap-up* (Step 3 sub-step) — sweep items that belong here before the cleanup scripts, so imported tasks lint and ride the wrap commit.
+- *On demand* — the roam-mode trigger phrases.
+
+Each project touches the roam inbox at least twice a session this way: once at startup, once at wrap-up.
+
+** The ownership rule (the coordination primitive)
+
+The inbox is shared, so the mode must never let two projects fight over an item or let one grab another's. Ownership is by explicit prefix:
+
+- =<project>: ...= heading → owned by that project. The current project claims only items prefixed with its own identifier.
+- Prefixed for *another* project → leave untouched (cross-project boundary, =protocols.org=).
+- *No prefix* → unowned. Never auto-claim. Surface as candidates a human can claim or prefix.
+
+The prefix partition is what makes concurrent triage across projects safe: each project only ever removes its own items, so two sessions editing the inbox touch disjoint lines.
+
+*Resolving this project's identifier (v1).* Use the project root basename plus its common aliases (=.emacs.d= ↔ =emacs=, and the obvious ones: =rulesets=, =work=, =home=). A project may override the inferred set with an =:INBOX_PREFIX:= line in =notes.org='s *Workflow State* section when the basename is fragile. The explicit override is optional in v1; the durable multi-project resolution is part of the deferred domain-aware mode.
+
+** Phase A — Process the project-local inbox
+
+1. Check the project-local =inbox/= with =.ai/scripts/inbox-status -q=.
+2. If pending handoffs exist, run *process mode* before touching the roam inbox. Project handoffs are already addressed to this project, so they are higher-confidence and cheaper to clear than shared roam captures.
+3. If =inbox-status= reports no =inbox/= directory, note it and continue to the roam inbox. Some projects only participate in the shared roam capture flow.
+4. If process mode cannot finish because it needs Craig's decision, stop after surfacing that decision. Do not remove roam items in the same run; the project still does not have a clean inbox.
+
+** Phase B — Identify, count, and match roam items
+
+1. Resolve the current project's identifier and aliases (above).
+2. Read =~/org/roam/inbox.org=. If absent, silent no-op (the file lives only on machines with the roam clone).
+3. Bucket every item under the inbox heading:
+ - *claimed* — prefixed for this project
+ - *foreign* — prefixed for another project → leave
+ - *unowned* — no project prefix
+ - *empty* — a heading with no title and no body: just stars, optionally a =TODO=/keyword, and whitespace (e.g. =** =, =** TODO =, =*** TODO =). These are aborted or accidental captures, owned by nobody, and safe to delete regardless of project. A heading with any title text or any body content is never empty.
+4. *Summarize the scan* (Craig's requirement, every scan): report the total item count in the inbox, then the count that appears related to this project. "Appears related" is the union of claimed items (exact prefix) and any unowned item whose topic plainly concerns this project's domain (a content judgment, surfaced as a candidate, never auto-claimed). Foreign-prefixed items are not "related" — they belong to their owner. Note the empty count separately.
+5. If claimed, related-unowned, *and* empty are all absent, report the total and stop (the common case for most wraps). Empty entries on their own are enough to enter Phase D — the cleanup runs even when this project owns nothing else, since empties belong to nobody and removing them is what "check the inbox" should always do.
+
+** Phase C — File each claimed roam item into todo.org
+
+Apply the core disposition discipline against the project's =todo.org=; don't reinvent it:
+
+1. *Status check first.* Already done, or already a task in =todo.org=? → drop it, or fold into the existing task (dated sub-entry per =todo-format.md=). Don't duplicate.
+2. *Rewrite* to terse-heading + body per =todo-format.md=.
+3. *Priority + tags from THIS project's scheme* (core §6) — the legend at the top of its =todo.org=, tags from that scheme's allowed set only. The project expresses someday-maybe with =[#D]=; there's no special someday-maybe routing.
+4. *File* under the project's Open Work section.
+
+** Phase D — Reconcile the shared roam inbox
+
+The roam inbox lives in a git repo (=~/org/roam=, auto-synced every 15 minutes by the =roam-sync= timer). Craig captures into it constantly, so its working tree is dirty most of the time — which is exactly why this mode never runs =git pull= itself. A pull on a dirty tree fails, and that would block triage on nearly every run. Instead, edit the file and hand the git work to =roam-sync=, which already commits-first-then-rebases and so handles the dirty tree correctly.
+
+1. *Guard against a live org-capture session* — run the capture-guard in poll mode (=capture-guard --wait=, core §5) before the edit, so a transient capture clears itself rather than bouncing the run. On a still-blocked exit 1 the caller-specific fallback (interactive stop-and-surface / auto-loop defer-to-next-cycle / wrap-up skip-and-self-heal) is in core §5.
+2. *Remove the claimed items and the empty entries* from the working-tree file. Never touch foreign or unowned (titled) items. Empty entries (Phase B's =empty= bucket) are removed on every triage regardless of who would own a titled version, since an aborted capture belongs to nobody. The claimed-item removal and the empty sweep happen in the same edit.
+3. *Hand the commit + push to =roam-sync=.* Don't =git pull=, =git commit=, or =git push= here. Trigger the sync so the edit lands promptly rather than waiting up to 15 minutes for the next timer tick:
+
+ #+begin_src bash
+ systemctl --user start roam-sync.service
+ #+end_src
+
+ =roam-sync= commits the edit (under its generic auto-sync message), rebases onto the remote, and pushes. The removal is safe to land without a pull-first because only this project ever touches =<project>:=-prefixed lines (the ownership partition), so =roam-sync='s rebase can't conflict on the edit. Provenance for the routed tasks lives in the project's =todo.org= and session log, not the roam commit message. If =systemctl= isn't available, leave the edit for the next timer tick — it still lands.
+
+ Don't pull or stash the roam tree to "clean" it first: that fights =roam-sync= for ownership of the repo's git state. The edit-then-sync handoff is the whole point.
+
+** Phase E — Surface
+
+Report: local project inbox disposition first (processed count and whether it is clear), then roam disposition: moved (with their new priorities and tags), folded, dropped-as-done, and empty entries swept (count). Then the residue: foreign items (left for their owners, count only) and unowned items (count plus the headings that appear related to this project, for manual claim or prefix). Same "summarize what we kept" shape.
+
+If triaging this batch surfaced a durable, cross-project fact (a reference pointer worth keeping, a pattern worth recording), consider writing it to the agent KB as one =:agent:= node (see =knowledge-base.md=; personal projects only). Skip silently when nothing durable came up — never pad an empty run with a KB line.
+
+** Skip conditions
+
+- No project-local =inbox/= and no =~/org/roam/inbox.org= → silent no-op.
+- Project-local =inbox/= exists but has no pending handoffs → continue to roam scan.
+- No =~/org/roam/inbox.org= after the local inbox check → report the local inbox disposition and stop.
+- No claimed, no related-unowned, and no empty roam entries → report the total, stop.
+- Live org-capture against the roam inbox (capture-guard exit 1) → surface (interactive) or skip-and-self-heal (wrap-up), per core §5.
+
+** Caller integration
+
+*Startup (read-only nudge).* Startup already checks the project-local =inbox/= via =inbox-status= and processes it through process mode when needed. It also reads =~/org/roam/inbox.org= and produces the roam scan summary; one line surfaces: "Roam inbox: N items total, M appear related to this project (K empty entries to sweep) — say 'inbox zero' to file them." Offered as one of the priority options. The empty count rides along so a clean-up-only run still gets offered. Startup never auto-files or auto-sweeps roam items; it counts and offers (the read-only nudge never edits, so empties are reported, not removed, until a real triage runs).
+
+*Wrap-up (Step 3 sub-step).* A sub-step at the start of wrap-up Step 3 (before the cleanup scripts, so imported tasks get linted and ride the wrap commit) delegates here for the claimed set. Skip-fast when nothing matches.
+
+** Deferred: domain-aware routing (future work, multi-project)
+
+v1 handles the single-destination case via the prefix rule. The multi-project parts are deferred until the need is real:
+
+1. *Domain-aware empty-it-all mode.* If rulesets held a description of each project's domain, one run could guess the owner of every item (prefixed or not) and empty the whole inbox at once, delivering each item to its owning project's =inbox/= via =inbox-send= (where that project's process-mode gate still decides whether to file it). Open: where the domain map lives, how confident a guess must be before auto-routing vs surfacing, and whether a low-confidence item stays put.
+2. *Explicit per-project =:INBOX_PREFIX:= as the durable resolver*, replacing basename inference.
+3. *Unowned-item lifecycle* once domain-aware routing exists (no item stays unrouted indefinitely).
+4. *Concurrent push contention* on the shared roam repo: triage now hands its commit + push to =roam-sync=, which already aborts-and-surfaces on a rebase conflict. If multi-machine contention ever makes that abort frequent, =roam-sync= may want a retry-once-after-rebase.
+
+Take these up when the single-destination version is in use and the multi-project pain is concrete.
+
+* Mode: auto inbox zero
+
+A recurring, *interactive* roam check. Trigger phrase: "auto inbox zero" (match before "inbox zero" — the longer phrase wins). On invocation, *ask Craig for the interval* (e.g. 30 min, 2 hours), then drive the loop with =/loop <interval>= running roam mode. It is in-session and interactive by design — each cycle reports what it found and filed.
+
+** Per cycle
+
+1. Run roam mode's scan (Phase A local check + Phase B roam scan), read-only — no =git pull=. The capture-guard still gates any write: use =capture-guard --wait= (core §5) so a transient capture clears itself; if it's still open after the wait, *defer this cycle's roam reconcile to the next cycle* rather than surfacing — the loop cadence is the retry, and the filed items get swept next time. The rare write hands its git to =roam-sync= (roam Phase D).
+2. *Nothing found* → no inbox summary. One heartbeat line: =inbox zero at HH:MM: nothing= (HH:MM local, from =date=) — the silent-until-signal policy, see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=. Nothing else. Keeping a quiet inbox quiet is the whole point.
+3. *Items found* → summarize the found items, file them as tasks (roam Phase C), and *append them to a displayed queue* — the harness task list, via =TaskCreate= — so the queue accumulates across cycles. Then ask: "run this batch next?"
+ - *Yes* → chain into =work-the-backlog.org= as an explicit second step after routing completes: pass it the eligibility query over the queued items (status =TODO= + =:solo:= per the scheme header, priority-ordered), =file-only= mode, paging off, cap 1. The highest-priority eligible candidate runs; the rest wait for the next tick or a later yes.
+ - *No* → they stay queued for a later go.
+ This mode never implements anything itself — routing ends here, and the execution loop lives in =work-the-backlog.org=, its one home.
+4. *Cross-cycle dedup.* Subsequent cycles add only *newly-found* items to the same displayed queue, never re-surfacing what's already there. Dedup against the queue (the =TaskCreate= list), not against what's already been implemented — a find that was queued-but-not-yet-run must not reappear, and one already filed into =todo.org= is dropped by roam Phase C's status check.
+
+A find is always surfaced and filed; execution happens only through the =work-the-backlog.org= chain and waits for Craig's yes. A quiet inbox produces only the =inbox zero at HH:MM: nothing= heartbeat. =auto inbox zero= is inherently in-session because its chain step waits for that yes.
+
+** Fully-unattended pass (=/schedule=) — vNext, not v1
+
+A fully-unattended cron pass (firing while Craig is away) is a *different contract* and is deferred. It can't wait for a yes, so it has to decide up front whether it may mutate =todo.org= and the roam inbox or stays read-only, how a find reaches Craig asynchronously, how dedup state survives across runs that don't share a session, and what session/auth context a cron run carries.
+
+The =/schedule= recipe, once that contract is designed, would look like:
+
+#+begin_src
+/schedule <cron-expression> run inbox.org roam mode read-only, and <surface-mechanism> any finds
+#+end_src
+
+v1 ships only the interactive =/loop= shape above; the unattended contract is logged to =todo.org= for its own design pass. Don't invent the unattended behavior here — route a request for it to that task.
+
+* Common Mistakes
+
+1. *Treating items as orders.* Inbox content is a proposal. The value gate is the rule. Implementing every item without evaluation inflates =todo.org= and trains senders to keep sending noise.
+2. *Filing without applying the value gate.* "File as TODO" is not a default — it's the disposition for proposals that pass the gate but wait. A reject is also a valid answer.
+3. *Filing raw TODOs when the project has a priority scheme.* Core §6 is mandatory when the scheme exists. An un-graded TODO in a project with a legend is a defect.
+4. *Silently deleting a project handoff.* Send a response naming which value-gate question failed. Silent rejection trains the sender to escalate to Craig instead of through the inbox channel.
+5. *Pushing back on a Craig directive only to immediately implement it anyway.* If you genuinely think Craig is wrong, say so and wait. If you don't, just do the work — don't theatre the pushback.
+6. *Skipping the implement-vs-fold-vs-file classification.* Defaulting every accept to "file as TODO" turns the inbox into a queue that flows into =todo.org= without filtering.
+7. *Not propagating value-gate failure to the response.* When you reject a handoff, name *which* gate question failed so the sender can recalibrate, not just resend.
+8. *Forgetting to delete the inbox file after acting.* The local inbox should be empty when process mode ends. Files left behind become noise on the next startup.
+9. *Applying a shared-asset change proposal without the skeptical review.* The value gate alone asks whether to take the change, never whether the change is right, complete, or as simple as it should be. (Worked example: the 2026-06-12 spec-decisions handoff was applied as-is and the after-the-fact review surfaced a lost state, a vacuous gate pass, and an enhancement — all catchable up front.)
+10. *Editing the roam inbox without the capture-guard.* A disk write under a live org-capture wedges the capture (core §5). Guard first, every roam write.
+11. *Auto inbox zero re-surfacing queued items.* The loop must dedup against the displayed queue, not just against what's been implemented — or every cycle re-lists the same un-run finds.
+
+* Living Document
+
+Refine the value gate's three questions if the project's mission sharpens. Tune the per-source rejection-response template if =inbox-send= response loops surface a pattern. Tune the monitor cadence if task-boundary checking proves too frequent or too sparse. Capture the auto-loop interval that worked once the pattern recurs.
+
+If a mode wants real depth — enough that it bloats the core — it can become an =inbox.<mode>.org= plugin under this engine's namespace (the pattern =triage-intake= uses) rather than swelling this file. The principle that inbox items are *ideas to evaluate* is the part that doesn't change.
diff --git a/claude-templates/.ai/workflows/journal-entry.org b/claude-templates/.ai/workflows/journal-entry.org
index 3f476a7..c70dfe8 100644
--- a/claude-templates/.ai/workflows/journal-entry.org
+++ b/claude-templates/.ai/workflows/journal-entry.org
@@ -1,5 +1,5 @@
#+TITLE: Journal Entry Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2025-11-07
* Overview
diff --git a/claude-templates/.ai/workflows/meeting-prep.org b/claude-templates/.ai/workflows/meeting-prep.org
index 162ae30..563328b 100644
--- a/claude-templates/.ai/workflows/meeting-prep.org
+++ b/claude-templates/.ai/workflows/meeting-prep.org
@@ -1,5 +1,5 @@
#+TITLE: Meeting-Prep Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-10
* Overview
diff --git a/claude-templates/.ai/workflows/meeting-prep.pre-wire.org b/claude-templates/.ai/workflows/meeting-prep.pre-wire.org
index 6a156c0..3e27c2a 100644
--- a/claude-templates/.ai/workflows/meeting-prep.pre-wire.org
+++ b/claude-templates/.ai/workflows/meeting-prep.pre-wire.org
@@ -1,5 +1,5 @@
#+TITLE: Meeting-Prep — Pre-Wire Method (supporting doc)
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-10
Supporting document for the [[file:meeting-prep.org][meeting-prep workflow]]'s Phase 3.5. The workflow carries the condensed, in-flow version of pre-wiring; this file is the full Manager Tools method, kept beside the workflow (same name + =.pre-wire= suffix) so it travels with the workflow. Source casts: "How to Prewire a Meeting" (2007) and "Peer Prewire" (2015).
diff --git a/claude-templates/.ai/workflows/monitor-inbox.org b/claude-templates/.ai/workflows/monitor-inbox.org
deleted file mode 100644
index 4912a2b..0000000
--- a/claude-templates/.ai/workflows/monitor-inbox.org
+++ /dev/null
@@ -1,122 +0,0 @@
-#+TITLE: Monitor Inbox Workflow
-#+AUTHOR: Craig Jennings & Claude
-#+DATE: 2026-05-31
-
-* Overview
-
-Keep the project's =inbox/= responsive: notice handoffs on a cadence, triage each one, decide whether to act now or file it, and reply to the sender. This workflow is the /when, how-often, and act-vs-file/ layer. The per-item disposition mechanics — the value gate, the implement/fold/file classification, the per-source rejection flow — live in [[file:process-inbox.org][process-inbox.org]] and are not duplicated here. Think of it as: monitor-inbox decides /that/ an item gets handled and /how I respond/; process-inbox decides /what disposition/ each item gets.
-
-The gap this closes: handoffs that arrive mid-session used to sit unseen until the user asked or the next startup ran. A handoff the sender can't see being handled trains them to escalate around the inbox channel.
-
-* Preconditions — before starting
-
-Never begin inbox monitoring on a dirty worktree or with a failing test suite. A dirty tree means the auto-commit at the end of an executed item sweeps up unrelated changes; a red suite means you can't tell whether the monitor broke something. At the start of the workflow, check both:
-
-1. =git status --porcelain= is empty (clean worktree).
-2. A full test run is all green (=make test= here, or the project's full-suite command).
-
-If the worktree is *dirty*: offer to commit the pending changes in discrete, logical batches before starting. If the suite is *red*: offer to investigate the failures first. Surface the blocker with inline numbered options per =interaction.md= and wait — monitoring does not start until the tree is clean and the suite is green.
-
-* When to Use This Workflow
-
-Trigger phrases:
-
-- "monitor the inbox" / "watch the inbox" — *the defined meaning:* run one process-inbox pass now, then loop process-inbox every 15 minutes (see Cadence). Not an opt-in extra — the phrase *is* the loop.
-- "respond to the handoffs" / "handle the handoffs" — a single pass now, no loop.
-
-Cadence auto-trigger (ambient, always on): check at every task boundary during a session, even when no loop is running — see Cadence below.
-
-* Cadence — how often to check
-
-*"Monitor the inbox" = run now, then loop every 15 minutes.* When Craig says monitor or watch the inbox, do one process-inbox pass over any pending handoffs immediately, then start the loop:
-
-#+begin_src
-/loop 15m check the inbox with inbox-status and run process-inbox.org over any pending handoffs
-#+end_src
-
-Each firing runs the cheap =inbox-status= check first and only does a full process-inbox pass when items are pending. The loop is the monitoring; it runs until Craig stops it or the session ends. Honor the Preconditions gate before the first pass and the Close-out gate when the loop stops.
-
-*Ambient task-boundary check (always on, even without a loop).* After finishing a unit of work, before reporting back or asking "what's next," run the cheap status check:
-
-#+begin_src bash
-.ai/scripts/inbox-status -q
-#+end_src
-
-Exit 1 means handoffs are pending — list them (drop =-q=) and process per process-inbox.org. Exit 0 means clean; say nothing. This is one =find=; it costs nothing to run often, and it's the fix for handoffs piling up unseen during long sessions.
-
-*Startup and wrap-up already cover their ends.* Startup Phase C processes a non-empty inbox; the wrap-up sanity check refuses to wrap with unprocessed handoffs. The task-boundary cadence fills the middle.
-
-*Mid-task arrivals.* If a handoff lands while you're mid-task and it's urgent (blocks the current work, or is time-sensitive), surface it right away. Otherwise batch it to the next task boundary so the current work isn't thrashed.
-
-* The act-vs-file decision
-
-Every accepted handoff (one that clears process-inbox's value gate) is then either acted on now or filed as a task. The rule, and how to surface it:
-
-*Act immediately — and just do it, no asking — when all of these hold:*
-- *Clear* — the action is unambiguous; no design decision or option-choice is needed.
-- *Bounded* — small, finishable this session, ideally a tight file set.
-- *Low-risk and verifiable now* — not a risky change to load-bearing infra (or trivially revertible), and testable/lintable this session.
-- *In-scope and safe* — within this project, not destructive or outward-facing without confirmation, not across a project boundary.
-- *Cheaper than deferring* — doing it now costs less than filing plus re-triaging later.
-
-When you decide to act, queue the work and do it. Don't ask first.
-
-*Exception:* a proposal to change a shared asset (template workflow, rule, skill, synced script) or a substantive convention never qualifies for silent act-now, however clear and bounded it looks — it routes through process-inbox's *Skeptical Review*, which carries its own approval (or, in a no-approvals session, park) step.
-
-*File a task when any of these hold:*
-- It needs a judgment call, a design decision, or an option the user would pick.
-- It's large, multi-session, or sprawls across many files.
-- It's blocked (a dependency, an external thing, the user is away).
-- It's risky enough to want the user's eyes before it lands.
-- It's off the session's active goal and acting now would derail it (file and keep going, unless it's urgent).
-
-When you decide to file, *ask first* — inline numbered options per =interaction.md=, with *filing as option 1 (the recommendation)* and *"do it now" as option 2*:
-
-#+begin_example
-<handoff> wants <X>. My read: file it (needs <reason>).
-
-1. File as a TODO ([#?] :tags:) — Recommended
-2. Do it now instead
-3. Something else
-
-Pick a number.
-#+end_example
-
-*Always ask if you're unsure* which side of the line an item falls on. Decisiveness on clear act-now items is the point of the rule; the ask is for genuine ambiguity and for filing.
-
-** Executing in no-approvals mode
-
-When Craig has put the session in no-approvals mode, an accepted item may be implemented automatically — but only when all three of these hold:
-
-1. *Agreed* — you've run the value gate and the full Skeptical Review and concluded the change should be done, not merely that it's harmless.
-2. *Quick* — the whole implementation, including verification, is under ~15 minutes.
-3. *Solo* — you can carry it end to end without a decision from Craig. Manual verification you perform yourself is fine; needing Craig to choose an option, approve a design, or resolve an ambiguity is not.
-
-All three → implement it, verify, then commit and push at the end of that item (the Step 0 reconcile and pre-push check from =commits.md= still run). Miss any one and it doesn't self-apply: a shared-asset or convention change needs Craig's decision, so it fails *solo* and routes to process-inbox's defer-and-stage park; an oversized item fails *quick* and gets filed.
-
-* Replying to handoffs
-
-A handoff came from another project's agent (or the user). Close the loop:
-
-- *Accepted and acted on* — send a confirmation to the sender via =inbox-send <sender> --text "..."=, naming what landed and the commit, so they're not left guessing (they can't see this project's git log). =inbox-send= excludes the current project as a target, so a self-sourced item is handled in-session, not sent.
-- *Accepted and filed* — a short confirmation that it's filed and where, so the sender knows it wasn't dropped.
-- *Rejected* — always state the why (which value-gate question failed), per process-inbox's per-source rejection flow.
-
-Cross-project boundary: never act on a file under another project's =.ai/= scope from here — route it back as a handoff (see =cross-project.md=).
-
-* The inbox-status script
-
-=.ai/scripts/inbox-status= lists unprocessed handoffs and exits nonzero when any are pending. Exclusions match the wrap-up sanity check (=.gitkeep=, =lint-followups.org=, =PROCESSED-*=). Exit 0 = clean, 1 = pending, 2 = no inbox/ or bad usage. Use =-q= for the count-only form the cadence check calls.
-
-* Close out — before finishing
-
-End the workflow the way it started: clean worktree, green suite. Before stopping the loop or reporting the pass done:
-
-1. Commit or revert everything left in the worktree — nothing uncommitted remains.
-2. Run the full test suite once more and confirm all green.
-
-If either can't be satisfied — a half-done item, a failure introduced during the pass — surface it rather than leaving it. The next monitor run assumes a clean, green starting state (the Preconditions gate), so handing it a dirty tree or a red suite breaks the next run before it begins.
-
-* Living Document
-
-Tune the cadence if task-boundary checking proves too frequent or too sparse in practice. Refine the act-vs-file criteria as edge cases recur. If the background-monitor loop becomes a common pattern, capture the interval that worked. The decision rule itself — act-now is silent, filing asks with file-as-option-1, ambiguity asks — is the stable core (set by Craig, 2026-05-30).
diff --git a/claude-templates/.ai/workflows/no-approvals.org b/claude-templates/.ai/workflows/no-approvals.org
index 1efce82..6b5c7fa 100644
--- a/claude-templates/.ai/workflows/no-approvals.org
+++ b/claude-templates/.ai/workflows/no-approvals.org
@@ -1,5 +1,5 @@
#+TITLE: No-Approvals Mode
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-28
* Overview
@@ -22,6 +22,8 @@ Craig activates the mode with any of:
- Queuing several tasks in =todo.org= followed by any phrase above
- Any equivalent phrasing that signals he doesn't want to be re-asked between items
+*Not this mode:* any phrase containing "speedrun" ("speedrun", "no approvals speedrun") routes to =work-the-backlog.org='s no-approvals speedrun preset — an autonomous batch over an explicit ordered task set, with a pre-flight Q&A, autonomous commits, always-push, and an end-of-set page. This mode is the general interaction-gate suspension for whatever work is already underway; the speedrun is the dedicated backlog-batch workflow.
+
Mode resets when:
- Craig says approvals are back on
@@ -33,7 +35,7 @@ Mode resets when:
The interaction gates that step the workflow back to Craig for an "OK to proceed?" check:
-- The commit-message gate in =commits.md= Step 2. Print the final commit message inline, then commit immediately. No "approve / request changes / open in editor" prompt.
+- The commit-message gate in the =publish= skill, Step 2. Print the final commit message inline, then commit immediately. No "approve / request changes / open in editor" prompt.
- The PR-description gate. Print the final body, then create the PR.
- The PR-review-reply gate. Print the final reply, then post.
- "Ready to start?" / "Plan looks like X, proceed?" gates before implementation work begins.
@@ -44,10 +46,10 @@ The interaction gates that step the workflow back to Craig for an "OK to proceed
The engineering-discipline gates protect quality, not Craig's interaction time. They remain in force:
-- =/review-code= against the staged diff before every commit. Critical and Important findings still block. Minor findings still surface. No "proceed anyway" override unless Craig has given it explicitly for this batch.
+- =/review-code= against the staged diff before every commit, dispatched as an isolated adversarial reviewer per the =publish= skill's Step 1 — no-approvals removes *interaction* gates, never the isolation. Critical and Important findings still block, and the re-review loop still runs to approval. Minor findings still surface. No "proceed anyway" override unless Craig has given it explicitly for this batch. If the review can't reach approval — three rounds, a recurring finding, or a =Needs Discussion= verdict — that is a genuine question: park the item per step 4 and move to the next one rather than committing past a standing finding.
- =/voice personal= on every publish artifact (commit messages, PR titles + bodies, PR review comments). The full pattern walk happens. The printed result just doesn't wait for approval.
- The full test suite + lint + compile before commit (per =verification.md=).
-- Fetch-and-reconcile in =commits.md= Step 0.
+- Fetch-and-reconcile in the =publish= skill, Step 0.
- Session Log updates per =protocols.org=. Every state-mutating turn writes to =.ai/session-context.org= before the closing message. The log is the crash-recovery anchor while Craig is away. Missing entries lose work.
- Subagent review-gate cadence (=subagents.md=). Review each subagent's output before the next dispatch.
- Destructive or irreversible operations per =CLAUDE.md='s "Executing actions with care": force-push, =rm -rf=, dropping a column, dropping a branch, package removal. These need explicit consent regardless of mode. No-approvals is for *interaction* gates, not destructive-action consent.
@@ -68,7 +70,7 @@ For each item:
- Do the work.
- Update the Session Log per the rules in =protocols.org=.
-- Before any commit: run =/review-code= against the staged diff. Surface Critical and Important findings inline; fix them and re-review until clean. Minor findings show but don't block.
+- Before any commit: dispatch the isolated adversarial reviewer per the =publish= skill's Step 1 — never review your own staged diff inline. Surface Critical and Important findings; fix them and send the updated diff back to the *same* reviewer until it approves. Minor findings show but don't block and never earn another round. If the review can't reach approval — three rounds, a finding that recurs after being reported fixed, or a =Needs Discussion= verdict — park the item per step 4 with the standing findings and move on; don't commit past a blocking finding.
- Draft the commit message. Run =/voice personal= (the skill, or walk the patterns inline if unavailable). Print the final message inline before committing so the log shows it.
- Commit and push.
- One-line status between items ("Task X done, on to Y.") so Craig knows what's happening when he checks back in.
diff --git a/claude-templates/.ai/workflows/open-tasks.org b/claude-templates/.ai/workflows/open-tasks.org
index fe782d6..205d95c 100644
--- a/claude-templates/.ai/workflows/open-tasks.org
+++ b/claude-templates/.ai/workflows/open-tasks.org
@@ -1,5 +1,5 @@
#+TITLE: Open Tasks Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-04-25
* Overview
@@ -23,15 +23,16 @@ Don't route "task review" / "review tasks" here — those trigger the hygiene ha
* Phase A: Data Gathering (both modes)
-** Phase A pre-step — archive any freshly-DONE tasks
+** Phase A pre-step — normalize freshly-closed tasks
-Before reading =todo.org=, run the cleanup script's archive-done sweep so completed level-2 subtrees move from =* $Project Open Work= to =* $Project Resolved=:
+Before reading =todo.org=, run two cleanup sweeps so the read reflects current state. First convert any done sub-tasks to dated entries, then archive completed level-2 subtrees from =* $Project Open Work= to =* $Project Resolved=:
#+begin_src bash
+emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks todo.org
emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done todo.org
#+end_src
-Costs a few hundred milliseconds. Without it, a task that completed earlier in the session sits as =** DONE= under Open Work until the next =clean-todo= or wrap-up pass, and Next Mode would surface it as a "what's next" candidate. The sweep makes Phase A's read of =todo.org= reflect current state.
+Costs a few hundred milliseconds. Without the archive sweep, a task that completed earlier in the session sits as =** DONE= under Open Work until the next =clean-todo= or wrap-up pass, and Next Mode would surface it as a "what's next" candidate. The convert sweep runs first so a completed parent's sub-tasks are already dated when it archives; it also keeps interactive level-3 closes from lingering as DONE keywords. Together they make Phase A's read of =todo.org= reflect current state.
Skip the sweep if the workflow is invoked in an explicit read-only or dry-run context. Default is to run it.
@@ -176,6 +177,10 @@ Next Mode answers two questions in one output: "what matters most right now?" (t
Apply the prioritization cascade in order. Stop at the first matching step. This is the importance/urgency answer.
+*Exclude blocked tasks.* A task tagged =:blocked:= has an unmet cross-project dependency (its body names the project and the work owed, per =todo-format.md=). It can't be worked until that other project delivers, so it is *never* the cascade recommendation — skip it at every cascade step below. Blocked tasks are surfaced on their own in Step 3 so the stalled dependency stays visible instead of silently dropping out of view.
+
+*Surface blocking tasks first.* The mirror of the above: a task tagged =:blocker:= is holding up work in *another* project (its body names which project and what's owed, per =todo-format.md=). Clearing it unblocks that project, so it carries borrowed urgency — surface it at the *top* of the cascade recommendation regardless of its own priority cookie, ahead of the normal In-Progress / deadline / priority order. When several =:blocker:= tasks exist, lead with the one blocking the most, or the longest. This is the "do the thing that unblocks someone else first" rule; a =:blocker:= task left at its own low priority is exactly how a cross-project dependency stalls.
+
**** 1. In-Progress Tasks
- Look for tasks marked =DOING= or partially complete.
- *If found:* Recommend that task (always finish what's started).
@@ -228,11 +233,22 @@ Within each row, pick a single task per the same-level tie-breakers above (block
The friction filter is the override path. When the cascade winner is partially blocked, hardware-dependent, or simply too large for the user's current state, one of the friction rows is what they pick instead.
+*** Step 3 — Blocked-on-other-projects surface
+
+Independently of the cascade and the friction filter, collect every open task tagged =:blocked:=. These are tasks this project can't advance until another project delivers; surfacing them keeps a cross-project dependency from rotting at low priority on the other side — the exact failure the tag exists to prevent (a blocked task whose blocker is a =[#D]= in another project sits forever otherwise).
+
+For each blocked task, read its body for the blocking project and what's owed, and present one line: the task, the blocking project, and what that project owes. Then offer — per blocked task — to nudge the blocker: an =inbox-send <project> --text= note naming what's needed and why it's blocking, so the dependency gets attention in the project that owns it. Don't send without the user's go.
+
+If no =:blocked:= tasks exist, omit this surface entirely (the common case).
+
*** Output Format
-Pair the cascade recommendation with the friction block beneath it. Recommendation-at-item-1 convention applies to the friction rows — quick+solo first, since it's the strongest low-friction pick.
+Pair the cascade recommendation with the friction block beneath it, and the blocked-on-other-projects surface (Step 3) beneath that when any blocked task exists. Recommendation-at-item-1 convention applies to the friction rows — quick+solo first, since it's the strongest low-friction pick.
#+begin_example
+Unblocks other projects (do these first):
+- ai-term wrap-teardown companion — :blocker:, unblocks rulesets (the three ai-term functions)
+
Cascade recommendation (importance/urgency):
- Fix org-noter reliability — [#A], Method 1, 8/18 complete, blocks daily reading/annotation
@@ -240,17 +256,25 @@ If you want lower friction instead:
1. Quick + solo: Bump linter config — [#C] :quick:solo:, ~15 min
2. Quick: Confirm new dirvish setup — [#B] :quick:, needs your eye
3. Solo: Refactor config-utilities — [#B] :solo:, bounded but multi-hour
+
+Blocked on other projects (can't advance until the blocker delivers):
+- Wrap-teardown feature — blocked by emacsd: ai-term companion functions — nudge?
#+end_example
+The =:blocker:= surface sits at the very top — clearing one of those is the highest-leverage thing on the list, since it frees work in another project. Omit it when no =:blocker:= task exists (the common case).
+
Include for each row:
- Task name / description.
- Priority + tag cluster.
- One-line reasoning. For the cascade row, name which cascade step matched. For friction rows, an effort hint when one is obvious.
- Progress indicator (for V2MOM-structured todos) on the cascade row only.
+- For a =:blocker:= row: the project it unblocks and what's owed (from the task body).
+- For a blocked row: the blocking project and what it owes (from the task body), plus the nudge offer.
**** Edge cases
- *Empty friction block.* If no =:quick:= or =:solo:= tagged tasks exist in the open set, omit the friction block entirely. Present only the cascade recommendation.
+- *No =:blocker:= tasks.* Omit the "Unblocks other projects" surface entirely (the common case) — show it only when a task carries the =:blocker:= tag.
- *Dedupe.* If the cascade recommendation IS the same task as one of the friction rows (e.g. it's =:quick:solo:= and also won the cascade), show it once at the top with both labels. Don't list it twice.
- *Decline behavior.* If the user declines the cascade recommendation, drop straight to the friction block as the natural next prompt. Do not fall through to lower-cascade-tier tasks; the friction filter IS the override.
diff --git a/claude-templates/.ai/workflows/page-me.org b/claude-templates/.ai/workflows/page-me.org
index 607ed51..7a3b792 100644
--- a/claude-templates/.ai/workflows/page-me.org
+++ b/claude-templates/.ai/workflows/page-me.org
@@ -1,21 +1,29 @@
#+TITLE: Page Me Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-01-31
#+UPDATED: 2026-02-27
* Overview
-This workflow enables Claude to set timers and alarms that reliably notify Craig, even if the terminal session ends or is accidentally closed. Notifications are distinctive (audible + visual with alarm icon) and persist until manually dismissed.
+This workflow enables Claude to set timers and alarms that reliably notify Craig, even if the terminal session ends or is accidentally closed. Notifications are distinctive (audible + visual with the blue info icon) and persist until manually dismissed.
-Uses the =notify= command (alarm type) for consistent notifications across all AI workflows.
+Uses the =notify= command (info type) for consistent notifications across all AI workflows. Info-level on purpose: the earlier alarm styling read as all-red urgency, and Craig's verdict was that a page "should be a persistent info notification" — noticeable, never crash-scary (2026-07-02).
* Trigger Phrase
Craig says *"page me"* (or variations like "page me in 10 minutes", "page me at 3pm").
-The word "page" is the trigger for this workflow. It means: set a timed notification.
+The word "page" is the trigger for this workflow. It means: set a timed notification on the *desktop* channel (=notify=).
-Previously called "set-alarm" -- renamed to "page-me" for a distinctive, short trigger phrase that won't collide with common words like "remind" or "alert."
+Two sibling triggers pick a different channel; the timed =at= machinery below is identical for all three, only the fired command changes:
+
+- *"page me"* — desktop =notify= (this workflow's default).
+- *"text me"* — a Signal push to Craig's phone via =agent-text= (the away channel).
+- *"text and page me"* — both, for when he might be either place.
+
+Scope the triggers to the reflexive "me": "page me" and "text me", not a bare "page" or "text" in prose. The full channel vocabulary lives in protocols.org "Reaching Craig".
+
+"page" was chosen (renamed from the old "set-alarm") for a distinctive, short trigger that won't collide with common words like "remind" or "alert".
* Problem We're Solving
@@ -63,8 +71,8 @@ Craig tells Claude when and why:
Claude schedules the alarm using the =at= daemon with =notify=:
#+begin_src bash
-echo "notify alarm 'Page' 'Time to call the dentist' --persist" | at 3:30pm
-echo "notify alarm 'Page' 'Meeting starts' --persist" | at now + 45 minutes
+echo "notify info 'Page' 'Time to call the dentist' --persist" | at 3:30pm
+echo "notify info 'Page' 'Meeting starts' --persist" | at now + 45 minutes
#+end_src
The =at= daemon:
@@ -89,30 +97,43 @@ Craig dismisses the notification and acts on it.
** Setting Alarms
-Use the =at= daemon to schedule a =notify alarm= command:
+Use the =at= daemon to schedule a =notify info= command:
#+begin_src bash
# Schedule for specific time
-echo "notify alarm 'Page' 'Meeting starts' --persist" | at 3:30pm
+echo "notify info 'Page' 'Meeting starts' --persist" | at 3:30pm
# Schedule for relative time
-echo "notify alarm 'Page' 'Check the build' --persist" | at now + 30 minutes
+echo "notify info 'Page' 'Check the build' --persist" | at now + 30 minutes
# Schedule for tomorrow
-echo "notify alarm 'Page' 'Call the dentist' --persist" | at 3:30pm tomorrow
+echo "notify info 'Page' 'Call the dentist' --persist" | at 3:30pm tomorrow
#+end_src
** Notification System
-Uses the =notify= command with the =alarm= type. The =notify= command provides 8 notification types with matching icons and sounds.
+Uses the =notify= command with the =info= type. The =notify= command provides 8 notification types with matching icons and sounds.
#+begin_src bash
-# Immediate alarm notification (for testing)
-notify alarm "Page" "Your message here" --persist
+# Immediate page notification (for testing)
+notify info "Page" "Your message here" --persist
#+end_src
The =--persist= flag keeps the notification on screen until manually dismissed. All page-me notifications should use =--persist= by default.
+** Texting Craig's phone (the "text me" channel)
+
+The timed =notify= alarm above is the desktop channel. When Craig says "text me" (or a run expects him away from the machine), use =agent-text= instead, a Signal push to his phone from any machine or agent runtime:
+
+#+begin_src bash
+agent-text "Build finished, ready for your eyes"
+
+# Timed phone message: same at-daemon pattern, different channel
+echo "agent-text 'Meeting starts in 5'" | at 3:25pm
+#+end_src
+
+Channel selection and the mechanics live in protocols.org "Reaching Craig". On "text and page me", fire both: the desktop notification persists for whenever he returns, the phone push reaches him now.
+
** Managing Alarms
#+begin_src bash
@@ -139,10 +160,10 @@ The alarm must fire. Use the =at= daemon which is designed for exactly this purp
Simple invocation - Claude runs one command. No complex setup required per alarm.
** Fail Audibly
-If the alarm fails to schedule, report the error clearly. Don't fail silently.
+If the page fails to schedule, report the error clearly. Don't fail silently.
** Testable
-The =notify alarm= command can be called directly to verify notifications work without waiting for a timer.
+The =notify info= command can be called directly to verify notifications work without waiting for a timer.
** Non-Alarming
Use normal urgency, not critical. The notification should be noticeable but not imply something has gone horribly wrong.
diff --git a/claude-templates/.ai/workflows/process-inbox.org b/claude-templates/.ai/workflows/process-inbox.org
deleted file mode 100644
index af406ee..0000000
--- a/claude-templates/.ai/workflows/process-inbox.org
+++ /dev/null
@@ -1,220 +0,0 @@
-#+TITLE: Process Inbox Workflow
-#+AUTHOR: Craig Jennings & Claude
-#+DATE: 2026-05-28
-
-* Overview
-
-Inbox items are *ideas to evaluate*, not orders to execute. They arrive from Craig (typed directives saved as files), from other projects (handoffs via =inbox-send=), and from scripts/automated systems. Each is a proposal. An item earns a place in =todo.org= or git history only when it passes the value gate: it advances an existing task, improves how the project works, or serves the project's stated mission.
-
-The workflow is the disposition discipline. Read each item, evaluate honestly, apply the decision, then notify the sender if it was a project handoff and you're rejecting. Silent rejection on a handoff is worse than no reply.
-
-* When to Use This Workflow
-
-User triggers:
-
-- "process inbox" / "process the inbox"
-- "handle the inbox"
-- "what's in inbox" / "what's in the inbox"
-- "let's clear the inbox" / "let's process the inbox items"
-
-Auto-invocation:
-
-- Startup =Phase C step 2= delegates here when the inbox is non-empty. Don't ask Craig — just run it.
-
-Do *not* invoke this for inbox items that are clearly out-of-scope for the project — those are cross-project routing problems, handled per the cross-project boundary rule in =protocols.org=.
-
-* The Value Gate
-
-Every inbox item passes through three questions. One *yes* is enough to accept.
-
-1. *Does it advance an existing TODO?* Look up by topic in =todo.org='s open work. If the item extends a filed task, fold it in. If it implements a filed task, do the work.
-2. *Does it improve how the project works?* Architecture cleanup, workflow refinement, tooling, rule hygiene, drift detection — anything that makes the project itself more effective.
-3. *Does it serve the project's stated mission?* Read =notes.org= *Project-Specific Context* if the mission isn't obvious from the working directory and current task. The item should advance that mission, not orbit it.
-
-Three *no*s means reject. The rejection isn't lazy — an idea that doesn't help any current task, doesn't improve the system, and doesn't serve the mission is genuine noise, and accepting it inflates =todo.org= without payoff.
-
-* The Skeptical Review (every arriving task and file)
-
-The value gate decides whether an item is worth taking. This review decides whether what it proposes is *right*, *complete*, and *as simple as it should be*. Run it on every task and file that arrives in the inbox — not only shared-asset change proposals. Pure FYIs and replies that ask for nothing skip it.
-
-Approach the file with curiosity and skepticism. Work through, in writing — the core pass on every item:
-
-1. Is the request actually right — does it do what it claims, and is the claim correct for this project?
-2. Is it complete, or does it leave a gap — an unhandled case, a missing step, an untested path?
-3. Should it be simpler?
-4. Can it be enhanced to be more effective than as proposed?
-5. Does it conflict with any existing instruction — workflows, skills, rules, protocols, CLAUDE.md?
-
-When the item proposes a change to shared assets — template workflows, rules, skills, scripts, anything synced to consuming projects — or to a substantive convention, add the cross-project battery. It arrived from one project's context; you're evaluating it for all of them:
-
-6. Does this make sense for *all* consuming projects, or just the sender's situation?
-7. How does it change a common activity Craig performs — better, worse, or differently than the sender assumed?
-8. Plus at least three more questions specific to this change — what breaks for artifacts already using the old shape, what tooling interacts with it, what's underspecified, what the sender's worked example doesn't exercise.
-
-Output: a short summary of the thinking and a recommendation (do it / do it with named changes / file / reject). For shared-asset and convention changes the recommendation is surfaced to Craig for approval before applying; for ordinary tasks and files it feeds the act-vs-file and no-approvals-execute decision (=monitor-inbox.org=).
-
-** In a no-approvals session: shared-asset changes defer and stage
-
-Shared-asset and convention changes still don't self-apply when Craig has put the session in no-approvals mode — they need his decision, so they fail the *solo* test in monitor-inbox's executing-in-no-approvals criteria. Ordinary tasks and files that pass the review and are quick + solo execute under that criteria instead; this defer-and-stage path is for the shared-asset and convention changes that don't qualify. Run the review, prepare the edits in =working/<task-slug>/= (a patch file or the worked-out diff), file a =[#B]= VERIFY carrying the decision package, and reply to the sender that it's parked. The sender's local stopgap (per =cross-project.md='s propagation process) means the delay costs nothing — the canonical update is about durability, not speed.
-
-Wording-only fixes — no consuming project acts differently — may proceed even then, logged in the session log.
-
-The VERIFY shape (top-level, =[#B]= so startup's A/B surfacing catches it; no =SCHEDULED= unless the proposal names a real deadline):
-
-#+begin_example
-** VERIFY [#B] Parked: <proposal topic> (from <sender>)
-What arrived: <one line — what the handoff proposes>.
-Recommendation: <accept as-is / accept with changes / reject> — <2-3 line
-skeptical-review summary: what's right, what to change, what was checked>.
-Prepared diff: [[file:working/<slug>/proposed.diff]] — apply is mechanical on
-your go.
-Say "approve the parked <topic>" (or adjust / reject) and it gets applied.
-#+end_example
-
-The full question-battery answers live in the session log and the =working/= dir, not the task body — the body carries the conclusion, with the trail one link away.
-
-* Phase A — Inventory (one parallel batch)
-
-Issue these reads in one parallel batch:
-
-1. List =inbox/= excluding =.gitkeep= and =PROCESSED-*= prefixes (use =\ls -la inbox/= per the protocols.org exa-alias note).
-2. Read =notes.org= *Project-Specific Context* if mission isn't already loaded in the session.
-3. Read =todo.org='s top-of-file priority scheme if present (look for a =* Priority and Tag Scheme= section or similar between the intro and the first =* <Project> Open Work= header).
-
-For each inbox file, parse the filename for sender. Two common patterns:
-
-- =YYYY-MM-DD-HHMM-from-<sender>-<topic>.<ext>= — from another project via =inbox-send=.
-- =<topic>.org= — typically from Craig directly, or from a script.
-
-Note the file type. =.eml= files need the extract script (not raw =Read=):
-
-#+begin_src bash
-# View mode
-python3 .ai/scripts/eml-view-and-extract-attachments.py inbox/<file>.eml
-
-# Pipeline mode (extract attachments to a directory)
-python3 .ai/scripts/eml-view-and-extract-attachments.py inbox/<file>.eml --output-dir assets/<target>/
-#+end_src
-
-Everything else, read directly.
-
-* Phase B — Evaluate each item
-
-For each inbox file:
-
-1. *Read it.* For substantive proposals (org files with TODO entries, design notes, multi-section docs), the full read is the right move. For short FYIs and one-liner asks, skim.
-2. *Identify the shape.* Is it an instruction, a question, a proposal, an FYI, or a handoff? Shapes guide disposition.
-3. *Apply the value gate.* Three questions above. One yes → candidate accept. Three nos → candidate reject.
-4. *Run the Skeptical Review* (section above) on the item before classifying — the core pass on every accepted task and file, plus the cross-project battery when it proposes a shared-asset or convention change. Its summary + recommendation rides along to Phase C; in a no-approvals session it gates whether the item self-applies (quick + solo + agreed, per =monitor-inbox.org=) or, for shared-asset and convention changes, defers and stages.
-5. *Within accept, classify:*
- - *Implement now* — small, scoped, clear, no design call required. The work is the disposition.
- - *Fold into existing TODO* — the item extends a task already filed; update the TODO body and link the inbox content if substantive.
- - *File as TODO* — substantive but waits, or needs design/triage before implementation.
-6. *Within reject, classify by source:*
- - *From Craig* — push back honestly in chat. State why you won't implement. Offer the conditions under which you would, if any. Wait for Craig to override or accept.
- - *From another project* — write a response file naming the rejection rationale and (optionally) the condition under which you'd reconsider. Deliver via =inbox-send <sender> --file <response>= per the cross-project handoff convention.
- - *From a script or automated system* — just delete; no notification needed.
-
-* Phase B.1 — Priority-scheme check
-
-This gates Phase C filing when there are accept-and-file items.
-
-Check whether =todo.org= has a top-of-file priority scheme (an explicit legend defining =[#A]= through =[#D]= semantics and mandatory/optional tag conventions).
-
-- *Scheme present* — file new TODOs per the scheme. Every TODO gets a priority cookie matching the legend's rules, the mandatory type tag, and any applicable effort/autonomy tags.
-- *Scheme absent* — surface one sentence: "This project has no priority scheme. We should adopt one before filing the new TODOs from this inbox pass — want me to propose one based on the rulesets scheme?" If Craig says yes, do that first (the =/research-priority-scheme= research subagent pattern in rulesets is the reference). If Craig says no, file the TODOs without grading but flag in the commit message that they're un-prioritized pending a scheme.
-
-The point is to avoid adding ungraded =TODO= entries to a project that's never agreed on what =[#A]= means.
-
-* Phase C — Surface dispositions
-
-Numbered options inline per =interaction.md= (no popup). Recommendation at item 1.
-
-Batch trivial items (one-line rejections of script noise, obvious file-as-TODO accepts where the scheme is already settled) into a single confirm-all prompt. Walk substantive items one at a time so the decision is visible.
-
-Per-item template:
-
-#+begin_example
-<filename> from <sender>: <one-line summary>
-Value-gate read: <yes/no on each of the three questions, one phrase each>
-Disposition recommendation: <implement / fold into <TODO> / file [#X] :tags: / reject>
-
-1. <recommendation as item 1>
-2. <alternative>
-3. Defer — leave in inbox under PROCESSED-<topic>.<ext> until <condition>
-4. Something else
-#+end_example
-
-For items that went through the Skeptical Review, the surfaced disposition includes its summary + recommendation, and approval here is what authorizes the apply. In a no-approvals session those items are reported as parked (the =[#B]= VERIFY) rather than surfaced for live approval.
-
-For pure FYIs that need no action, surface as a single line and recommend delete-with-acknowledgment.
-
-* Phase D — Apply
-
-Apply each disposition. The flow is autonomous past Craig's Phase C approval.
-
-** Implement-now
-
-Do the work. Commit per the project's commit flow. Delete the inbox file. The commit message references the inbox item by filename so the provenance lands in =git log=.
-
-** Fold into existing TODO
-
-Update the parent TODO's body with a dated reconciliation sub-entry per =todo-format.md= (=*** YYYY-MM-DD Day @ HH:MM:SS -ZZZZ <what landed>=). Move substantive content to =docs/design/<date>-<topic>.<ext>= if it's worth keeping; reference from the TODO body. Delete the inbox file.
-
-** File as TODO
-
-Add the TODO under =* <Project> Open Work= with priority + tags per Phase B.1. Body summarizes the proposal and links the inbox content if it's been moved to =docs/design/=. Delete the inbox file (or move it to =docs/design/= first if the content survives).
-
-** Reject from Craig
-
-State the rejection in chat clearly: what you won't implement, why, and the conditions (if any) under which you would. Wait for Craig's override or acknowledgment. The inbox file stays until Craig confirms — if he overrides, re-enter Phase D as accept; if he acknowledges the rejection, delete the file.
-
-** Reject from another project (handoff)
-
-Write the response file at =/tmp/inbox-response-<topic>.org=. Contents:
-
-- Heading naming the original handoff and date
-- One paragraph: the rejection rationale (which value-gate question failed and why)
-- One paragraph: the condition under which you'd reconsider, if such a condition exists. If the answer is "never, this misreads the project's mission," say so directly.
-
-Deliver via =inbox-send <sender> --file /tmp/inbox-response-<topic>.org=. The =inbox-send= script (per =cross-project.md=) handles the from-prefix, date stamp, and target inbox path.
-
-Delete the local inbox file after the response lands in the sender's inbox.
-
-** Reject from script or automated system
-
-Just delete. No notification.
-
-** Defer
-
-Rename in place to =inbox/PROCESSED-<original-filename>= and add a brief comment line at the top: =# Deferred YYYY-MM-DD: <condition>=. Don't accumulate deferred items indefinitely — sweep them on a future =process-inbox= run when the condition is met or the deferral has aged out.
-
-** Park (Skeptical Review in a no-approvals session)
-
-Move the proposal file into =working/<task-slug>/= alongside the prepared diff, file the =[#B]= VERIFY per the Skeptical Review section, reply to the sender that it's parked for Craig's review, and delete the inbox file. On Craig's approval the apply is mechanical: apply the prepared edits, run the normal verify-and-publish flow, rewrite the VERIFY to a dated log entry per =todo-format.md=, and send the sender the acceptance reply. On rejection, the reject-from-another-project flow above runs unchanged.
-
-* Phase E — Close out
-
-Verify =inbox/= is empty (excluding =.gitkeep= and any intentional =PROCESSED-*= files). Run =\ls -la inbox/= and confirm.
-
-Update the session log per =protocols.org= with one short paragraph summarizing this pass: count processed, count accepted (implement/fold/file split), count rejected (Craig/handoff/script split), and the commit SHA if a commit landed.
-
-Stamp =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section if it exists, so future workflows that gate on freshness can read it. Same format as =:LAST_AUDIT:= (=YYYY-MM-DD=).
-
-* Common Mistakes
-
-1. *Treating items as orders.* Inbox content is a proposal. The value gate is the rule. Implementing every item without evaluation inflates =todo.org= and trains senders to keep sending noise.
-2. *Filing without applying the value gate.* "File as TODO" is not a default — it's the disposition for proposals that pass the gate but wait. A reject is also a valid file-as-TODO answer to nothing.
-3. *Filing raw TODOs when the project has a priority scheme.* Phase B.1 is mandatory when the scheme exists. An un-graded TODO in a project with a legend is a defect.
-4. *Silently deleting a project handoff.* Send a response. The sender's next session sees the response in their inbox and learns the rejection rationale. Silent rejection trains the sender to escalate to Craig instead of through the inbox channel.
-5. *Pushing back on a Craig directive only to immediately implement it anyway.* If you genuinely think Craig is wrong, say so and wait for his call. If you don't, just do the work — don't theatre the pushback.
-6. *Skipping the implement-vs-fold-vs-file classification.* Defaulting every accept to "file as TODO" turns the inbox into a queue that flows into =todo.org= without filtering. Small, scoped, clear items get implemented now; substantive proposals get filed; extensions to existing work get folded.
-7. *Not propagating value-gate failure to the response.* When you reject a handoff, the response should name *which* gate question failed (advances no current task / doesn't improve the project / doesn't serve the mission) so the sender can recalibrate, not just resend.
-8. *Forgetting to delete the inbox file after acting.* The inbox should be empty when this workflow ends. Files left behind become noise on the next startup.
-9. *Applying a shared-asset change proposal without the Skeptical Review.* The value gate alone asks whether to take the change, never whether the change is right, complete, or as simple as it should be. A proposal that's clear and bounded can still carry a design gap — the review is where that surfaces, before the change syncs to every consuming project. (Worked example: the 2026-06-12 spec-decisions handoff was applied as-is and the after-the-fact review surfaced a lost state, a vacuous gate pass, and an enhancement — all catchable up front.)
-
-* Living Document
-
-Refine the value gate's three questions if the project's mission sharpens. Tune the per-source rejection-response template if =inbox-send= response loops surface a pattern. Add new auto-classification shortcuts if certain item shapes (e.g. routine FYIs from a script) become common.
-
-The workflow is shaped by use. The principle that inbox items are *ideas to evaluate* is the part that doesn't change.
diff --git a/claude-templates/.ai/workflows/process-meeting-transcript.org b/claude-templates/.ai/workflows/process-meeting-transcript.org
index 4dd340f..d0806ad 100644
--- a/claude-templates/.ai/workflows/process-meeting-transcript.org
+++ b/claude-templates/.ai/workflows/process-meeting-transcript.org
@@ -1,5 +1,5 @@
#+TITLE: Process Meeting Transcript Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-03
* Overview
@@ -10,16 +10,16 @@ This workflow defines the process for processing meeting recordings from start t
Trigger this workflow when:
- Craig says "process the transcript" or "process the recording" or similar
-- New recording files (.mkv) appear in ~/sync/recordings/ after meetings
+- New recording files (.mkv, .m4a, or .flac) appear in ~/sync/recordings/ after meetings
- Craig wants to process meeting recordings into labeled transcripts
* Prerequisites
-- Recording file(s) exist in ~/sync/recordings/ (*.mkv)
+- Recording file(s) exist in ~/sync/recordings/ (*.mkv, *.m4a, or *.flac)
- Calendar files available at ~/.emacs.d/data/*cal.org for meeting titles
- AssemblyAI transcription script at ~/.emacs.d/scripts/assemblyai-transcribe
- AssemblyAI API key stored in ~/.authinfo.gpg (machine api.assemblyai.com)
-- ffmpeg available for audio extraction
+- ffmpeg available for audio extraction (video .mkv only; .m4a and .flac skip extraction)
* The Workflow
@@ -43,13 +43,13 @@ Classification is per recording, not per session — a single run can carry this
Find and match recording files with calendar events. *Run sub-steps 1 and 3 (recording list + calendar dump) as a single parallel batch* — they're independent. Sub-step 2 (parse timestamps) and sub-step 4 (matching) work from those two outputs in-memory, so they're sequential after the batch.
-1. **List recordings:** Find all recording files in ~/sync/recordings/ (video .mkv or audio-only .m4a)
+1. **List recordings:** Find all recording files in ~/sync/recordings/ (video .mkv or audio-only .m4a / .flac)
#+begin_src bash
- ls -la ~/sync/recordings/*.mkv ~/sync/recordings/*.m4a 2>/dev/null
+ ls -la ~/sync/recordings/*.mkv ~/sync/recordings/*.m4a ~/sync/recordings/*.flac 2>/dev/null
#+end_src
- Audio-only recordings (.m4a) are used when no screen content is expected. These skip Step 3 (audio extraction) since they're already in a transcribable format.
+ Audio-only recordings (.m4a or lossless .flac) are used when no screen content is expected. These skip Step 3 (audio extraction) since they're already in a transcribable format. FLAC is the recorder's current audio format — its frames are self-contained, so an interrupted recording still decodes.
-2. **Extract timestamps:** Parse date/time from each filename (format: YYYY-MM-DD-HH-MM-SS.mkv or .m4a)
+2. **Extract timestamps:** Parse date/time from each filename (format: YYYY-MM-DD-HH-MM-SS.mkv, .m4a, or .flac)
3. **Match with calendar:** Check ~/.emacs.d/data/*cal.org for meetings at those times
#+begin_src bash
@@ -74,7 +74,7 @@ Per =cross-project.md=: transcribe and label here, then deliver the *labeled tra
** Step 3: Extract Audio (video recordings only)
-*Skip this step for .m4a files* — they are already audio and can go directly to transcription.
+*Skip this step for .m4a and .flac files* — they are already audio and can go directly to transcription.
For .mkv video recordings, extract audio for transcription:
@@ -96,11 +96,11 @@ Output: /tmp/FILENAME.m4a (temporary, deleted after transcription)
#+begin_src bash
# For .mkv files (audio was extracted to /tmp/):
~/.emacs.d/scripts/assemblyai-transcribe /tmp/FILENAME.m4a > ~/sync/recordings/FILENAME.txt
- # For .m4a files (transcribe directly):
+ # For .m4a or .flac files (transcribe directly — AssemblyAI accepts both natively):
~/.emacs.d/scripts/assemblyai-transcribe ~/sync/recordings/FILENAME.m4a > ~/sync/recordings/FILENAME.txt
#+end_src
-2. **Clean up:** Delete intermediate .m4a file after successful transcription (only for .mkv extractions — do NOT delete original .m4a recordings)
+2. **Clean up:** Delete intermediate .m4a file after successful transcription (only for .mkv extractions — do NOT delete original .m4a or .flac recordings)
#+begin_src bash
rm /tmp/FILENAME.m4a
#+end_src
@@ -181,13 +181,13 @@ Present the speaker identification table to Craig for confirmation:
** Step 9: Copy Recording to Meetings Folder
-1. Ensure engagement meetings folder exists and patterns are in .gitignore (~*/meetings/*.mkv~ and ~*/meetings/*.m4a~)
+1. Ensure engagement meetings folder exists and patterns are in .gitignore (~*/meetings/*.mkv~, ~*/meetings/*.m4a~, and ~*/meetings/*.flac~)
2. Copy the recording file with descriptive name:
#+begin_src bash
# Video recordings:
cp ~/sync/recordings/YYYY-MM-DD-HH-MM-SS.mkv {engagement}/meetings/YYYY-MM-DD_HH-MM-meeting-name.mkv
- # Audio-only recordings:
+ # Audio-only recordings (.m4a or .flac — copy with the original extension):
cp ~/sync/recordings/YYYY-MM-DD-HH-MM-SS.m4a {engagement}/meetings/YYYY-MM-DD_HH-MM-meeting-name.m4a
#+end_src
Example: ~deepsat/meetings/2026-02-03_11-02-standup-ipm-grooming.mkv~
diff --git a/claude-templates/.ai/workflows/read-calendar-events.org b/claude-templates/.ai/workflows/read-calendar-events.org
index be66bf4..5eac529 100644
--- a/claude-templates/.ai/workflows/read-calendar-events.org
+++ b/claude-templates/.ai/workflows/read-calendar-events.org
@@ -1,5 +1,5 @@
#+TITLE: Read Calendar Events Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-01
* Overview
diff --git a/claude-templates/.ai/workflows/readability-audit.org b/claude-templates/.ai/workflows/readability-audit.org
new file mode 100644
index 0000000..90ad366
--- /dev/null
+++ b/claude-templates/.ai/workflows/readability-audit.org
@@ -0,0 +1,242 @@
+#+TITLE: Readability Audit Workflow
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-28
+
+* Overview
+
+A pass over one file, a set of modules, or the whole tree that makes the code
+*readable to a future maintainer*. It checks four things and fixes the cheap
+ones in place: the file-top commentary, the inline comments, the names, and the
+physical organization of the code. Structural changes that need a real refactor
+(splitting a module, renaming a public symbol) are not done here — they are
+filed as =:refactor:= tasks so they get their own design and test pass.
+
+This is language-agnostic. Where a step names a language-specific tool or
+convention, it's stated as "the project's <X>, if it has one" — read the
+project's =CLAUDE.md= / =notes.org= and the language bundle to resolve the
+concrete tool.
+
+* Where it sits among the code-quality tools
+
+These tools are a pipeline, not duplicates. Knowing which to reach for:
+
+- *readability-audit* (this workflow) — prose and human-reader clarity:
+ comments, file headers, names, and physical organization. Judgment-driven
+ (does this comment lie? does this name reveal intent? can a newcomer place
+ this file in a minute?).
+- =/refactor= — structure on measurable metrics: complexity, duplication,
+ dead-code, the =simplification= lens (behavior-preserving logic/size
+ reduction), and =rename= (executes a codebase-wide symbol rename).
+- =/simplify= — behavior-preserving cleanup of the current diff, applied
+ directly.
+
+The link that keeps them from overlapping: when this audit finds a structural
+problem too big for a comment/name fix — a module to split, a *public* symbol to
+rename across call sites — it *files* a =:refactor:= task rather than doing it
+here. =/refactor= (rename, simplification) or =/start-work= then executes that
+filed task with a proper design and test plan. Readability finds and files;
+=/refactor= transforms.
+
+* Problem We're Solving
+
+Source files drift toward two opposite failure modes, and both hurt the next
+person to open the file:
+
+- *Documentation rot and noise.* Headers carry stale user-manual content
+ (quick-starts, full option matrices, setup walkthroughs) that belongs in user
+ docs; comments restate what the next line already says; comments go out of
+ date and start lying; placeholder =TODO=/=FIXME= stubs and conversational
+ asides accumulate. A blank summary or a missing file-top description leaves a
+ reader with no map.
+- *Structural fog.* Names that don't reveal intent force the reader to decode
+ them; related functions scatter; a public entry point sits far from the
+ private helpers it calls; a file grows to hold several unrelated
+ responsibilities.
+
+Left alone, opening a file costs more every month. The fix is a repeatable audit
+with a clear, checkable standard, run on demand or as files are touched.
+
+* Exit Criteria
+
+For the audited scope:
+
+1. *Every file has an accurate top section* that states what the file does and
+ how it fits the rest of the codebase — terse, no user-manual content, and
+ carrying the project's file-header convention where it has one.
+2. *Every surviving comment earns its place* — it explains a *why* the code
+ can't (a constraint, a workaround and its reason, an ordering dependency, a
+ warning), it is accurate against the current code, and it is terse. Obvious
+ "describe the next line" comments are gone.
+3. *Names reveal intent* — no cryptic abbreviations; the project's
+ public/private visibility convention is applied consistently.
+4. *Related code is co-located* — a public function's private helpers sit right
+ after it; the file reads top-to-bottom by descending abstraction; sections
+ group what belongs together.
+5. *Structural problems too big to fix in a comment pass are filed* as
+ =:refactor:= tasks, not left as a vague note and not half-done inline.
+6. *Nothing broke* — the build is clean and the test suite is green
+ (comment/name edits are behavior-preserving, so this should always hold; it
+ is the proof, not a hope). See "Graceful degradation" for projects without a
+ suite.
+
+* When to Use This Workflow
+
+- "Let's run the readability-audit workflow."
+- "Audit the comments and commentary in <file/area>."
+- "Clean up the structure/organization of <module>."
+- After landing a feature, on the files it touched, before moving on.
+- On a single file you just found hard to read.
+- As a tree-wide sweep: inventory all the source files, audit each, batch the
+ fixes.
+
+Do NOT use this to *perform* the structural refactors themselves (use
+=/refactor= or =/start-work= against a filed task) or to hunt for bugs /
+complexity / duplication (that is =/refactor=, not a readability pass).
+
+* Approach: How We Work Together
+
+** Phase 1 — Scope and inventory
+
+Pick the target: one file, a named module set, or the whole tree. For a sweep,
+list the source files (honor =.aiignore=) and decide coverage. Lean on the
+language's own doc linters as a first filter where they exist — many flag a
+missing or blank file summary and malformed headers; run the project's lint
+target first.
+
+** Phase 2 — Audit each file against the four dimensions
+
+Record findings as =file:line — issue — proposed fix=. The four dimensions:
+
+*** A. File-top commentary (the map)
+
+- Present, and *accurate* against what the file now does.
+- States purpose, the file's role/architecture, and key entry points —
+ *tersely*. A reader should learn what this is and how it connects in a few
+ lines.
+- Carries the project's file-header convention where it has one (a metadata
+ block, a module docstring, a standard header comment). If the project has no
+ header convention, skip this sub-check — don't invent one.
+- Does *not* carry user-manual content — quick-starts, full option matrices,
+ step-by-step setup. That belongs in user docs; move it, don't keep it in the
+ source header.
+- Mechanics are correct for the language: a filled summary line (not blank), the
+ expected section markers, the expected footer.
+
+*** B. Inline comments (why, not what)
+
+- Explains a *why* the code cannot: a workaround *and its reason*, an ordering
+ or load dependency, business-logic rationale, a real warning ("do not reorder
+ these — deadlock").
+- Is *accurate* — matches the current code. A wrong comment is worse than none;
+ fix or delete on sight.
+- Is *terse and useful*. Delete the obvious "describe the next line" comment
+ unless it names a non-obvious constraint. Replace a stale placeholder or a
+ rambling aside with the real one-line reason, or remove it.
+- Convert a comment that's only restating the code into a better *name* instead
+ (see C).
+
+*** C. Names (carry the what/how so comments don't have to)
+
+- Intention-revealing variable and function names; no cryptic single letters or
+ abbreviations outside tight local scopes.
+- The project's public/private convention is applied consistently and correctly:
+ a helper only called within the file is private; a user-facing or
+ intentionally-reusable symbol is public. (Resolve the concrete convention from
+ the language and the project — a naming prefix, an export list, an
+ access modifier.)
+- When a comment exists only to explain a name, rename instead.
+
+*** D. Organization (co-location and ordering)
+
+- Related functions sit together. A public function's private helpers come
+ *right after* it (stepdown / proximity / "reads like a newspaper").
+- The file reads top-to-bottom by descending abstraction.
+- Sections group what belongs together.
+- *Cohesion check:* if the file holds several unrelated responsibilities, or has
+ grown large enough that the top no longer describes one coherent thing, flag a
+ split into layered owners — but see Phase 4: that's a filed refactor, not an
+ inline fix.
+
+** Phase 3 — Apply the cheap, safe fixes inline
+
+Dimensions A, B, and C are *comment- and name-only* and *solo* (no design or
+preference call): apply them directly. After each file (or a batch), verify with
+the project's gates: parse/syntax check, a clean build (no new warnings), and a
+green test suite. Comment/name edits can't change behavior, so green is the proof
+the edit was clean, not a behavior check.
+
+For a tree-wide sweep, drive the uniform rewrites mechanically and verify the
+whole batch at once: a *mechanical applier with a boundary assertion* that
+replaces a well-defined header span is reliable and fast, then one suite run
+covers the batch. Keep the varied cases (header-line fixes, summary fixes that
+must preserve surrounding metadata, inline-comment surgery, generated-file
+headers) as careful per-file edits. (The boundary markers are language-specific;
+the principle — mechanical applier + assert + one suite run for uniform
+rewrites, per-file judgment for varied cases — is not.)
+
+** Phase 4 — File the structural refactors, don't do them here
+
+Dimension D's bigger findings — split a module, rename a *public* symbol across
+call sites, move a function to a different file — are real refactors with their
+own risk and test surface. Do *not* slip them into a readability pass. File each
+as a =:refactor:= task in =todo.org= with the specific finding, so it gets
+=/refactor= or =/start-work= with a proper design and test plan. This is the
+line between the cheap clarity win and the structural change; keeping it sharp is
+what lets the audit stay safe and fast.
+
+** Phase 5 — Verify and commit in logical batches
+
+Full suite green, build clean. Commit the doc/comment changes as =docs:= (or
+=refactor:= where a header/structure normalized) in cohesive batches — one
+commit per coherent slice (a set of condensed commentaries, the
+generated-file-header fixes, the obvious-comment prune), not one mega-commit and
+not one-per-file. Generated files are fixed *in their generator* and then
+regenerated, so the next regen stays compliant.
+
+* Graceful degradation
+
+The audit adapts to what the project provides:
+
+- *No file-header convention* → skip dimension A's metadata sub-check; still
+ check the summary/description for accuracy and terseness.
+- *No test suite* → the green-suite proof in Phases 3 and 5 is unavailable. Fall
+ back to the strongest gate the project has (compile/byte-compile, parse check,
+ linters) and *flag the weaker proof as a known limit* — a behavior-preserving
+ edit is lower-risk, but say plainly that there's no suite to confirm it.
+- *No doc linter* → do the Phase 1 first-filter by reading instead; the audit
+ still runs, just without the cheap pre-pass.
+
+* Principles to Follow
+
+- *Comments explain why; code explains what.* If a comment restates the code,
+ delete it or turn it into a better name.
+- *Accuracy beats completeness.* A wrong or stale comment is worse than no
+ comment. When in doubt, delete.
+- *Terse and useful.* Every comment and every header line earns its place. The
+ source header is not the user manual — move manuals to user docs.
+- *Readable means the next person, fast.* The test of the top-section and the
+ organization is whether a maintainer who has never seen the file can place it
+ and navigate it in under a minute.
+- *Keep the cheap pass cheap.* Comment/name fixes are solo and land inline.
+ Structural splits and public renames are not — they get filed, designed, and
+ tested separately.
+- *Preserve legal and attribution headers verbatim.* Vendored / GPL / copyright
+ notices are never condensed away by a readability pass.
+- *Manual validation is still Craig's.* Solo means no input is needed to *do*
+ the work; visual/behavior confirmation afterward is expected where relevant.
+
+* Living Document
+
+Update this with what real runs teach. Lessons worth keeping as the standard
+sharpens:
+
+- *Interpretation default for "fix blank summary":* when a rewrite shows only a
+ header + summary and omits a metadata block the file already has, keep the
+ existing metadata and replace only the header line and the summary. Its
+ absence from the rewrite means "leave it," not "delete it."
+- *Generated files:* fix the *generator*, then regenerate. Editing the generated
+ file directly is reverted on the next regen.
+- *Vendored files:* preserve the copyright/attribution; do not auto-condense a
+ licensed header.
+- *Mechanical applier + assert + one suite run* is the safe way to do a
+ many-file uniform rewrite; per-file judgment is for the varied cases.
diff --git a/claude-templates/.ai/workflows/rename-artifact.org b/claude-templates/.ai/workflows/rename-artifact.org
index 7b9f15b..a8d1246 100644
--- a/claude-templates/.ai/workflows/rename-artifact.org
+++ b/claude-templates/.ai/workflows/rename-artifact.org
@@ -1,5 +1,5 @@
#+TITLE: Rename an .ai Artifact
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-31
* Summary
diff --git a/claude-templates/.ai/workflows/send-email.org b/claude-templates/.ai/workflows/send-email.org
index 065f925..82d2286 100644
--- a/claude-templates/.ai/workflows/send-email.org
+++ b/claude-templates/.ai/workflows/send-email.org
@@ -1,5 +1,5 @@
#+TITLE: Email Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-01-26
* Overview
diff --git a/claude-templates/.ai/workflows/sentry.org b/claude-templates/.ai/workflows/sentry.org
new file mode 100644
index 0000000..b25fc14
--- /dev/null
+++ b/claude-templates/.ai/workflows/sentry.org
@@ -0,0 +1,227 @@
+#+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 cycle walks a fixed list of 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 and refactor finding, and (opt-in) solo-task implementation — 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-cycle 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.
+
+A second, *independent* marker gates the solo-task implementation pass (pass 12):
+
+: :SENTRY_MAY_IMPLEMENT: yes
+
+=:COMMIT_AUTONOMY:= lets sentry commit its hygiene sweeps to the branch; =:SENTRY_MAY_IMPLEMENT:= additionally lets it implement solo, decision-free backlog tasks on the branch. The split exists because the two carry different morning costs: hygiene is a two-minute merge, implemented code is a review session. A project can run hygiene-only sentry without the implement pass, and most should until sentry has quiet weeks behind it. Absent =:SENTRY_MAY_IMPLEMENT:=, pass 12 skips; sentry still runs every other pass. Requires =:COMMIT_AUTONOMY:= alongside it — implementing implies committing.
+
+* 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 cycle 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-cycle body being one sentry cycle (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 cycle acquires it at cycle start and releases it at cycle end, and refreshes it between passes (the heartbeat, so a live cycle's lock never ages past one pass). If =/loop= fires again while a previous cycle still holds it, the new cycle's acquire fails and the cycle skips with one digest line — no two cycles run at once. The bounded wait is short (a few seconds); a live cycle 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 cycle records it. A reclaim during a genuinely slow pass is possible, so it's never silent.
+
+* The Pass Runner — one contract per pass
+
+Each cycle, 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 cycle (every pass probe-skipped or no-op) are not written one by one — the cycle collapses to a single heartbeat at cycle-end (below), so an idle cycle 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 (cycle start, before the passes)
+
+After acquiring the lock, confirm the cycle 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 cycle 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 cycle — skip it with a digest line, the next cycle 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 cycle retries. Unsafe states and their scope:
+
+- *Unexpected dirty tree* (non-spine) → skip the whole cycle.
+- *Lost or un-acquirable single-runner lock* → skip the cycle (another cycle 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 cycle-end* (see below) → the commits stay on the branch, flagged in the digest for morning review; the cycle doesn't roll back.
+
+Skips are never silent and never partial. Inside a *working* cycle, a pass line means the pass fully ran and a skip line names why it didn't. An *all-quiet* cycle 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 cycle has surfaced that there was none.
+
+** Multi-day stall notification
+
+An unmerged prior =sentry/*= branch at cycle start (the morning review never happened) skips the cycle. After the *second consecutive* cycle 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 cycle: 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 cycle 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 cycle-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 cycle. 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 and refactor finding* — hunt for real bugs and worthwhile refactoring opportunities 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 cycle. Rotate the area across cycles 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-cycle work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few cycles find the standing defects; a quiet hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four cycles, 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 bug as a graded task in =todo.org= per the severity × frequency matrix (=todo-format.md=), and each refactoring opportunity as a =:refactor:= task, deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. *Find, never fix in this pass* — the finding files a task and stops. A fix happens only in the opt-in implementation pass below, and only after the finding is a filed task that pass then re-verifies from scratch (see the premise rule there). A freshly-found "bug" can be a misread — one was filed and retracted two cycles apart on 2026-07-23 — so the file-then-verify-then-fix pipeline is deliberate: the task is the checkpoint, not a same-breath fix. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles; refactor-finding added 2026-07-24.)
+
+12. *Solo-task implementation (opt-in — =:SENTRY_MAY_IMPLEMENT:=)* — work the backlog's solo, decision-free tasks on the branch. Probe: =.ai/notes.org= Workflow State carries =:SENTRY_MAY_IMPLEMENT: yes= *and* the project holds =:COMMIT_AUTONOMY:= (the implement pass commits). Absent the marker, skip — this pass is off by default, because it turns the morning from a two-minute merge into a code review, and that's the project owner's call. When on: invoke =work-the-backlog.org= under its unattended-loop contract (no pre-flight Q&A — there's no Craig overnight), eligibility =TODO= + =:solo:=, with the defer checklist deciding act-vs-file. The overnight-only tightening: only the *ready* bucket implements (clears every checklist item with zero open decisions); a task needing even one quick decision defers to a =VERIFY= rather than guessing, exactly as the loop caller already does. Commit each logical change to the sentry branch; *never push* — the morning review and merge is the gate, same as every other pass. The full quality bar holds (TDD, suite green before each commit, the isolated adversarial review per =publish= Step 1 with its re-review loop, =/voice=), and the review here runs the *premise check first*: reproduce the bug or confirm the problem is real before judging the diff. The review is the fact-checker that a filed claim never got, and it is what makes fixing-on-a-branch safe (Craig, 2026-07-24). A task that fails its premise check is not implemented — the finding was wrong, and that outcome is a digest line, not a commit. A task whose review never reaches approval — three rounds, a recurring finding, or a =Needs Discussion= verdict — is the same shape: no commit, and a digest line naming the standing findings, so the morning review sees what the reviewer would not pass rather than finding the task silently absent. (Added at Craig's direction 2026-07-24: overnight implement-on-branch, gated and never-pushed.)
+
+(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.)
+
+* Cycle-end — conditional suite, then the digest commit
+
+After the passes:
+
+1. *Conditional suite run.* If any pass this cycle 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 cycle'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 cycle into minutes all night. Cycles that only touched org/spine files skip this.
+
+2. *Heartbeat or digest, then commit.* Decide quiet vs working. A *quiet* cycle — 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* cycle — 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> cycle= for a working cycle, =chore(sentry): heartbeat — <date> <time>= for a quiet one, so even a quiet cycle 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 cycle 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* cycle appends its block to the =session-context.org= Session Log (the spine the cycle already writes), so it survives a crash, rides the session archive, and is on screen in the running session. One block per working cycle: the timestamp, then one line per pass (ran + what, or skipped + why), plus any lock reclaim notes. A *quiet* cycle (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-cycle artifact; it still carries one line per pass so a real skip inside a working cycle 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 cycles 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 cycles.
+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 cycle 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 cycle 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 cycle-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. The one sanctioned exception is pass 12's solo-task implementation, and only because it inherits work-the-backlog's full defer checklist (data-loss and irreversible actions defer, never execute) plus a premise-verifying review, and it commits to the branch rather than acting on anything live.
+7. *A silent skip* — inside a working cycle, 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 cycle 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 cycles. 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 finding/hygiene passes, one opt-in implementation pass, and a deferred KB pass. The pass list, the interval default, the =:SENTRY_MAY_IMPLEMENT:= default, and the queue-vs-execute line for each pass are the knobs most likely to move with dogfooding. The implement pass especially is new (2026-07-24) and unproven at scale — watch the corrections signal (work-the-backlog's metric for autonomous commits later reverted or hand-fixed) before widening it past the projects that opt in. 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/claude-templates/.ai/workflows/session-harvest.org b/claude-templates/.ai/workflows/session-harvest.org
index c48d689..54a7c09 100644
--- a/claude-templates/.ai/workflows/session-harvest.org
+++ b/claude-templates/.ai/workflows/session-harvest.org
@@ -1,5 +1,5 @@
#+TITLE: Session-Harvest Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-11
* Overview
diff --git a/claude-templates/.ai/workflows/spec-create.org b/claude-templates/.ai/workflows/spec-create.org
index f90c511..39758a0 100644
--- a/claude-templates/.ai/workflows/spec-create.org
+++ b/claude-templates/.ai/workflows/spec-create.org
@@ -1,5 +1,5 @@
#+TITLE: Spec-Create Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-09
* Overview
@@ -10,7 +10,7 @@ The guiding principle, drawn from how Google, Oxide, Amazon, Basecamp, and the A
It is the front of a trio:
- =spec-create.org= (this one) — author writes the spec.
-- =spec-review.org= — a reviewer gates the spec for implementation-readiness and writes =<spec-basename>-review.org=.
+- =spec-review.org= — a reviewer gates the spec for implementation-readiness and records findings in the spec's =* Review findings= section.
- =spec-response.org= — the author folds the review back in.
The spec this workflow produces has to *pass spec-review's gate* — that gate is the definition of done. So the structure below is built to answer the reviewer's questions up front. Keep it lightweight anyway: a short required spine plus a *readiness-dimensions menu* where each item is either answered or explicitly marked "N/A because…". The best spec is the shortest one that still lets an engineer build it, test it, and ship behavior that matches the user's mental model.
@@ -47,6 +47,8 @@ Capture, in this order:
** Phase 2 — Design, alternatives, decisions
1. *Design* — overview first, then detail. Write the reasoning as *prose, not bullet dumps* — prose exposes weak logic that bullets let you hide. Use bullets only for genuinely enumerable lists. When the thing has an interface, use the *two-altitude* split (Rust RFC): explain it once for a user/caller, once for an implementer.
+
+ *Non-trivial UI.* When the deliverable is a real UI (a panel, a multi-control surface, an interacting visual layout — not a single dialog, a CLI flag, or a one-off prompt), the design isn't settled on the page. Run the research → ~5 distinct working-prototype directions → iterate-one-to-final process in =claude-rules/ui-prototyping.md= before treating the UI design as done, and add a =Prototype iterations= subsection under the spec's status heading linking every iteration (final linked in the design section). A UI design decision moves to =DONE= only once it's been seen working in a prototype.
2. *Alternatives considered* — the load-bearing section authors skip and reviewers need most. For each option, force a why-not with the MADR grammar: "Good, because… / Bad, because… / Neutral, because…". Even one rejected option, with the reason, beats presenting one path as inevitable.
3. *Decisions* — capture each real choice as an org =TODO= task carrying an inline mini-ADR (Nygard's spine):
- The heading is =** TODO <Decision name>=. It flips to =DONE= when the decision-maker agrees with the call; until then it stays =TODO=.
@@ -59,7 +61,7 @@ Capture, in this order:
This is where the spec earns a "Ready" from review: an engineer must be able to build it in steps, know when it's done, and never have to invent product behavior mid-implementation.
-1. *Implementation phases* — decompose the work into phases each small enough to finish in one focused session and each leaving the tree in a working (not half-broken) state. =spec-review= lifts this section straight into =todo.org= tasks, so a spec that can't be phased fails the gate — the absence is itself a finding.
+1. *Implementation phases* — decompose the work into phases each small enough to finish in one focused session and each leaving the tree in a working (not half-broken) state. =spec-review= checks this section decomposes cleanly and =spec-response= lifts it into =todo.org= tasks, so a spec that can't be phased fails the gate — the absence is itself a finding.
2. *Acceptance criteria* — the observable conditions that mean the feature works, written as checkable items. The review's test-surface task mirrors these.
3. *Readiness dimensions* — walk this menu and, for each, either define the behavior or write "N/A because…". The escape hatch keeps a simple spec short; the prompt keeps a hidden decision from slipping into implementation:
- *Data model & ownership* — what's user-authored / generated / cached / remote; who owns each editable region; what persists vs refreshes.
@@ -82,8 +84,9 @@ This is where the spec earns a "Ready" from review: an engineer must be able to
** Phase 5 — Wire it up (conventions)
-- *Filename + location:* =docs/<problem-slug>-spec.org=. Org-mode. The slug names the *problem/feature*, not a date. Must end in =-spec.org=.
-- *Metadata header:* a small table at the top — Status, Owner, Reviewer(s), Date, Related (link to the task/ticket).
+- *Filename + location:* =docs/specs/YYYY-MM-DD-<problem-slug>-spec.org= — formal specs live in =docs/specs/=, never =docs/design/= (that's for notes, brainstorms, inventories; see =claude-rules/docs-lifecycle.md=). Org-mode. The slug names the *problem/feature*; no status suffixes ever — status lives in the file. Must end in =-spec.org=.
+- *Status heading (first element after the file header):* a top-level heading carrying the lifecycle keyword, stamped =DRAFT= at authoring — spec-create owns this flip. It holds an =:ID:= UUID (generate with =uuidgen=) and dated history lines, newest first. The keyword is authoritative; the Metadata =Status= field mirrors it in lowercase. Transitions are three lines in one file (keyword + history line + mirror): spec-review flips =READY=, spec-response flips =DOING= at decomposition, the final build task flips =IMPLEMENTED=. Terminal states always record a reason.
+- *Metadata header:* a small table at the top — Status (the lowercase mirror), Owner, Reviewer(s), Date, Related (link to the task/ticket).
- *Review-and-iteration-history stub:* add a =Review and iteration history= section at the bottom and seed it with the author's first entry. =spec-review= and =spec-response= append provenance entries here, so the heading shape is a contract: =YYYY-MM-DD Day @ HH:MM:SS -ZZZZ — Contributor — Role=, body fields What / Why / Artifacts.
- *Cross-link both ways:* the spec links its task; the task links the spec (replace the task's inline plan with a terse description + a =file:= link to the spec).
@@ -103,7 +106,14 @@ Then it's ready for =spec-review.org=. Snapshot-vs-living rule: keep the spec li
,#+TITLE: <Feature> — Spec
,#+AUTHOR: <author>
,#+DATE: <YYYY-MM-DD>
-,#+TODO: TODO | DONE SUPERSEDED CANCELLED
+,#+TODO: TODO | DONE
+,#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
+
+,* DRAFT <spec short name>
+:PROPERTIES:
+:ID: <uuid — generate with uuidgen>
+:END:
+- <YYYY-MM-DD Day @ HH:MM:SS -ZZZZ> — drafted.
,* Metadata
| Status | draft |
diff --git a/claude-templates/.ai/workflows/spec-response.org b/claude-templates/.ai/workflows/spec-response.org
index 2686cf8..7628e49 100644
--- a/claude-templates/.ai/workflows/spec-response.org
+++ b/claude-templates/.ai/workflows/spec-response.org
@@ -5,9 +5,9 @@
* Overview
-The spec-response workflow processes external reviews of a design spec and folds them into the spec until it is implementation-ready. A reviewer (human or another agent) leaves a review file next to the spec — typically produced by its counterpart, the *spec-review* workflow, which writes =<spec-basename>-review.org= and assigns a readiness rubric. Claude works through every recommendation, deciding accept / modify / reject for each, updates the spec for the accepted ones, documents the modified and rejected ones with reasons, then deletes the review file. Repeat for each spec under review.
+The spec-response workflow processes a review's findings and folds them into the spec until it is implementation-ready. A reviewer (human or another agent) records findings in the spec's =* Review findings= section — typically via its counterpart, the *spec-review* workflow, which writes one =TODO= task per finding (=[/]= cookie on the heading) and assigns a readiness rubric. Claude works through every finding, deciding accept / modify / reject, updates the spec body for the accepted ones, and completes each finding task in place: accept and modify finish =DONE= (the modify's change noted in the body), reject finishes =CANCELLED= with the reason. Repeat for each spec under review.
-The output is a spec a reader could implement from, plus a durable record — inside the spec — of why any recommendation was changed or declined, so the reviewer can find the reasoning later without re-litigating.
+The output is a spec a reader could implement from, plus a durable record — inside the spec — of why any finding was changed or declined, so the reviewer can find the reasoning later without re-litigating. The reasoning lives on the completed finding task, not a separate file.
This workflow was first run on 2026-05-23 against the linear-emacs =issue-query-spec.org= and =issue-representation-spec.org= reviews, and written up from that run.
@@ -27,25 +27,25 @@ A review is only useful if every point in it gets a decision. Without a defined
A spec's review is fully processed when:
-1. *Every recommendation has an explicit disposition* — accepted, modified, or rejected. None dropped.
-2. *Accepted recommendations are woven into the spec body* — the spec reads as if they were always there, not appended as a changelog.
-3. *Modified and rejected recommendations are documented in a bottom section* (e.g. "Review dispositions") with a one-paragraph reason each, so the reviewer can find the reasoning.
+1. *Every finding has an explicit disposition* — accepted, modified, or rejected, and its task completed (=DONE= or =CANCELLED=). None dropped.
+2. *Accepted findings are woven into the spec body* — the spec reads as if they were always there, not appended as a changelog.
+3. *Modified and rejected findings carry a one-paragraph reason in the completed task's body*, so the reviewer can find the reasoning.
4. *Review/response provenance is documented in the spec* — iteration count/date, contributor, role, what changed, and why.
5. *Pre-agreed decisions are flipped to =DONE=* — each settled decision's =TODO= becomes =DONE=, and the =[/]= cookie on the spec's =* Decisions= heading reflects the tally.
6. *Cross-spec tensions are reconciled in writing* when related specs were reviewed together.
-7. *The review file is deleted* once 1-6 hold.
+7. *Every finding task is completed* — the =* Review findings= =[/]= cookie reads complete (each finding =DONE= or =CANCELLED=).
8. *Tracking is updated* — the spec's VERIFY/task body notes "review incorporated" and whether it's implementation-ready.
9. *Implementation tasks exist* — once the author confirms the spec is Ready, the project's =todo.org= carries the full implementation-task breakdown (Phase 6), reviewed for completeness, with =:solo:= marked and a Manual-testing task for everything else.
-The whole run is done when no =*-review.org= files remain and each spec is judged implementation-ready (or its remaining blockers are named).
+The whole run is done when every spec's =* Review findings= cookie reads complete and each spec is judged implementation-ready (or its remaining blockers are named).
-*Measurable validation:* a reader scanning the review against the revised spec can find, for every review point, either the change in the body or its disposition at the bottom. Nothing is unaccounted for.
+*Measurable validation:* a reader scanning the spec can find, for every finding, either the change in the body or its disposition on the completed finding task. Nothing is unaccounted for.
* When to Use This Workflow
Trigger when:
-- A reviewer drops a review file alongside a spec — convention: same basename with a =-review.org= suffix (=foo.org= → =foo-review.org=).
+- A reviewer records findings in the spec's =* Review findings= section — typically via the spec-review workflow.
- Craig says "respond to the review" / "let's run the spec-response workflow" / "process the spec reviews."
- Any time a spec needs to absorb structured external feedback and converge to implementation-ready.
@@ -63,7 +63,7 @@ the file should be renamed first. Spec workflows require the -spec.org suffix as
guard against pointing the workflow at tutorial, inventory, or setup docs.
#+end_example
-The review file the response consumes follows the convention =<spec-basename>-review.org=, so a misnamed spec produces a mis-pointed review file too. Fix the spec name first.
+The findings the response consumes live in the spec's own =* Review findings= section, so the spec filename is the handle the workflow keys on — point it at the right =-spec.org= file. Fix the spec name first.
The user resolves the mismatch and re-invokes the workflow. Do not proceed with the response against a misnamed spec.
@@ -71,7 +71,7 @@ The user resolves the mismatch and re-invokes the workflow. Do not proceed with
** Phase 0: Orient
-1. List the review files (=ls docs/*-review.org= or wherever they live). Process them one at a time in whatever order; the user may name an order.
+1. Find specs with open findings — a =* Review findings= section whose =[/]= cookie isn't complete (=TODO= findings remain). Process them one at a time in whatever order; the user may name an order.
2. Re-read the *current* spec, not your memory of it — it may have changed since you wrote it (the user or a linter may have edited it, and pre-agreed decisions may already be encoded in the tracking file).
3. Note any *pre-agreed decisions* the reviewer or user has already settled — in the review's own "Agreed decisions" section, or in the spec's tracking task. These are settled inputs. Don't reopen them; bake them in.
@@ -79,15 +79,15 @@ The user resolves the mismatch and re-invokes the workflow. Do not proceed with
Read the entire review first. Recommendations interact — an early "medium" finding may be subsumed by a "high" one, or two findings may point at the same edit. Decide dispositions with the whole picture in view, not finding-by-finding as you scroll.
-** Phase 2: Decide a disposition for every recommendation
+** Phase 2: Decide a disposition for every finding
-For each recommendation, choose one:
+For each finding, choose one:
-- *Accept* — the recommendation is right as written. Plan the edit.
-- *Modify* — the recommendation is right in spirit but wrong in detail or scope. Adjust it, and record what you changed and why.
-- *Reject* — the recommendation doesn't fit. Record why.
+- *Accept* — the finding is right as written. Plan the edit.
+- *Modify* — the finding is right in spirit but wrong in detail or scope. Adjust it, and record what you changed and why.
+- *Reject* — the finding doesn't fit. Record why.
-*Engage critically.* Rubber-stamping is a failure mode. On a strong review most points are accepts, but actively look for the genuine modify/reject cases — they are where your judgment earns its place. Examples from the first run:
+*Engage critically.* Rubber-stamping is a failure mode. On a strong review most findings are accepts, but actively look for the genuine modify/reject cases — they are where your judgment earns its place. Examples from the first run:
- *Modify:* the review proposed an automatic cache-TTL defcustom. Accepted the goal (fresh data) but deferred TTL to vNext because for a single-user tool an explicit force-refresh + clear-cache command covers it without invalidation complexity.
- *Reject:* the review floated a separate =default-issue-filter= defcustom. Rejected as redundant — a fixed default command plus a default-view preference already covered it.
@@ -101,8 +101,8 @@ When related specs were reviewed together, two reviews can recommend opposite th
** Phase 4: Update the spec
-1. *Weave accepted recommendations into the body.* The spec should read naturally — a new "Selector semantics" section, a revised phase plan, an added test-strategy section — not a list of "review said X so I did Y." The body reflects the decisions; it doesn't narrate them.
-2. *Add a bottom "Review dispositions" section* listing only the *modified* and *rejected* recommendations, each with a short reason. Close it with a one-line "everything else accepted as written" so the reader knows the omissions from this section are accepts, not gaps.
+1. *Weave accepted findings into the body.* The spec should read naturally — a new "Selector semantics" section, a revised phase plan, an added test-strategy section — not a list of "review said X so I did Y." The body reflects the decisions; it doesn't narrate them.
+2. *Complete each finding task in place.* Accept → =DONE=, body noting where it was folded; modify → =DONE=, body noting what you changed and why; reject → =CANCELLED=, body giving the reason. The =[/]= cookie on =* Review findings= tracks progress. The reason on a modified or rejected finding is the durable record — accepted findings are recorded by the body change itself, so they need no separate note. The asymmetry is deliberate.
3. *Update or add a bottom "Review and iteration history" section.* Every response pass gets an entry, even when all findings are accepted. Each entry is an org subheading with a compound id followed by three body fields:
Heading format: =YYYY-MM-DD Day @ HH:MM:SS -ZZZZ — Contributor — Role=
@@ -113,26 +113,28 @@ When related specs were reviewed together, two reviews can recommend opposite th
- *What changed:* compact summary of accepted, modified, and rejected work.
- *Why:* the rationale or decision pressure behind the changes.
- - *Artifacts:* review filename, disposition section, task IDs, source checks, or commits when useful.
+ - *Artifacts:* the relevant findings, task IDs, source checks, or commits when useful.
4. *Flip settled decisions to =DONE=.* Each decision the decision-maker has agreed flips its =TODO= to =DONE=; the =[/]= cookie on the =* Decisions= heading tracks the tally. A contested decision stays =TODO= with the back-and-forth under its =*** Discussion= child header. Decisions still =TODO= should be only what genuinely still blocks, each with an owner and a by-when.
-5. *Raise the spec to implementation-ready:* consolidate decisions up front, add any implementation prerequisites the review surfaced (e.g. a schema-verification checklist), a consolidated test strategy, and a phased plan ordered so dependencies (like an output model everything depends on) come early. *Gate:* the spec Status cannot move past =draft= to implementation-ready while any decision is still =TODO= — the =[/]= cookie must read complete, or the author consciously accepts and records the risk of building with one open.
+5. *Raise the spec to implementation-ready:* consolidate decisions up front, add any implementation prerequisites the review surfaced (e.g. a schema-verification checklist), a consolidated test strategy, and a phased plan ordered so dependencies (like an output model everything depends on) come early. *Gate:* the spec Status cannot move past =draft= to implementation-ready while any decision or any =:blocking:= finding is still =TODO= — both =[/]= cookies must read complete, or the author consciously accepts and records the risk of building with one open. *If this response expanded scope* — folding a finding in added new phases, decisions, or external-dependency assumptions — re-run spec-review's readiness rubric against the *expanded* spec, and file any new gap as a finding or decision before claiming =Ready=. Disposition-completeness gates the *review*; the readiness rubric gates the *spec*. A response can resolve every finding and still be less ready than before, because the answers introduced unproven obligations — the cookies only protect you if the new obligation is actually filed.
6. *Update the status line* to note "review incorporated (<reviewer>, <date>)."
** Phase 5: Close out and iterate
-1. *Delete the review file* — only after every recommendation has a disposition. Its deletion is the signal the review is fully processed.
-2. *Update tracking* — the spec's VERIFY/task body gets a line noting review incorporated, what changed at a high level, which recommendations were modified (pointing at Review dispositions), and whether it's now implementation-ready pending final go.
+1. *Confirm every finding is completed* — the =* Review findings= =[/]= cookie reads complete (every finding =DONE= or =CANCELLED=). The complete cookie is the signal the review is fully processed; there is no file to delete.
+2. *Update tracking* — the spec's VERIFY/task body gets a line noting review incorporated, what changed at a high level, which findings were modified or rejected (pointing at the completed findings), and whether it's now implementation-ready pending final go.
3. *Update the session log* (state changed this turn).
-4. *Move to the next review file.* Repeat Phases 1-5 until none remain.
+4. *Move to the next spec with open findings.* Repeat Phases 1-5 until none remain.
5. *Report* what was accepted-wholesale, what was modified/rejected and why, any cross-spec reconciliations, and the implementation-ready verdict per spec.
** Phase 6: On Ready, build the implementation-task breakdown
This is the *last* step of the workflow, and it runs *only after the author confirms the spec is Ready* — never during review iterations. A Ready spec nobody can act on is unfinished; this phase turns it into tracked work. It applies to every project type (library, application, service, docs set).
-1. *Decide where the tasks live.* If the work is spinning off into its own project/repo, move the parent task into that project's =todo.org= (and relocate the spec with it); otherwise use the current project's =todo.org=. One parent task owns the effort; the phase tasks hang under it.
+*This phase owns the =READY= → =DOING= lifecycle flip* (docs-lifecycle convention): when the decomposition below lands, update the spec's top-level status heading keyword to =DOING=, add a dated history line, and set the Metadata =Status= mirror to =doing= — three lines, one file.
-2. *Create one task per implementation phase* from the spec's =Implementation phases=, in dependency order, so the task set as a whole describes the *full* milestone (e.g. v1) with no gaps. Each task body names the deliverable, its tests, and how it is verified. Carry over deferred/vNext work and any publish/release steps as their own tasks.
+1. *Decide where the tasks live.* If the work is spinning off into its own project/repo, move the parent task into that project's =todo.org= (and relocate the spec with it); otherwise use the current project's =todo.org=. One parent task owns the effort; the phase tasks hang under it. *Stamp the binding:* the parent task's =:PROPERTIES:= drawer gets a =:SPEC_ID:= line holding the spec's status-heading UUID. That property is the durable join task-audit uses to police =DOING= specs (a =DOING= spec whose bound parent is closed, archived, or missing gets flagged).
+
+2. *Create one task per implementation phase* from the spec's =Implementation phases=, in dependency order, so the task set as a whole describes the *full* milestone (e.g. v1) with no gaps. Each task body names the deliverable, its tests, and how it is verified. Carry over deferred/vNext work and any publish/release steps as their own tasks. *Always end the set with the flip task:* a final "flip the spec to IMPLEMENTED (+ dated history line + mirror)" task under the same parent — the tracked obligation that closes the lifecycle loop when the build finishes. Never skip it; "a human remembers" is the failure mode this exists to prevent.
3. *Turn a critical eye on completeness.* Re-read the spec — every phase, every acceptance criterion, every named deliverable, every data-safety/principle rule — and confirm each has a home in a task. The work is not done when the tasks merely exist; it is done when nothing in the spec is left untracked. This completeness pass is mandatory regardless of project type.
@@ -150,7 +152,7 @@ The workflow is complete when these tasks exist, the completeness pass confirms
Accept, modify, or reject — but never silently drop. The reviewer must be able to account for every point.
** Document the no's, not the yes's
-Accepted recommendations live in the spec body (the change *is* the record). Modified and rejected ones need an explicit written reason at the bottom, because the change is invisible and the reasoning would otherwise be lost. The asymmetry is deliberate.
+Accepted findings live in the spec body (the change *is* the record). Modified and rejected ones need an explicit written reason on the completed finding task, because the change is invisible and the reasoning would otherwise be lost. The asymmetry is deliberate.
** Critique, don't rubber-stamp
A review you accept entirely without finding a single thing to push on probably wasn't read critically. Your judgment — including a well-reasoned no — is the value you add.
@@ -164,11 +166,11 @@ When reviews conflict, find the framing where both are right. Silently honoring
** A reject goes back to the reviewer, not just into the file
Recording a reasoned reject is the floor, not the close. Communicate the rejection and its reason to the reviewer — a reject is a two-party event, not a unilateral call. If the reviewer disagrees, that's a discussion: weigh the counter, and if you still can't agree, escalate to whoever owns the decision rather than letting the author's "no" stand by default. "I'm not doing that" with no reason the reviewer can engage is the failure mode. (For a tight solo author-reviewer loop this is lightweight; for a team it's the difference between a review and a rubber-stamp-in-reverse.)
-** The spec reads forward, the dispositions read backward
-The body is written for the implementer (no review archaeology). The dispositions section is written for the reviewer (the reasoning trail). Keep the two audiences separate.
+** The spec reads forward, the findings read backward
+The body is written for the implementer (no review archaeology). A completed finding's reason is written for the reviewer (the reasoning trail). Keep the two audiences separate.
** The history explains provenance, not implementation behavior
-The spec body should still be the implementation contract. The bottom =Review and iteration history= section is for provenance: number of iterations, dates, contributors (including agents), roles, what each pass contributed, and why. Keep it short enough that future readers can understand how decisions evolved without rereading chats, deleted review files, or session logs.
+The spec body should still be the implementation contract. The bottom =Review and iteration history= section is for provenance: number of iterations, dates, contributors (including agents), roles, what each pass contributed, and why. Keep it short enough that future readers can understand how decisions evolved without rereading chats or session logs.
** Re-read before editing
The spec may have changed since you last saw it. Edit the current file, reconcile against the latest tracking state.
@@ -213,3 +215,8 @@ Update this workflow as we learn what works. Capture new disposition patterns, b
- *What:* Reconciled this workflow to spec-create's new Decisions convention (each decision is an org =TODO= task that flips to =DONE= on agreement, with a =[/]= cookie on the =* Decisions= heading and a =*** Discussion= child for disputes). Exit Criterion 5, Phase 2's pre-agreed-decisions step, and Phase 4 steps 4-5 now speak in flip-to-=DONE= terms, and the implementation-ready step gates on the all-=DONE= cookie.
- *Why:* The convention change landed in spec-create.org via an .emacs.d handoff (originated in its keymap-consolidation spec); this workflow still described the retired =State: proposed | accepted | superseded= model.
- *Artifacts:* Handoff =inbox/2026-06-12-1906-from-.emacs.d-spec-create-decisions-todo-note.org=. Paired spec-create.org and spec-review.org edits in the same commit.
+
+** 2026-06-21 Sun @ 23:16:06 -0400 — Claude Code (rulesets) — responder
+- *What:* Folded the review into the spec. Findings are now =* Review findings= =TODO= tasks the responder completes in place (accept/modify → =DONE=, reject → =CANCELLED= with the reason) instead of a "Review dispositions" section; the response is done when the =[/]= cookie reads complete, not when a review file is deleted. Phase 0 finds open work by an incomplete findings cookie; the Phase 4 implementation-ready gate now also requires the findings cookie, and rerun-the-readiness-rubric-on-expanded-scope is folded into that gate (a scope-expanding response must file new obligations as findings or decisions before claiming =Ready=).
+- *Why:* Deleting the review file left the iteration-history =Artifacts= line dangling and lost the verbatim review; keeping the file collided with this workflow's file discovery and its "no review files remain" done-condition. Craig's call: incorporate the review into the document, reusing the decisions machinery so the readiness signal is a cookie. The scope-expansion rerun closes a real gap — a response can resolve every finding and still introduce unreviewed obligations.
+- *Artifacts:* Paired spec-review.org edits in the same commit. Inbox handoffs =2026-06-20-2339-from-home-spec-response-readiness-gate-proposal.org= and =2026-06-21-0156-from-home-companion-to-tonight-s-spec-response.org=.
diff --git a/claude-templates/.ai/workflows/spec-review.org b/claude-templates/.ai/workflows/spec-review.org
index d956f00..0da8e65 100644
--- a/claude-templates/.ai/workflows/spec-review.org
+++ b/claude-templates/.ai/workflows/spec-review.org
@@ -5,9 +5,9 @@
* Overview
-The spec-review workflow evaluates a feature/specification document before implementation and decides one thing: can an engineer implement it confidently, test it thoroughly, and ship behavior that matches the user's mental model? If yes, say so and stop. If no, write a review file next to the spec naming every blocking gap and the concrete change that closes it.
+The spec-review workflow evaluates a feature/specification document before implementation and decides one thing: can an engineer implement it confidently, test it thoroughly, and ship behavior that matches the user's mental model? If yes, say so and stop. If no, record every blocking gap and the concrete change that closes it as findings in the spec's own =* Review findings= section.
-This is the *reviewer* side of a pair. Its counterpart is the spec-response workflow, which the spec's author runs to fold a review back in. The contract between them is the review file: =<spec-basename>-review.org= (e.g. =docs/issue-query-spec.org= → =docs/issue-query-spec-review.org=). spec-review produces it; spec-response consumes it.
+This is the *reviewer* side of a pair. Its counterpart is the spec-response workflow, which the spec's author runs to disposition the findings. The contract between them lives *in the spec*: a =* Review findings= section carrying one =TODO= task per finding, with a =[/]= cookie — the same shape the spec's =* Decisions= section already uses. spec-review writes the findings; spec-response completes them. No separate review file is written, so nothing dangles when a review is processed and the full review/response trail stays in the spec.
The goal is not to prove the spec is clever. It is to leave the implementer with *fewer* hidden decisions, not more prose.
@@ -28,7 +28,7 @@ A review is complete when:
1. *The implementation-readiness gate has been evaluated* and a rubric label assigned (=Ready= / =Ready with caveats= / =Not ready= / =Needs research=).
2. *If ready:* the user is told plainly ("This spec is implementation-ready. I have no further blocking review notes."), and the review stops — no churn for its own sake.
-3. *If not ready:* a =<spec>-review.org= file is written next to the spec, in the standard structure, with every finding specific and actionable (current behavior named, risk explained, change recommended, blocking-or-not stated).
+3. *If not ready:* findings are recorded in the spec's =* Review findings= section as =TODO= tasks (one per finding, =[/]= cookie on the heading), each specific and actionable (current behavior named, risk explained, change recommended, blocking-or-not stated).
4. *The spec's review history is updated* with who reviewed it, when, which iteration it was, what changed or was recommended, and why.
5. *Deferred work is logged* to =todo.org= (v1 = =[#B]=, vNext/someday = =[#D]=), not left only in chat.
6. *Implementation tasks are enumerated* — the spec's =Implementation phases= section is lifted into a drop-in =todo.org= block (one entry per phase plus a test-surface entry), or, if the spec has no phase decomposition, that gap is raised as a finding.
@@ -50,6 +50,11 @@ Run it *early* — design review exists to catch viability problems and costly m
Before Phase 1, verify the file under review ends with =-spec.org=. Every design, decision, or planning document under a project's =docs/= directory carries that suffix as its identifier. The =.org= extension alone is not enough because =docs/= holds non-spec org files too (tutorials, frozen inventories, reference material).
+*Location expectation (docs-lifecycle convention).* Formal specs live in =docs/specs/=. Whether that's enforced depends on whether the project has run its one-time =spec-sort= retrofit:
+
+- =:LAST_SPEC_SORT:= present in =.ai/notes.org= Workflow State → the project has sorted; a =-spec.org= file outside =docs/specs/= fails this precondition. Surface it: "this spec sits outside docs/specs/ — move it (and update inbound links) before review."
+- Marker absent → legacy locations (=docs/= root, =docs/design/=) stay reviewable; add one nudge line to the review output ("this project's docs pile has never been spec-sorted — say 'run spec-sort' to sort it") and proceed. No legacy spec is ever unreviewable during the transition.
+
If the file does not end with =-spec.org=, stop immediately and surface the mismatch:
#+begin_example
@@ -93,19 +98,19 @@ Mark the spec implementation-ready only if *all* of these hold:
- The plan can be phased without shipping broken intermediate states, and phases are small enough to reach a clean stopping point in one focused work session.
- External API assumptions are verified or explicitly listed as prerequisites.
-If all true → tell the user it's ready and stop unless they ask for more. If any false → continue and write the review file. A "ready" at this phase is provisional; confirm it at Phase 3 after the code read.
+If all true → tell the user it's ready and stop unless they ask for more. If any false → continue and record findings (Phase 5). A "ready" at this phase is provisional; confirm it at Phase 3 after the code read.
** Phase 2: Required reading order
Never review a spec in isolation.
1. *Read the existing implementation first.* The code paths the spec would touch: public commands and entry points, internal helpers/boundaries, current data representation, persistence/write-back, async/sync, caching, error handling, existing tests, naming/style. Capture current-state facts with function names and file paths. Don't recommend designs that ignore how the package works today.
-2. *Read related specs and task tracking.* Companion specs, relevant =todo.org= tasks, README/testing docs, prior review files. Record which tasks the spec absorbs, which stay separate, which decisions are already made, which are still open.
+2. *Read related specs and task tracking.* Companion specs, relevant =todo.org= tasks, README/testing docs, prior reviews (in each spec's =* Review findings= and =Review and iteration history=). Record which tasks the spec absorbs, which stay separate, which decisions are already made, which are still open.
3. *Read the target spec end to end — twice.* First for its problem/behavior/phases/assumptions; second looking only for gaps. The second read asks: "What would an implementer still have to invent?"
** Phase 3: Re-run the gate (authoritative)
-After reading code and spec, re-run the Phase 1 gate — this is the pass that counts, because now you can actually judge the items that needed the code: architecture fit, API verification, integration points. If now ready, don't manufacture churn. If not, write the review file.
+After reading code and spec, re-run the Phase 1 gate — this is the pass that counts, because now you can actually judge the items that needed the code: architecture fit, API verification, integration points. If now ready, don't manufacture churn. If not, record findings (Phase 5).
** Phase 4: Evaluate across dimensions
@@ -128,6 +133,8 @@ Work the spec against these. Each is a source of concrete findings, not a box to
- *Performance & scale.* Expected counts (issues/comments/labels/teams/projects/views)? Server-side filtering where possible? Bounded, visible pagination? Cached name→ID lookups? Sync calls in the command path acceptable? Could a save hook or whole-file scan make N network calls? Rendering linear? Full-file rewrites avoided? Long-running operations async/cancellable/observable? Is concurrency/queueing/backpressure defined? Are high-output process filters throttled and cheap? Is progress/ETA exposed only when defensible, and are hung/stalled operations detectable and killable? Identify UI freezes, repeated network calls, unbounded pagination — without premature optimization.
- *Security & privacy.* API keys safe? Debug logs leaking secrets or private issue text? Confirmations before mutating shared workspace objects? Personal vs shared distinguished? Local files holding sensitive descriptions/comments? Anything to redact from messages/logs? Any work-tracker integration may handle private company data.
- *UX & accessibility.* Discoverable commands? Recoverable mistakes? Prompts ordered to the task? Safe, useful defaults? Informative-not-noisy status messages? Does the UI avoid implying unsupported actions are supported? Match the upstream product's permissions/concepts? Are customizations named in user language, with clear defaults and docstrings? For Emacs packages, command names, completion candidates, buffer layout, defcustom names, and message wording *are* the UX.
+- *Operational-panel UI traps.* Applies when the spec covers a user-facing panel, dialog, or control surface; skip otherwise. Lists that mix saved, current, and generated items must name each item's source. Refresh or scan actions must not gate data that could be shown immediately. Add-forms must not ask the user to retype values the system already discovered. Destructive confirmations read in future tense before the action and verified-result tense after it. Diagnostics, performance, logging, and repair affordances are reviewed as one coherent flow before extra pages or buttons are added. A popup launched from a bar, tray, or tool surface should visually belong to that launcher. (Promoted from archsetup's Waybar network-panel review, 2026-06-30.)
+- *Prototype process for non-trivial UI.* Applies when the deliverable is a real UI (a panel, a multi-control surface, an interacting visual layout — not a single dialog or CLI flag); skip otherwise. Verify the =claude-rules/ui-prototyping.md= process ran: category research is cited in Goals/Design, the final prototype is linked in the design section, a =Prototype iterations= subsection under the status heading lists every pass, and each UI design decision is backed by a prototype it was seen working in rather than asserted on the page. A non-trivial-UI spec with decisions but no prototype evidence is a =:blocking:= finding.
- *Test strategy and coverage.* Characterization tests before behavior changes? Pure functions to unit-test? API responses needing fixtures? Command flows needing stubs? Regression tests for prior bugs? Boundary/error cases? What's covered elsewhere and shouldn't be re-tested? Which existing tests must change? How is coverage generated, summarized, and used to find untested/refactor-worthy code? Prefer tests that lock contracts: representation shape, query compilation, sync no-op, conflict refusal, pagination, dirty-buffer protection, log redaction, and long-running/slow-operation behavior via fakes rather than flaky live dependencies.
- *Observability & operations.* How does a user see what the package is doing? Progress messages for long ops? Useful, safe debug logging? Are logs structured enough to isolate issues from a bug report? Are commands provided to inspect/clear caches, test connectivity, diagnose backends/tools, copy redacted debug info, or reproduce command invocations? How are terminal states discovered: completion, failure, partial success, stalled/hung, cancelled, cleanup-unverified, and "needs user action"? Does the product notify only when useful, avoid noisy success spam, and keep non-success states visible until acknowledged? For generated org files, headers should often carry source, filter/view name, refresh time, count, truncation state.
- *Comparable-product sentiment.* When there are obvious adjacent products, research what users love and hate about them from official docs plus current community reports. Do not cargo-cult their feature set; translate findings into the spec's scope. For each loved behavior, say whether the spec provides it, intentionally omits it, or defers it. For each hated behavior, say whether the spec avoids, resolves, inherits, or accepts it.
@@ -136,66 +143,41 @@ Work the spec against these. Each is a source of concrete findings, not a box to
- *Development tooling.* Does the repo give contributors obvious commands for setup, fast tests, specific tests, compile, lint, coverage, cleanup, slow/manual tests, and release checks? Are optional/live tests gated by explicit environment variables? Is the Makefile/script surface consistent with sibling projects?
- *Small enhancement radar.* Are there low-complexity, high-value affordances already provided by the platform that should be surfaced now or explicitly deferred? Examples: archive/compress commands in file managers, built-in history, previews, diagnostics, or doctor commands. Keep the hot path simple; capture the opportunity rather than accidentally losing it.
-** Phase 5: Write the review file
+** Phase 5: Record findings in the spec
-Use this structure for =<spec-basename>-review.org= unless the spec calls for something different:
+Findings live in the spec, not a sibling file. Add (or append to) a =* Review findings= section near the spec's =* Decisions= section, with a =[/]= cookie on the heading. Each finding is a =** TODO= task: the heading is the smallest noun phrase naming the gap; the body names current behavior, the risk, and the recommended change. Tag a blocking (high-priority) finding =:blocking:= — it holds the rubric at =Not ready= until dispositioned; leave non-blocking findings untagged. Findings accumulate across review rounds the way decisions do, and the responder completes each one in place (Phase 4 of spec-response), so the section becomes the full review/response trail.
#+begin_src org
-,#+TITLE: Review: <Spec Title>
-,#+AUTHOR: <reviewer>
-,#+DATE: <date>
-,#+STARTUP: showall
-
-,* Scope reviewed
-What code, tests, docs, and specs you read.
-
-,* Implementation-readiness
-Whether the spec is ready. If not, summarize the blockers.
-
-,* Overall assessment
-The short senior-engineering read: what's right, what's risky, what must be clarified.
-
-,* High-priority findings
-Concrete headings. Each: why it matters and what to change.
-
-,* Medium-priority findings
-Important improvements that shouldn't block all progress.
-
-,* UX observations
-,* Architecture observations
-,* Robustness and performance observations
-,* Test strategy recommendations
-Specific test cases, not generic "add tests".
-,* Documentation and tooling recommendations
-README/user/developer docs, Makefile/package scripts, coverage, debug tools, and customization surface.
-
-,* Suggested spec edits
-Concrete edits to make the spec implementation-ready.
-
-,* Agreed decisions
-Answers reached during review. Omit if none.
-
-,* Open questions
-Only questions that truly block or materially affect implementation.
-
-,* vNext candidates
-Deferred features to capture in task tracking.
+,* Review findings [/]
+,** TODO Comment edit-back is undefined :blocking:
+The spec says fetched comments render as subheadings but doesn't define whether
+editing one syncs back. Linear only lets users edit their own comments. V1 should
+treat fetched comments as remote-owned display content and support only adding new
+comments; editing own comments can be vNext. (blocking)
+,** TODO Empty result and fetch error render identically
+A failed fetch and a successful-but-empty fetch produce the same buffer, so the
+user can't tell "no issues" from "the query broke." Define a distinct empty-state
+message. (non-blocking)
#+end_src
+Where the old review-file sub-sections go now: the scope-reviewed and overall-assessment narrative goes in the =Review and iteration history= entry (Phase 6); suggested spec edits are the recommended-change line in each finding's body; agreed decisions flip the spec's own =* Decisions= tasks; open questions are =:blocking:= findings or open decisions; vNext candidates are logged to =todo.org= as =[#D]= (Phase 6). The Phase 4 review dimensions are where findings come *from* — not headings to reproduce in the spec.
+
** Phase 6: Assign the rubric and update tracking
Assign one label consistently:
-- =Ready= — no blocking open questions; implementation can start. Requires no decision in the spec's =* Decisions= section to still be =TODO= (the =[/]= cookie reads complete; =SUPERSEDED= and =CANCELLED= count as resolved) — a decision still =TODO= holds the rubric at =Not ready=, or =Ready with caveats= if the author consciously accepts and records the risk.
+- =Ready= — no blocking open questions; implementation can start. Requires both cookies complete: no decision in =* Decisions= and no =:blocking:= finding in =* Review findings= still =TODO= (the =[/]= cookies read complete; =SUPERSEDED=/=CANCELLED= and a completed or rejected finding count as resolved) — a still-=TODO= decision or =:blocking:= finding holds the rubric at =Not ready=, or =Ready with caveats= if the author consciously accepts and records the risk. A non-blocking finding left =TODO= is author's discretion and does not hold the rubric.
- =Ready with caveats= — can start if the caveats are accepted and tracked.
- =Not ready= — blocking ambiguity / missing decisions would force implementers to invent product behavior.
- =Needs research= — external API/library/platform assumptions must be verified first.
The most useful reviews move a spec from =Not ready= to =Ready with caveats= or =Ready= once decisions are captured.
+*The =Ready= verdict flips the spec's lifecycle status.* spec-review owns the =DRAFT= → =READY= transition (docs-lifecycle convention): on assigning =Ready= (or =Ready with caveats= the author accepts), update the spec's top-level status heading keyword to =READY=, add a dated history line under it naming the review that passed, and set the Metadata =Status= mirror to =ready= — three lines, one file. Any other rubric label leaves the keyword where it stands (a re-review that finds new blockers on a =READY= spec demotes it back to =DRAFT= the same three-line way, with the reason in the history line).
+
Finding severity maps to blocking power: *high-priority findings block =Ready=* — they hold the rubric at =Not ready= (or =Ready with caveats= if the author accepts and tracks them) until dispositioned; *medium-priority findings are the author's discretion* and don't block. State the blocking status on each finding so the author running spec-response knows which ones gate the rubric.
-Then update the spec's review history. Specs should carry a bottom section named =Review and iteration history= (or the nearest existing equivalent) that tracks each material author/reviewer pass. Add a concise entry for this review even when the spec is ready and no review file is written.
+Then update the spec's review history. Specs should carry a bottom section named =Review and iteration history= (or the nearest existing equivalent) that tracks each material author/reviewer pass. Add a concise entry for this review even when the spec is ready and no findings are recorded.
Each entry is an org subheading with a compound id followed by three body fields.
@@ -207,27 +189,11 @@ Body fields:
- *What changed or was recommended:* high-signal summary, not a duplicate of the whole review.
- *Why:* the decision pressure or rationale that caused the contribution.
-- *Artifacts:* links to the review file, response/disposition section, commits, task IDs, or source checks when useful.
+- *Artifacts:* links to the relevant findings, commits, task IDs, or source checks when useful.
If the spec has no such section, add it at the bottom. Keep the history short and cumulative; it is provenance for future readers, not a session transcript.
-*Emit implementation tasks (drop-in for =todo.org=).* Read the spec's =Implementation phases= section and turn it into a paste-ready block in the review file, under a heading =Implementation tasks (drop-in for todo.org)=. One =** TODO= entry per phase, plus a final entry for the test surface. The point: the handoff to whoever implements is one paste, not a re-read of the spec, and a spec that can't be decomposed into phases fails this step, surfacing a shape problem before =Ready=.
-
-Per-phase entry, following =todo-format.md= (terse heading names the phase; body holds the one-line deliverable plus a pointer back to the spec; tags on the heading):
-
-#+begin_example
-** TODO [#B] <phase name — smallest noun phrase> :feature:
-<what this phase delivers, one line>. Spec: [[file:<spec path>]] (Implementation phases, phase N).
-#+end_example
-
-Final test-surface entry, mirroring the spec's =Acceptance criteria= when present:
-
-#+begin_example
-** TODO [#B] <feature> — test surface :test:
-Unit: <...>. Integration: <...>. E2e / manual-verify: <acceptance criteria as checkable items>. Spec: [[file:<spec path>]] (Acceptance criteria).
-#+end_example
-
-Priority and tags follow the deferred-work rule below. Emit the block in the review file; the author pastes it into =todo.org= during spec-response, or you log it directly when you're also closing the loop. If the spec has no =Implementation phases= section, don't invent one — that absence is the finding, and the step becomes the prompt to ask the author to add a phase decomposition before the spec can be =Ready=.
+*Check the spec decomposes into phases.* A =Ready= spec needs an =Implementation phases= section an implementer can turn into one task per phase plus a test surface. Confirm it's present and decomposable — each phase small enough to reach a clean stopping point in one focused session, with no broken intermediate states. If it's missing or can't be phased, file that as a =:blocking:= finding; don't invent the phases. The phase-to-task breakdown itself is spec-response's job (its Phase 6 reads =Implementation phases= directly once the author confirms =Ready=); the reviewer only verifies the section exists and is sound.
Then log deferred work to =todo.org=: v1 implementation = =[#B]= (unless urgent or speculative); vNext/someday = =[#D]=. Tag =:feature:= / =:bug:= / =:refactor:= / =:test:= / =:quick:= / =:solo:= only when accurate. Don't leave important deferred decisions only in chat.
@@ -256,8 +222,11 @@ Every material comment should be tagged by force: blocking, should-fix, or optio
** Make feedback author-usable
Review comments should be specific, neutral, and actionable: quote or name the spec behavior, explain the risk, recommend the smallest concrete change, and say how the author can verify the fix. Avoid personal language, rhetorical questions, vague "this needs work" comments, and comments that require the author to infer the desired edit.
+** Keep review and response roles explicit
+If the user asks for review plus "enhance the spec" in the same turn, produce the findings first. Make only low-risk provenance and tracking edits unless the user clearly wants the reviewer to respond too. Don't silently resolve product decisions on the author's behalf — a proposed default belongs in a finding until it's accepted, modified, or rejected.
+
** Preserve iteration provenance
-Future reviewers and implementers need to know not just the current decision, but how the spec got there: how many review/response loops happened, who contributed, what they changed or recommended, and why. Keep that record in the spec itself under =Review and iteration history= so the trail survives deleted review files, chat loss, and agent handoffs.
+Future reviewers and implementers need to know not just the current decision, but how the spec got there: how many review/response loops happened, who contributed, what they changed or recommended, and why. Keep that record in the spec itself under =Review and iteration history= so the trail survives chat loss and agent handoffs.
** Be strict about ownership
Especially for org-mode features: a user treats visible text as editable unless the representation says otherwise. Make generated-vs-editable explicit.
@@ -265,6 +234,9 @@ Especially for org-mode features: a user treats visible text as editable unless
** Never depend on an unverified API shape
If the spec assumes fields/mutations/enums, they're verified against current schema/docs/live responses, or listed as a research prerequisite. =Needs research= is a real, useful verdict.
+** Source external-dependency checks in the finding
+When a finding turns on a current external-dependency fact (release version, API capability, platform behavior, package availability, hosted-service terms), cite the checked source in the finding body. Stale dependency assumptions are common, and the next reviewer needs to tell "verified this pass" from "remembered from prior context."
+
** Favor small pure cores and thin IO layers
Push findings toward separable, unit-testable pure functions surrounded by thin command/transport layers.
@@ -354,3 +326,8 @@ Sources:
- *What:* Two refinements to the same-day decisions convention after Craig's review: the gate item and =Ready= rubric now read "no decision is still =TODO=" with =SUPERSEDED= and =CANCELLED= counting as resolved (spec-create's template defines them as done-class keywords via a =#+TODO:= header), and a spec still on the retired =State:= field model explicitly fails the gate item until converted — closing the vacuous-pass hole on old specs.
- *Why:* Review of the freshly-landed convention flagged that TODO/DONE alone lost the old model's superseded state and that the gate as written would silently pass a spec with no decision tasks at all. Craig chose the two done-class keywords and the auto-added =#+TODO:= header (the in-file header is what makes custom keywords portable).
- *Artifacts:* Paired spec-create.org edits (keyword scheme + template header) in the same commit.
+
+** 2026-06-21 Sun @ 23:16:06 -0400 — Claude Code (rulesets) — responder
+- *What:* Moved findings from a sibling =<spec>-review.org= file into the spec itself. Findings are now =** TODO= tasks under a =* Review findings= section with a =[/]= cookie, mirroring =* Decisions=; =:blocking:= marks high-priority. Phase 5 records findings in the spec instead of writing a review file; the Phase 6 =Ready= rubric gates on both the decisions and the findings cookie; the implementation-task drop-in (which lived in the review file) is gone, leaving the reviewer to verify the spec decomposes into phases and spec-response to build the breakdown. Also added two reviewer-practice principles harvested from a home spec-review: keep review and response roles explicit, and source external-dependency checks in the finding.
+- *Why:* The delete-the-review-file convention left the iteration-history =Artifacts= line dangling and dropped the verbatim review; keeping the file instead collided with spec-response's file discovery and its "no review files remain" done-condition. Craig's call: incorporate the review into the document, reusing the decisions machinery so the readiness signal is a cookie, not a file's presence or absence. The role-explicit and source-checking practices came in from the home finance-report spec via inbox handoffs.
+- *Artifacts:* Paired spec-response.org edits in the same commit. Inbox handoffs =2026-06-20-2339-from-home-spec-response-readiness-gate-proposal.org=, =2026-06-21-0156-from-home-companion-to-tonight-s-spec-response.org=, and the home-edited =2026-06-21-0156-from-home-spec-review.org=.
diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org
index 59c9c54..2262eea 100644
--- a/claude-templates/.ai/workflows/startup.org
+++ b/claude-templates/.ai/workflows/startup.org
@@ -1,5 +1,5 @@
#+TITLE: Startup Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-04-25
* Summary
@@ -10,8 +10,8 @@ The workflow is structured into four phases. *Phase A.0* is a sequential pre-fli
Quick contract — runs / produces:
- *Phase A.0* (sequential): refresh rulesets, then the project repo.
-- *Phase A* (parallel batch): timestamp, session-context check, guarded =.ai/= sync, recent sessions, inbox-status, cross-agent status, notes.org, staleness, language-bundle freshness.
-- *Phase B* (parallel batch): read the crash-recovery anchor if present, the recent session summaries, new inbox items, pending cross-agent messages.
+- *Phase A* (parallel batch): timestamp, session-context check, guarded =.ai/= sync, recent sessions, inbox-status, notes.org, staleness, language-bundle freshness.
+- *Phase B* (parallel batch): read the crash-recovery anchor if present, the recent session summaries, new inbox items.
- *Phase C* (interactive): surface findings, process the inbox, run project startup-extras, ask priorities.
* Execution
@@ -29,10 +29,16 @@ Inside a rulesets session, the project-repo refresh below covers this — the ru
#+begin_src bash
rs="$HOME/code/rulesets"
if [ -d "$rs/.git" ]; then
- if (cd "$rs" && git diff --quiet --ignore-submodules HEAD -- 2>/dev/null); then
+ gate="$rs/claude-templates/bin/git-worktree-gate"
+ if [ -x "$gate" ] && "$gate" sync-safe "$rs"; then
+ (cd "$rs" && git pull --ff-only origin main 2>&1) | tail -3
+ elif [ ! -x "$gate" ] \
+ && (cd "$rs" && git diff --quiet --ignore-submodules HEAD -- 2>/dev/null); then
+ # Bootstrap fallback for a checkout old enough not to have the shared
+ # gate yet. The pull that follows installs it for subsequent starts.
(cd "$rs" && git pull --ff-only origin main 2>&1) | tail -3
else
- echo "rulesets: dirty working tree — using as-is, skipping pull"
+ echo "rulesets: changes beyond untracked inbox deliveries — using as-is, skipping pull"
fi
else
echo "rulesets: not a git checkout — skipping"
@@ -40,10 +46,12 @@ fi
#+end_src
Behavior:
-- *Clean working tree* → fast-forward pull. =git pull --ff-only= refuses any merge or rebase, so the operation is either a no-op (already current) or a clean advance.
-- *Dirty working tree* → skip the pull. Don't auto-stash and don't auto-merge — those would either lose work or invite conflicts at the worst possible moment (session start).
+- *Clean working tree, or untracked deliveries only beneath =inbox/=* → fast-forward pull. =git pull --ff-only= refuses any merge or rebase, so the operation is either a no-op (already current) or a clean advance. Inbox files are queue input, not source-tree work, and do not block other projects from receiving rulesets updates.
+- *Any staged or tracked change, dirty submodule, Git operation in progress, or untracked file outside =inbox/=* → skip the pull. Don't auto-stash and don't auto-merge — those would either lose work or invite conflicts at the worst possible moment (session start).
- *Non-fast-forward history* → =--ff-only= aborts with an error. Surface that to the user; the rsync still proceeds against the working tree as-is.
+*Template-freshness policy (applies to every dirty-check in the synced workflows).* The shared =git-worktree-gate sync-safe= policy is the source of truth: untracked files beneath =inbox/= and gitignored files do not block a pull, fast-forward, or monitoring gate; every other staged, tracked, untracked, submodule, or in-progress-operation state does. Projects must not fall behind merely because somebody sent them a task, but an arbitrary scratch file is not silently treated as safe. One deliberate exception remains: the rsync WIP-guard below is narrower than the repository gate and counts untracked files within rulesets' own synced source paths, because an untracked half-written template is exactly the WIP it exists to hold back.
+
*** Install rulesets symlinks into ~/.claude (idempotent)
A skill, rule, or bin script added to rulesets and pushed reaches each machine's *files* on the next pull, but not its =~/.claude= *symlink* — =make install= only links what isn't already linked, and =git pull= doesn't run it. So a newly-added skill stays silently uninstalled until someone re-runs =make install= by hand. The flush skill sat in that gap from 2026-06-02 until a manual install on 2026-06-05. Running =make install= here, right after the rulesets pull, closes it: "add a skill, commit, push" becomes enough for it to reach every machine on the next session.
@@ -72,8 +80,11 @@ if [ -d .git ]; then
current=$(git symbolic-ref --short HEAD 2>/dev/null)
dirty=0
- if ! git diff --quiet --ignore-submodules HEAD -- 2>/dev/null \
- || [ -n "$(git status --porcelain --untracked-files=no)" ]; then
+ gate="$HOME/code/rulesets/claude-templates/bin/git-worktree-gate"
+ if [ -x "$gate" ]; then
+ "$gate" sync-safe "$PWD" >/dev/null 2>&1 || dirty=1
+ elif ! git diff --quiet --ignore-submodules HEAD -- 2>/dev/null \
+ || [ -n "$(git status --porcelain --untracked-files=no)" ]; then
dirty=1
fi
@@ -105,8 +116,8 @@ fi
#+end_src
Behavior, per branch:
-- *Behind only, current branch, clean tree* → =git merge --ff-only= advances HEAD.
-- *Behind only, current branch, dirty tree* → fetched but not advanced. Surface so Craig can ff manually after dealing with the dirty state.
+- *Behind only, current branch, sync-safe tree* → =git merge --ff-only= advances HEAD. An untracked =inbox/= delivery is sync-safe.
+- *Behind only, current branch, sync-blocking tree* → fetched but not advanced. Surface so Craig can ff manually after dealing with the reported state.
- *Behind only, non-checkout branch* → =git fetch . upstream:branch= advances the ref without touching the working tree.
- *Diverged* (ahead and behind) → leave alone. Surface for Craig to resolve. Don't auto-rebase or auto-merge.
- *Ahead only* or *up to date* → silent no-op.
@@ -124,7 +135,7 @@ These calls have no dependencies on each other. Issue them all together in one m
sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org)
[ -e "$sc" ] && echo "present: $sc" || echo "absent: $sc"
#+end_src
-3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync.
+3. *Sync =.ai/= from templates — but only when the synced source paths in rulesets are clean.* Guard the three rsyncs behind a check that =claude-templates/.ai/{protocols.org,workflows/,scripts/}= have no uncommitted changes. Otherwise Phase A copies in-flight rulesets WIP (tracked edits or new untracked files) into this project's =.ai/workflows/= and =.ai/scripts/=, where it shows up as drift the user didn't author. Skipping once is cheap — the next session with rulesets clean catches up. The check is scoped to the synced paths, so unrelated rulesets dirt (a stray =session-context.org=, scratch files) doesn't needlessly block the sync. A second guard skips the same rsyncs when the *project* branch is behind its upstream (=git rev-list --left-right --count @{u}...HEAD= with =behind > 0=): syncing templates onto a stale committed =.ai/= baseline measures the diff against old content, so it comes out huge and conflicts when the branch later reconciles to upstream, whose history already carries the newer templates. It composes with the rulesets-clean guard — a stable rulesets source and a current project branch are both required before the sync runs.
#+begin_src bash
rs="$HOME/code/rulesets"
@@ -132,26 +143,75 @@ These calls have no dependencies on each other. Issue them all together in one m
claude-templates/.ai/protocols.org \
claude-templates/.ai/workflows/ \
claude-templates/.ai/scripts/ 2>/dev/null)
- if [ -z "$synced_dirty" ]; then
+ # Skip the sync when the project branch hasn't reached its upstream. Syncing
+ # templates onto a behind baseline measures the diff against stale committed
+ # .ai/, producing confusing drift that conflicts when the branch reconciles —
+ # the newer .ai/ is already in upstream. behind==0 (up-to-date or ahead-only)
+ # means HEAD contains all of upstream, so the baseline is current. No upstream
+ # (new/unpushed branch) → rev-list fails → proj_behind stays 0, sync runs.
+ proj_behind=0
+ if [ -d .git ]; then
+ counts=$(git rev-list --left-right --count '@{u}...HEAD' 2>/dev/null) \
+ && [ "$(printf '%s' "$counts" | cut -f1)" -gt 0 ] 2>/dev/null \
+ && proj_behind=1
+ fi
+
+ if [ -n "$synced_dirty" ]; then
+ echo "rulesets has uncommitted changes under the synced template paths — skipping .ai/ sync this session (catches up when rulesets is clean):"
+ echo "$synced_dirty" | sed 's/^/ /'
+ elif [ "$proj_behind" -eq 1 ]; then
+ echo "project branch is behind upstream — skipping .ai/ sync this session (templates never land on a stale baseline; the sync runs once the branch is current)"
+ else
rsync -a "$rs/claude-templates/.ai/protocols.org" .ai/protocols.org
rsync -a --delete "$rs/claude-templates/.ai/workflows/" .ai/workflows/
rsync -a --delete --exclude='__pycache__' --exclude='.pytest_cache' --exclude='*.pyc' \
"$rs/claude-templates/.ai/scripts/" .ai/scripts/
echo ".ai/ synced from templates"
- else
- echo "rulesets has uncommitted changes under the synced template paths — skipping .ai/ sync this session (catches up when rulesets is clean):"
- echo "$synced_dirty" | sed 's/^/ /'
fi
#+end_src
4. =\ls -t .ai/sessions/ 2>/dev/null | head -5= — list 5 most recent session files. The backslash bypasses any =ls= alias in the user's profile. Without it, bare =ls -t= silently returns no output under =exa= (a common =ls= replacement) — which makes a sessions directory full of files look empty, and the agent then skips Phase B step 2.
5. =\ls -la inbox/ 2>/dev/null= — inventory the inbox. Same reason for the backslash escape, applied uniformly across the Phase A =ls= calls.
-6. =cross-agent-status 2>/dev/null || true= — snapshot of pending cross-agent messages across local projects. This is layer A of the cold-start design from =cross-agent-comms.org=: pending messages from other agents (delivered while no session was active here) get surfaced on session start. The =|| true= keeps Phase A from failing if =cross-agent-status= isn't installed yet — older projects without the script still boot cleanly. If HALT is active, =cross-agent-status= prints a banner; surface that prominently in Phase C.
-7. Read =.ai/notes.org= — Project-Specific Context, Active Reminders, Pending Decisions sections (skip About This File).
-8. Read =.ai/project-workflows/startup-extras.org= if it exists.
-9. =[ -f todo.org ] && .ai/scripts/task-review-staleness.sh todo.org 7 || true= — count top-level tasks overdue for review (the daily task-review habit's startup nudge). The =[ -f todo.org ]= guard skips projects without a root todo.org; =|| true= keeps Phase A from failing if the script isn't synced yet. Threshold 7 days is one review cycle of slack — softer than the wrap-up health check's 30-day alarm.
-10. =bash ~/code/rulesets/scripts/sync-language-bundle.sh "$PWD" 2>/dev/null || true= — language-bundle freshness for the current project. Fingerprint-detects which bundle (if any) the project has, auto-fixes drifted rulesets-owned files (=.claude/rules/*.md=, =.claude/hooks/*=, =githooks/*=), and surfaces drift in =settings.json= without writing it (a project may have customized it). =CLAUDE.md= is deliberately left untracked — it's seed-only in =install-lang= and project-owned afterward, mirroring how =diff-lang= skips it. Quiet when there's no bundle or everything's clean. Hardcodes the rulesets path because =languages/= is the canonical source and lives only there — the same absolute-path dependency the rsyncs already carry. =|| true= keeps Phase A from failing on older checkouts where the script isn't present yet. The =.ai/= rsyncs and this call write to disjoint paths (=.ai/= vs =.claude/=/=githooks/=), so the batch stays parallel-safe.
-11. =[ -f "$HOME/org/roam/inbox.org" ] && grep -cE '^\*\* ' "$HOME/org/roam/inbox.org" || true= — count items in the roam global inbox (=~/org/roam/inbox.org=), the inbox-zero startup nudge. Silent if the roam clone isn't on this machine. Phase C reads the file when the count is non-zero, splits total vs items related to this project, and surfaces the offer (see =inbox-zero.org=). Read-only; never files at startup.
+6. Read =.ai/notes.org= — Project-Specific Context, Active Reminders, Pending Decisions sections (skip About This File).
+7. Read =.ai/project-workflows/startup-extras.org= if it exists.
+8. =[ -f todo.org ] && .ai/scripts/task-review-staleness.sh todo.org 7 || true= — count top-level tasks overdue for review (the daily task-review habit's startup nudge). The =[ -f todo.org ]= guard skips projects without a root todo.org; =|| true= keeps Phase A from failing if the script isn't synced yet. Threshold 7 days is one review cycle of slack — softer than the wrap-up health check's 30-day alarm.
+9. =bash ~/code/rulesets/scripts/sync-language-bundle.sh "$PWD" 2>/dev/null || true= — language-bundle freshness for the current project. Fingerprint-detects which bundle (if any) the project has, auto-fixes drifted rulesets-owned files (=.claude/rules/*.md=, =.claude/hooks/*=, =githooks/*=), and surfaces drift in =settings.json= without writing it (a project may have customized it). =CLAUDE.md= is deliberately left untracked — it's seed-only in =install-lang= and project-owned afterward, mirroring how =diff-lang= skips it. Quiet when there's no bundle or everything's clean. Hardcodes the rulesets path because =languages/= is the canonical source and lives only there — the same absolute-path dependency the rsyncs already carry. =|| true= keeps Phase A from failing on older checkouts where the script isn't present yet. The =.ai/= rsyncs and this call write to disjoint paths (=.ai/= vs =.claude/=/=githooks/=), so the batch stays parallel-safe.
+10. =[ -f "$HOME/org/roam/inbox.org" ] && grep -cE '^\*\* ' "$HOME/org/roam/inbox.org" || true= — count items in the roam global inbox (=~/org/roam/inbox.org=), the roam-mode startup nudge. Silent if the roam clone isn't on this machine. Phase C reads the file when the count is non-zero, splits total vs items related to this project, and surfaces the offer (see =inbox.org= roam mode). Read-only; never files at startup.
+11. KB surface prep (the read + contribute startup nudges; see =docs/specs/2026-06-16-encourage-kb-contribution-spec.org=). Gated on the agent KB clone. Counts =:agent:= nodes, lists up to 5 whose content matches the current project basename (titles only; a few most-recent nodes as a fallback when nothing matches), and resolves the best-practices node path. Read-only; silent when the clone is absent. Phase C surfaces the relevant titles (consult) and the best-practices link (contribute).
+
+ The best-practices lookup matches the node's *filename*, not its content. A roam node's slug lives only in its filename, so the earlier content-grep (=rg -l 'agent-kb-best-practices'=) matched nothing and the contribute nudge silently pointed at an empty path in every project, every session, for as long as it shipped. =find= rather than a glob keeps the probe identical under bash and zsh (zsh aborts on an unmatched glob) — the same reason the spec-sort probe below uses =find=.
+
+ #+begin_src bash
+ ra="$HOME/org/roam/agents"
+ if [ -d "$ra" ]; then
+ proj=$(basename "$PWD")
+ echo "kb-total: $(rg -l '#\+filetags:.*:agent:' "$ra" 2>/dev/null | wc -l)"
+ echo "kb-bestpractices: $(find "$ra" -maxdepth 1 -name '*agent-kb-best-practices*.org' -print -quit 2>/dev/null)"
+ matches=$(rg -il "$proj" "$ra" 2>/dev/null | head -5)
+ [ -z "$matches" ] && matches=$(\ls -t "$ra"/*.org 2>/dev/null | head -3)
+ echo "kb-relevant-titles:"
+ for f in $matches; do rg -m1 '^#\+title:' "$f" 2>/dev/null | sed 's/^#+title:/ -/'; done
+ fi
+ #+end_src
+
+12. Spec-sort probe (the docs-lifecycle retrofit nudge; see the docs-lifecycle spec in =docs/specs/=). Read-only; prints one line when the project has an unsorted docs pile — a =docs/design/= directory or stray =docs/*-spec.org= root files — and no =:LAST_SPEC_SORT:= marker in =.ai/notes.org=. Silent for projects with nothing to sort or an already-stamped marker (the marker permanently clears it).
+
+ #+begin_src bash
+ { [ -d docs/design ] || [ -n "$(find docs -maxdepth 1 -name '*-spec.org' -print -quit 2>/dev/null)" ]; } \
+ && ! grep -qs ':LAST_SPEC_SORT:' .ai/notes.org \
+ && echo "spec-sort: unsorted docs present" || true
+ #+end_src
+
+ The stray-root check uses =find= rather than a glob so the probe behaves identically under bash and zsh (=compgen= is bash-only, and zsh aborts on an unmatched glob).
+
+13. Host-identity probe (see the host-identity rule in =claude-rules/=). Read-only; flags fixed machine-identity claims in the project's tracked/synced docs — the "This machine is ratio" trap, false on every machine but the one that wrote it. Silent when nothing matches.
+
+ #+begin_src bash
+ grep -inE '\b(this|the current) (machine|host|box|laptop|workstation) is ' \
+ CLAUDE.md .ai/notes.org 2>/dev/null | head -3 || true
+ #+end_src
+
+ Fleet descriptions ("the fleet is ratio and velox") and runtime derivations ("run =uname -n= to find the hostname") don't match — only current-identity assertions do. Fixture-verified under bash and zsh.
Notes on the rsync commands:
- Trailing slashes on both source and destination matter — they tell rsync to sync /contents/ rather than nest a directory inside.
@@ -159,6 +219,7 @@ Notes on the rsync commands:
- protocols.org is a single file, no =--delete= needed.
- The =scripts/= sync excludes Python build artifacts (=__pycache__/=, =.pytest_cache/=, =*.pyc=). Running rulesets' own pytest leaves these in =claude-templates/.ai/scripts/tests/=, and =rsync -a= copies by disk presence regardless of =.gitignore=, so without the excludes every consuming project's tree gets polluted with machine-specific cache files. The excludes also protect existing dest copies from =--delete= cleanup, so a project that already received the cache must remove it once by hand.
- The sync is guarded to skip when rulesets has uncommitted changes under the synced source paths. =rsync -a --delete= copies the working tree by disk presence, so without the guard a downstream session started while rulesets had in-flight WIP would pull that WIP into its =.ai/workflows/= and =.ai/scripts/=, surfacing as drift the user never authored (and tempting a fake "chore: sync .ai tooling" commit). The guard is scoped to the synced paths, not the whole repo, so unrelated rulesets dirt doesn't block the sync. From the jr-estate handoff 2026-05-29.
+- The sync is also guarded to skip when the *project* branch is behind its upstream (=proj_behind=). Phase A.0 correctly declines to fast-forward a diverged or behind-and-dirty branch, but the rsync would then land templates on the stale committed =.ai/= baseline — a huge diff measured against old content that conflicts once the branch reconciles to upstream's newer templates. Skipping is safe: the sync runs next session once the branch is current. Not an auto-discard — startup never =git checkout=s drift away, because a legitimate local stopgap in a synced file is indistinguishable from accidental drift by content alone (home reverted an intentional =flashcard-to-anki.py= fix this way on 2026-06-22). Prevention is safe; blind cleanup-after is not. Phase C's template-sync-churn safety net still surfaces any pre-existing dirt for a human decision. From the home handoff 2026-07-04.
- The sync touches only =protocols.org=, =workflows/=, and =scripts/=. The project-owned dirs =project-workflows/= and =project-scripts/= are deliberately *outside* the synced set, so a project's own workflows and scripts survive startup. This is why a project script that a workflow imports must live in =.ai/project-scripts/=, never =.ai/scripts/= — the latter is wiped to match the template by =--delete= on every startup. Naming: a script imported as a Python module needs an importable name (underscores, e.g. =zlibrary_api.py=); a CLI-invoked script can stay kebab-case like the template tooling (=cmail-action.py=).
Rationale: Every call in Phase A is read-only or writes to a distinct path. Running them sequentially wastes round-trips; running them in parallel gives Claude the complete starting picture in one round-trip.
@@ -170,7 +231,6 @@ These calls depend on Phase A outputs, but are independent of each other. Issue
1. *Read =.ai/session-context.org= if Phase A reported it exists.* The file is the crash-recovery anchor — if it's there, the previous session was interrupted and the context lives only in this file.
2. *Read each of the 5 most recent session files* from Phase A's =\ls -t .ai/sessions/= output. Read just the =* Summary= section of each — not the full file. The Summary gives Active Goal / Decisions / Data Collected / Findings / Files Modified / Next Steps. That's enough to pick up where things left off. Drill into a specific =* Session Log= later only if you need the /why/ or sequence on something. *If Phase A's listing came back empty, sanity-check with =\ls -la .ai/sessions/= before treating empty as definitive — sessions/ should normally be populated, and an empty result usually means the listing got swallowed somewhere, not that the directory is genuinely empty.*
3. *Read each new inbox file* from Phase A's =\ls -la inbox/= output. For =.eml= files, defer to Phase C — those need the extract script (below) rather than a raw Read.
-4. *Process pending cross-agent messages.* For each project with a pending count >0 in Phase A's =cross-agent-status= output (typically the current project; cross-project pending is surfaced too but only acted on if the user asks), run =cross-agent-recv <message-file>= on the file path =cross-agent-status= named. The script returns a structured decision (=process= / =dedup= / =query= / =reject=) per the protocol. For =process=, read the message body to determine the action. For =query=, prepare a clarifying reply. For =reject=, surface to user with the reason. For =dedup=, no action — silent retry already handled. Surface all decisions in Phase C alongside other findings.
Rationale: Reads are independent and benign. Batching them means the whole session-history view + inbox view lands in one round-trip instead of one per file.
@@ -184,7 +244,11 @@ This phase touches the user and runs sequentially:
- Mention Pending Decisions from notes.org.
- Briefly note significant template updates noticed during sync (new workflows, protocol changes).
- *Task-review nudge.* If the Phase A staleness count (step 11) is greater than zero, surface one line: "=<N>= top-level tasks unreviewed for >7 days — say 'let's do a task review' to run a cycle." If zero, say nothing.
- - *Roam inbox nudge.* If the Phase A roam-inbox count is greater than zero, read =~/org/roam/inbox.org=, split total vs items related to this project (claimed by the =<project>:= prefix, plus any unprefixed item whose topic plainly concerns this project), and surface one line: "Roam inbox: =<N>= total, =<M>= appear related to this project — say 'inbox zero' to file them." Offer it as a priority option; never auto-file. If the count is zero or the file is absent, say nothing. See =inbox-zero.org=.
+ - *Roam inbox nudge.* If the Phase A roam-inbox count is greater than zero, read =~/org/roam/inbox.org=, split total vs items related to this project (claimed by the =<project>:= prefix, plus any unprefixed item whose topic plainly concerns this project), and surface one line: "Roam inbox: =<N>= total, =<M>= appear related to this project — say 'inbox zero' to file them." Offer it as a priority option; never auto-file. If the count is zero or the file is absent, say nothing. See =inbox.org= roam mode.
+ - *KB consult nudge (read side).* If the Phase A KB-surface prep returned any =kb-relevant-titles=, surface one line listing them (capped 5): "KB lessons that may be relevant: =<title>=; =<title>=… — open the node before related work." The titles are declarative, so the list alone tells you whether to open one. Gated on the roam clone; silent when the clone is absent or nothing relevant surfaced. See the best-practices node and =knowledge-base.md=.
+ - *KB contribute nudge (write side).* Once per session, surface one line pointing at the best-practices node (the =kb-bestpractices= path from Phase A): "Learned something durable? See =<path>= for how to write a KB node — contributing cross-project facts is welcome (personal projects only; work/unknown projects never write per =knowledge-base.md=)." Light encouragement, never a gate. Gated on the roam clone; silent when absent.
+ - *Spec-sort nudge.* If the Phase A spec-sort probe printed =spec-sort: unsorted docs present=, surface one line: "this project's docs pile has never been spec-sorted — say 'run spec-sort' to sort it." If the probe was silent, say nothing. A project with nothing to sort never sees the line; a stamped =:LAST_SPEC_SORT:= marker permanently clears it. See the docs-lifecycle rule and the spec in =docs/specs/=.
+ - *Host-identity flag.* If the Phase A host-identity probe printed any match, surface it with the file:line and the fix: "this doc asserts a fixed machine identity — false on every other machine; replace with a runtime derivation (run =uname -n=), per the host-identity rule." The probe flags for judgment, never blocks. Silent when the probe is silent.
- *Language-bundle sync.* If the Phase A step-12 call (=sync-language-bundle.sh=) printed anything, surface it. =fixed= lines are informational — the drift was already repaired (note that =.claude/= is now dirty if the project commits it). A =drift= line on =settings.json= is surface-only and needs the printed =make install-<lang> PROJECT=.= to reconcile; flag it so the user can decide. If the call was silent, say nothing.
- *Newly-installed symlinks.* If the Phase A.0 =make install= step printed any =link= / =relink= / =WARN= line, surface it. A =link= line means a skill, rule, hook, or script added to rulesets is now linked into =~/.claude= for the first time on this machine. For a newly-linked *skill*, check the agent's available-skills list: if the harness already registered it mid-session, note it's available and move on; if it's absent, stop and tell Craig to restart the agent so it loads (whether a mid-session reload works is harness-version-dependent). For a newly-linked *hook*, note that the harness reads hooks at session start — it fires from the next session (or after Craig opens =/hooks= once); its settings.json wiring travels with the tracked file, so the link is usually the only missing piece. A =WARN ... not a symlink= line is a real collision at the target path — surface it; it needs a human. If the step printed only "nothing new to link", say nothing.
- *Template-sync churn (safety net).* Check whether Phase A's rsync left uncommitted churn in the synced =.ai/= paths — accumulated from a prior session that crashed before wrap-up, or freshly added this session when rulesets advanced. Without surfacing, it builds up silently until it blocks Phase A.0's auto-ff (git won't ff a dirty tree). Skip in the rulesets repo itself (there =.ai/= is a committed mirror, kept honest by the pre-commit hook). The check is sequential here, after the rsync has finished — not a Phase A step, to keep that batch race-free.
@@ -197,8 +261,7 @@ This phase touches the user and runs sequentially:
#+end_src
If it reports a count, surface one line: wrap-up's Step 4.0 will commit it as =chore: sync .ai tooling from templates=, or offer to commit it now. If silent, say nothing. This is the crashed-session counterpart to the wrap-up commit step (the primary fix). From the 2026-05-31 jr-estate + work handoffs.
- - *Surface pending cross-agent messages.* If =cross-agent-status= reported any pending messages, list them with their =cross-agent-recv= decision (process / query / reject) per file. For =process= messages in this project's inbox, propose handling now or after the current task. For pending in other projects, mention the count so the user knows to switch projects when ready. If HALT was active, surface that prominently — cross-agent activity is paused until =cross-agent-resume= clears it.
-2. *Process inbox if non-empty.* Mandatory — don't ask, just delegate to [[file:process-inbox.org][process-inbox.org]]. That workflow owns the value gate (advances an existing TODO / improves the project / serves the mission), the per-source rejection flow (Craig / project handoff / script), the priority-scheme check before filing, and the =.eml= extraction path. Single source of truth for the discipline.
+2. *Process inbox if non-empty.* Mandatory — don't ask, just delegate to [[file:inbox.org][inbox.org]] process mode. That mode owns the value gate (advances an existing TODO / improves the project / serves the mission), the per-source rejection flow (Craig / project handoff / script), the priority-scheme check before filing, and the =.eml= extraction path. Single source of truth for the discipline.
3. *Execute project-specific startup extras* (the contents of =.ai/project-workflows/startup-extras.org= read in Phase A). If the file didn't exist, skip.
4. *Ask about priorities.* "What would you like to work on, or is there something urgent you need?"
- If urgent: proceed immediately.
diff --git a/claude-templates/.ai/workflows/status-check.org b/claude-templates/.ai/workflows/status-check.org
index efff16d..4a9972c 100644
--- a/claude-templates/.ai/workflows/status-check.org
+++ b/claude-templates/.ai/workflows/status-check.org
@@ -1,5 +1,5 @@
#+TITLE: Status Check Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-02
* Overview
diff --git a/claude-templates/.ai/workflows/summarize-emails.org b/claude-templates/.ai/workflows/summarize-emails.org
index 6ac5e6f..c9c7001 100644
--- a/claude-templates/.ai/workflows/summarize-emails.org
+++ b/claude-templates/.ai/workflows/summarize-emails.org
@@ -1,5 +1,5 @@
#+TITLE: Summarize Emails Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-14
* Overview
diff --git a/claude-templates/.ai/workflows/suspend.org b/claude-templates/.ai/workflows/suspend.org
new file mode 100644
index 0000000..166f9c9
--- /dev/null
+++ b/claude-templates/.ai/workflows/suspend.org
@@ -0,0 +1,143 @@
+#+TITLE: Session Suspend Workflow
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-06-28
+
+* Overview
+
+This workflow captures the live state of a session when Craig must leave
+abruptly, so a future session resumes with nothing lost. It is the fast,
+capture-only workflow for departure: it writes down where every thread stands,
+notes any uncommitted work, then STOPS — no cleanup, no archive, no teardown.
+
+Triggered by Craig saying "suspend the session," "suspend," "I need to go,"
+"stick a pin in everything," or similar. "I need to go" is broad — if it reads
+as a conversational aside rather than a request to suspend, confirm before
+running.
+
+* Where suspend sits among its neighbors
+
+Three workflows touch the session anchor (=.ai/session-context.org=); keep them
+straight:
+
+- =flush= ([[file:../../flush/SKILL.md]] / =/flush=) — *stay and sharpen.*
+ Refreshes the anchor in place, prompts Craig to type =/clear=, and a hook
+ resumes the *same* logical session in a fresh context. Craig is still here.
+- *suspend* (this workflow) — *leave.* Captures richly into the anchor, leaves
+ the file in place, detaches the tmux client so the session parks in the
+ re-attachable set, and Craig walks away. The next session is a cold startup
+ that detects the present anchor and resumes from it — or Craig re-attaches the
+ still-live session directly.
+- =wrap-it-up= ([[file:wrap-it-up.org][wrap-it-up.org]]) — *end.* Writes the
+ Summary, archives the anchor into =.ai/sessions/=, commits + pushes, and runs
+ the phrase-dependent teardown.
+
+Suspend and flush share one core — capture into the anchor, leave it in place.
+They differ in the exit (leave vs clear-and-continue) and the resume path
+(startup vs the =/clear= hook). Suspend reuses flush's capture discipline (its
+Phase 1 anchor-refresh) rather than restating it, and adds a richer,
+resume-weighted Session Log entry because it's written for a cold resume after a
+gap, not a same-session reset.
+
+* Suspend vs wrap-up — the one structural difference
+
+=wrap-it-up= ARCHIVES =.ai/session-context.org= (renames it into
+=.ai/sessions/=); its absence at the next startup is the signal that the last
+session ended cleanly.
+
+Suspend does the opposite: it LEAVES =.ai/session-context.org= in place. Its
+presence at startup is exactly the signal that the previous session was
+interrupted, so the startup workflow reads it and resumes. Suspend provides only
+the *capture* half — startup's existing interrupted-session path (Phase A checks
+for the anchor, Phase B reads it, Phase C offers to resume) is the *resume* half,
+already built.
+
+So: never archive, never rename the context file in a suspend. Capture into it
+and leave it.
+
+* What gets captured
+
+The point is zero lost information, weighted toward RESUME. Into the
+=* Session Log= of =.ai/session-context.org=, append one dated
+=** YYYY-MM-DD ... — SUSPENDED= entry holding:
+
+1. *Open threads — resume here.* For each active or pending thread: the topic,
+ its status (ACTIVE / PINNED / SET ASIDE / DEFERRED), the immediate next
+ step, and the pointers needed to act on it cold (files + line numbers,
+ commit SHAs, the specific finding or decision). This is the core; spend the
+ most words here. Order newest / most-active first.
+2. *Pending decisions / open questions* awaiting Craig — anything blocked on
+ his input, with enough context that the answer is actionable.
+3. *Shipped this session* — a terse list of what landed, each with its commit
+ SHA, so the resume knows what is already done and need not re-derive it.
+4. *Uncommitted work* — anything modified on disk but not committed, named
+ file by file, so the resume knows what state the tree is in.
+5. *Key findings not yet recorded elsewhere* — anything learned this session
+ that isn't already in a commit, a file, or memory, so it survives.
+6. *Background work* — any running task, agent, or job, and how to check it.
+7. *Resume hint* — the single most likely "start here" next action.
+
+Also update the top of =* Summary= (Active Goal) with a one-line SUSPENDED
+pointer to the entry, so startup reading the top sees the current state even
+when the Summary body is from an earlier thread.
+
+* Steps
+
+1. *Write the SUSPENDED entry* into the Session Log, per "What gets captured"
+ above. Timestamp with =date "+%Y-%m-%d %a @ %H:%M:%S %z"=.
+2. *Update the Active Goal pointer* at the top of =* Summary=.
+3. *Record uncommitted work, don't force-commit it.* A suspend records state, it
+ does not tidy it. Name every uncommitted change in the SUSPENDED entry and
+ leave the tree as it is — on an abrupt departure, a dirty tree (like any
+ crash) is safer than a blind commit of arbitrary mid-work state. (If a
+ project defines a standing always-commit set in its own workflow, commit only
+ that set — but the default shared behavior is to leave the tree alone.)
+4. *Leave =.ai/session-context.org= in place.* Do not archive it.
+5. *Brief handoff* — one or two lines: what was captured, where the resume
+ pointer is, the most-active thread. This is the last thing Craig sees before
+ the view detaches (Step 6), so deliver it complete.
+6. *Detach the tmux client.* As the final action, detach the client viewing the
+ =aiv-<project>= session so it drops out of Craig's active view while staying
+ alive in the background. This is a DETACH, not a teardown: the session and the
+ agent process keep running, nothing is killed, no context is lost.
+
+ #+begin_src bash
+ sess=$(tmux display-message -p '#S' 2>/dev/null)
+ [ -n "$sess" ] && tmux detach-client -s "$sess"
+ #+end_src
+
+ Run it as the very last tool call, after the handoff text has rendered — tmux
+ preserves the pane, so Craig sees the full handoff when he re-attaches. Unlike
+ wrap-up's teardown (which must defer to a =Stop= hook because it kills the
+ session the agent runs in, which would cut off the valediction), detach runs
+ inline: it disconnects the view but leaves the agent's session alive, so
+ nothing is cut off. Degrade gracefully — if not inside tmux (=$TMUX= unset, no
+ session), skip silently and the session simply stays attached.
+
+ Why detach on every suspend: Craig cycles his live agent sessions in Emacs
+ with alt-space, and rotates through everything — including re-attaching
+ detached ai-term sessions — with shift+alt+space. A suspended session left
+ attached clutters the active rotation; detaching parks it in the
+ re-attachable set, which is what makes suspend-and-walk-away work. Re-attach
+ is one keystroke (shift+alt+space) or =tmux attach -t aiv-<project>=.
+
+* What suspend does NOT do
+
+Speed over completeness. A suspend deliberately skips everything wrap-it-up
+does beyond capture:
+
+- No =* Summary= rewrite beyond the one-line Active Goal pointer.
+- No todo.org cleanup / archive-done.
+- No KB / memory promotion sweep.
+- No Linear / board reconciliation.
+- No session-record archive (the file stays live).
+- No teardown. Suspend DETACHES the tmux client (Step 6) but never kills the
+ session: the =aiv-<project>= session and the agent process stay alive in the
+ background, only the view disconnects. It drops no =Stop=-hook teardown
+ sentinel, so the wrap-teardown hook stays dormant. Teardown — killing the
+ session — is wrap-it-up's job, not suspend's; detach is the lighter move that
+ parks a still-live session.
+- No blind commit of working files (step 3).
+- No valediction. A suspend is a pause, not a goodbye.
+
+If Craig later wants the clean end, he runs wrap-it-up, which picks up the
+captured state and finishes the job.
diff --git a/claude-templates/.ai/workflows/sync-email.org b/claude-templates/.ai/workflows/sync-email.org
index 52a7caf..863b400 100644
--- a/claude-templates/.ai/workflows/sync-email.org
+++ b/claude-templates/.ai/workflows/sync-email.org
@@ -1,5 +1,5 @@
#+TITLE: Sync Email Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-02-01
* Overview
diff --git a/claude-templates/.ai/workflows/task-audit.org b/claude-templates/.ai/workflows/task-audit.org
index 67ce496..aa50176 100644
--- a/claude-templates/.ai/workflows/task-audit.org
+++ b/claude-templates/.ai/workflows/task-audit.org
@@ -1,5 +1,5 @@
#+TITLE: Task Audit Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-22
* Overview
@@ -61,6 +61,8 @@ For each open task, read its body and cross-check its claims against the actual
- *Calendar* — did a scheduled event happen; is a SCHEDULED/DEADLINE date now past.
- *Meeting recordings* — if a task hinges on "did this conversation happen / what was said," check the recording queue (e.g. =~/sync/recordings/=) and transcribe via =process-meeting-transcript.org= if the answer lives in an un-transcribed recording. (This is exactly how a "did the interview happen?" task gets resolved instead of guessed.)
+*Spec lifecycle reconcile (docs-lifecycle convention).* If the project has a =docs/specs/=, run the =:SPEC_ID:= query as part of this phase: for each spec whose top-level status heading reads =DOING=, find the =todo.org= task whose =:SPEC_ID:= property matches the spec's =:ID:=. Flag the spec NEEDS-USER when that bound parent is =DONE=/=CANCELLED=, archived, or missing — the build finished (or evaporated) without the =IMPLEMENTED= flip, exactly the drift this check exists to catch. Check the parent's own keyword, not its children (completed children become dated entries and the final flip task is a child, so child-counting misleads).
+
Assign each task a bucket (CURRENT / STALE / NEEDS-USER) and, for STALE, the specific factual update.
*Scale tactic.* For a large open-task set, dispatch read-only investigation sub-agents over batches of tasks (parallel-safe per =subagents.md= — independent read-only domains). Each returns a per-task bucket + suggested update. *Never* let sub-agents write to =todo.org= concurrently — apply all edits serially in the main thread (concurrent writes to one file race and lose work).
@@ -79,11 +81,41 @@ For every STALE task, edit it in the main thread:
- *Ensure priority is set per the project scheme.* The top of the project's =todo.org= should carry the priority legend (=[#A]= through =[#D]=). Every task should carry an explicit priority cookie. If a cookie is missing, or no longer matches the reconciled facts, assign the right level per the legend. If the level is unambiguous from the body, do it autonomously; if it's a judgment call (especially the [#A] / [#B] line for important-but-not-urgent work), flag NEEDS-USER. Also enforce the [#A]-discipline rule from the legend — an [#A] task without a =SCHEDULED:= or =DEADLINE:= line is mis-graded and is either down-graded to [#B] (when reconciled facts say "important but not urgent") or surfaced as NEEDS-USER for the user to date.
- *Ensure a type tag is set.* Every task carries one type tag from the project's tag legend (typically =:feature:= / =:chore:= / =:spec:= / =:bug:=). If missing or wrong, assign or correct it from the body when the type is unambiguous. If two tags fit (a refactor that also fixes a bug; a spec that's also a chore), flag NEEDS-USER rather than picking one silently.
- *Enforce the project's declared tag vocabulary.* If the project's tag legend declares an *exhaustive* set of allowed tags, strip from each task any tag outside that set — the heading and parent section already carry topic/scope context, so ad-hoc tags only fragment the vocabulary and defeat tag-based filtering. Normalize near-duplicate spellings to the canonical tag (a plural to its singular, say). Where the legend does not declare the set closed, leave existing tags alone; this step applies only where the allowed set is exhaustive by design.
-- *Re-assess the =:quick:= and =:solo:= tags* — reconciliation can change a task's effort or autonomy: a resolved dependency may make a stuck task =:solo:=, a scope cut may make it =:quick:=, and new complexity surfaced by the sources can invalidate either. Add or remove the tags per the definitions in the project's tag legend (and [[file:task-review.org][task-review.org]]) when the reconciled facts make the call clear. When they don't — an effort estimate you can't pin down, a =:solo:= gate you can't confirm — it's a NEEDS-USER flag, not a guess.
+- *Re-assess the =:quick:= and =:solo:= tags (mandatory — an audit that skips this is incomplete).* Reconciliation can change a task's effort or autonomy: a resolved dependency may make a stuck task =:solo:=, a scope cut may make it =:quick:=, and new complexity surfaced by the sources can invalidate either. Add or remove the tags per the hard definitions in [[file:../../claude-rules/todo-format.md][todo-format.md]] ("Hard definitions: :solo: and :quick:"; task-review carries the same three-gate walk). Autonomous execution reads =:solo:= as its eligibility gate and trusts the tag, so a stale one is a run-time hazard, not cosmetic drift. When the call isn't clear — an effort estimate you can't pin down, a =:solo:= gate you can't confirm — it's a NEEDS-USER flag, not a guess.
- Bump =:LAST_REVIEWED:= on each edited task.
Follow =todo-format.md= for completion mechanics (depth-based DONE vs dated-rewrite) and the working-files / link-hygiene rules when moving artifacts.
+** Phase C.5 — Consolidate related tasks (interactive)
+
+Phase C's *Consolidate duplicates* bullet folds tasks that track the *same* thing. This step is the broader case: tasks that aren't duplicates but are really *one effort* fragmented across the list. A spread-out effort — several tasks all circling "make the tooling agent-agnostic," say — is harder to see, plan, and finish as a whole than one task, or one parent with the pieces as children.
+
+After the Phase C edits, read the open-task set as a whole and look for *clusters*: tasks that share a goal, a subsystem, or an obvious sequence. Use judgment over the task bodies, not a keyword heuristic — adjacency is a semantic call, and a brittle title-match both misses real clusters and invents false ones.
+
+For each cluster, surface it to Craig (inline numbered options per =interaction.md=, no popup) with a recommendation, offering the two shapes:
+
+- *Merge* — fold the cluster into one task when the members are genuinely the same work split up (near-duplicates, or steps with no independent value). The merged task keeps the strongest priority, unions the type tags, and absorbs each member's body as a dated note or a short list; the absorbed tasks close per =todo-format.md= (a =**= task → =CANCELLED= + =CLOSED:= with a one-line "merged into <task>", or deletion if it carried nothing unique).
+- *Parent with children* — when the members are related but distinct (each ships independently or has its own value), promote a parent task and re-home the members beneath it as sub-tasks, so the list shows the effort as a unit without losing the individual pieces.
+
+Never merge or re-parent autonomously — which tasks belong together, and whether they're one-work or related-distinct, is a judgment only Craig ratifies. Propose, don't apply, until he picks. A cluster he declines stays as separate tasks; don't re-surface it every audit (note the decline in the session log).
+
+When no clear cluster exists, say so in one line and move on — most audits won't find one, and forcing a merge fragments worse than it consolidates.
+
+** Phase C.6 — Retire completed parents and promote stragglers (interactive)
+
+Phase C.5 consolidates related *open* tasks. This step retires parent tasks whose work is *finished*, so completed containers don't linger in Open Work as scaffolding.
+
+Run =todo-cleanup.el --convert-subtasks= first (it's part of the =clean-todo= / wrap-up cleanup, and =open-tasks.org= runs it too) so every completed sub-task is a dated event-log entry rather than a lingering =DONE= keyword. The closure logic below reads "open child" as a child heading still carrying a task keyword (=TODO=/=DOING=/=WAITING=/=VERIFY=/=NEXT=/=PROJECT=/=STALLED=/=DELEGATED=); a dated entry is correctly not open.
+
+Two shapes, both proposed to Craig (inline numbered options per =interaction.md=, no popup) before applying:
+
+- *Zero open children → close the parent.* A parent whose child *tasks* are all resolved (now dated) and that carries no open child task is finished: close it per =todo-format.md= (=**= parent → =DONE=/=CANCELLED= + =CLOSED:=), and it moves to Resolved on the next =--archive-done=. If the work resurfaces later, a fresh task is created then; a completed container shouldn't sit open as a placeholder.
+- *One or two open children → promote, then close.* When a parent has only one or two open children, pull them out and rewrite them as standalone =**= level-2 tasks — give each a priority per the project scheme, and make the heading stand alone without the parent's context — then close the now-childless parent and let it move to Resolved. The former children become first-class Open Work tasks; the retired parent stops being scaffolding for one or two stragglers.
+
+*The leaf-with-notes carve-out (important).* "Zero open children" is not the same as "done." A =**= leaf task whose only descendants are dated *notes* — a captured "Ideas", "Goals", or "Current State" entry, not a real completed sub-task — is unstarted work with a note attached, not a finished container. Do not close it. Tell the two apart by intent: a container reads as a grouping (a =PROJECT= keyword, an explicit "parent grouping ..." line, or several dated entries that were genuinely separate sub-tasks that shipped); a leaf-with-notes is a single feature/bug task whose title names unstarted work and whose lone dated child is a design note. When the call is ambiguous, flag it NEEDS-USER rather than closing.
+
+Never close or promote autonomously past the ambiguous line — surface the candidates with a recommendation and let Craig ratify, the same interactive stance as Phase C.5. Clear container completions (a =PROJECT= whose every child is dated) can be proposed as a batch; leaf-with-notes ambiguities are flagged individually. Verify open-vs-done counts against the actual headings (a real scan of the subtree), not a fragile regex that a shell's =\b= support can silently break — a miscount here closes live work.
+
** Phase D — Flag the judgment calls (interactive)
Present the NEEDS-USER bucket as a short, scannable list — one line per task, naming the decision or the fact required. Adjudicate with the user one item at a time (inline numbered options per =interaction.md=, no popup). Apply the user's calls as they come (which may itself produce more autonomous updates, or new tasks).
@@ -132,3 +164,7 @@ Two Phase C behaviors added, both surfaced by an Emacs-config =todo.org= audit:
- *Tag-vocabulary enforcement.* That project declares a closed tag set (=bug=, =feature=, =refactor=, =test=, =quick=, =solo=); the audit had to strip ~44 ad-hoc tags that had accumulated across the file. The prior workflow only checked that a type tag was *present* — it had no concept of an exhaustive allowed set. The new bullet enforces a declared closed vocabulary and leaves open-vocabulary projects untouched.
- *Code-complete-but-unverified closing.* Many tasks had shipped (tests green, live in the daemon) but stayed open awaiting a manual or visual verification, so they accumulated as half-open. Leaving them open is noise; auto-closing them would violate "never claim a fix verified before the user confirms." The fix routes the pending human check into the project's =Manual testing and validation= parent (dedup-checked) per =verification.md='s manual-verification hand-off, then closes the implementation task. The work is done and the check is tracked; a failed check promotes to a bug.
+
+** 2026-07-01 — Retire completed parents (Phase C.6)
+
+Added Phase C.6: retire a parent task once its child *tasks* are all done. Zero open children → close the parent; one or two open children → promote them to standalone level-2 tasks, then close. Surfaced by an Emacs-config =todo.org= audit where several PROJECT containers had all children complete. Depends on =todo-cleanup.el --convert-subtasks= running first so completed sub-tasks are dated (not lingering =DONE= keywords) and the open-child count is accurate. Carries a leaf-with-notes carve-out: a =**= leaf task whose only descendant is a dated design note ("Ideas"/"Goals") is unstarted work, not a finished container, and must not be closed — the ambiguous case is flagged NEEDS-USER. The step also warns against counting open-vs-done with a fragile regex (a =\b= that a given shell/awk silently drops miscounts and closes live work).
diff --git a/claude-templates/.ai/workflows/task-review.org b/claude-templates/.ai/workflows/task-review.org
index 69e172d..7ea2e8e 100644
--- a/claude-templates/.ai/workflows/task-review.org
+++ b/claude-templates/.ai/workflows/task-review.org
@@ -1,5 +1,5 @@
#+TITLE: Task Review Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-20
* Overview
@@ -57,7 +57,9 @@ Keep is the common case — most tasks are still right and just need re-stamping
*** Tagging =:quick:= — small tasks
-While reviewing each task, estimate its effort. If you judge it *30 minutes or less* and it doesn't already carry =:quick:=, add the tag to the heading line. If the heading and body don't tell you how long it'll take, *ask Craig* — don't guess. A wrong =:quick:= is worse than none: the tag exists so Craig can grab a genuinely small task in a spare moment, and a mislabeled one wastes that moment.
+The =:quick:= and =:solo:= assessments (this section and the next) are *mandatory* for every reviewed task except a Kill — a review that skips them is incomplete. The hard definitions live in [[file:../../claude-rules/todo-format.md][todo-format.md]] ("Hard definitions: :solo: and :quick:"); autonomous execution (work-the-backlog / the no-approvals speedrun) reads =:solo:= as its eligibility gate and trusts the author's tag, so the run-time gate is only as trustworthy as this pass.
+
+While reviewing each task, estimate its effort. If you judge it *30 minutes or less* and it doesn't already carry =:quick:=, add the tag to the heading line. If the heading and body don't tell you how long it'll take, *ask Craig* — don't guess. A wrong =:quick:= is worse than none: the tag exists so Craig can grab a genuinely small task in a spare moment, and a mislabeled one wastes that moment. =:quick:= is an effort hint only, never an eligibility gate — size does not decide what runs autonomously.
This is orthogonal to the action chosen — a task can be kept (or re-graded, or marked DOING) *and* tagged =:quick:= in the same pass. Skip the assessment on a Kill, since it's leaving the pool. Tags go on the heading line per [[file:../../claude-rules/todo-format.md][todo-format.md]], sharing one =:tag1:tag2:= cluster.
@@ -67,7 +69,7 @@ While reviewing each task, judge whether Claude could build *and* verify it with
1. *Buildable* — Claude has the capability and access to do the work.
2. *Verifiable by Claude* — an objective or local check exists that Claude can run itself. Craig's routine spot-checking does not count against this, and neither does handing off a residual human-in-the-loop confirmation as a structured manual-testing reminder (the =verification.md= "Handing Off Manual Verification" pattern). The disqualifier is having no verification path of Claude's own at all — when the success criterion is only judgeable by Craig's eyes or subjective taste.
-3. *No upfront decision* — no design or preference call Craig must make before Claude can begin.
+3. *No deliberation* — no open design question and no "weigh these approaches" with real tradeoffs. At most one or two *quick, upfront-answerable* factual decisions are allowed — the speedrun preset batches those into its pre-flight Q&A, so they don't break the hands-off run. A genuine design or preference call disqualifies.
If any gate is shaky, leave the tag off. Like =:quick:=, a wrong =:solo:= is worse than none — it tells Craig he can hand the task off and walk away, so a mislabeled one wastes that trust. When the heading and body don't make all three gates clear, ask Craig instead of guessing.
@@ -90,12 +92,14 @@ Set =:LAST_REVIEWED:= to today's date (from above) in the task's =:PROPERTIES:=
Body...
#+end_example
-The exact date string matters: =task-review-staleness.sh= and the wrap-up health check both parse =:LAST_REVIEWED: YYYY-MM-DD=.
+Format: =:LAST_REVIEWED:= takes a bare ISO date (=2026-05-20=) or an org-native inactive timestamp (=[2026-05-20 Tue]=, matching the =CREATED:=/=CLOSED:= cookies beside it); =task-review-staleness.sh= and the wrap-up health check normalize both to the date. A value that is neither is a data error — the staleness script warns loudly to stderr (naming the file, line, and value) and leaves the task out of the stale count rather than silently reporting a freshly-reviewed task as never-reviewed. Stamp a clean date and the warning never fires.
*** Killing a task
Follow the completion rules in [[file:../../claude-rules/todo-format.md][todo-format.md]]. A killed top-level =**= task stays task-shaped: change the keyword to =CANCELLED=, add a =CLOSED: [YYYY-MM-DD Day]= line under the heading (generate with =date "+%Y-%m-%d %a"=), and leave the priority and tags intact. It's then a candidate for =--archive-done= at the next cleanup. Don't stamp =:LAST_REVIEWED:= on a kill — it's leaving the review pool anyway.
+A killed *sub-task* (=***= or deeper, under a parent task) instead becomes a dated event-log entry per the depth rule — but you don't have to hand-format it here. =todo-cleanup.el --convert-subtasks= (run in the =clean-todo= and wrap-up cleanup passes) rewrites any level-3+ DONE/CANCELLED/FAILED heading into its dated form mechanically from the =CLOSED= cookie, so a keyword-plus-=CLOSED= close at depth gets normalized on the next cleanup rather than lingering. =lint-org.el= flags any that slip through (checker =subtask-done-not-dated=).
+
* Phase D: Close out
When the batch is done (or Craig calls it early):
diff --git a/claude-templates/.ai/workflows/triage-intake.cmail.org b/claude-templates/.ai/workflows/triage-intake.cmail.org
index d818c72..8d8abfb 100644
--- a/claude-templates/.ai/workflows/triage-intake.cmail.org
+++ b/claude-templates/.ai/workflows/triage-intake.cmail.org
@@ -1,5 +1,5 @@
#+TITLE: Triage Intake — cmail (Proton) Source
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-26
# Source plugin for the triage-intake engine. See triage-intake.org for the
diff --git a/claude-templates/.ai/workflows/triage-intake.github-prs.org b/claude-templates/.ai/workflows/triage-intake.github-prs.org
index c1bc796..644421c 100644
--- a/claude-templates/.ai/workflows/triage-intake.github-prs.org
+++ b/claude-templates/.ai/workflows/triage-intake.github-prs.org
@@ -1,5 +1,5 @@
#+TITLE: Triage Intake — Personal GitHub PRs Source
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-26
# Source plugin for the triage-intake engine. See triage-intake.org for the
diff --git a/claude-templates/.ai/workflows/triage-intake.org b/claude-templates/.ai/workflows/triage-intake.org
index b257f2d..55cc939 100644
--- a/claude-templates/.ai/workflows/triage-intake.org
+++ b/claude-templates/.ai/workflows/triage-intake.org
@@ -1,5 +1,5 @@
#+TITLE: Triage Intake Workflow (Engine)
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-01
* Summary
@@ -11,12 +11,14 @@ Think of it as the ER intake queue: every new message, invite, and PR notificati
*This file is the engine.* It carries no sources of its own. Every source it scans comes from a *source plugin* — a =triage-intake.<source>.org= file the engine loads at Phase 0. The engine is source-agnostic and project-agnostic; the project- and account-specific knowledge lives entirely in the plugins. To add a source, drop a plugin file. To change one, edit its plugin. Never wire a source into this file.
+*Which sources a project pulls is a per-project choice.* A *project-specific* plugin (=.ai/project-workflows/triage-intake.*.org=, never synced) is active by presence — dropping it is the declaration. A *general* plugin (=.ai/workflows/triage-intake.*.org=, template-synced into every project — personal Gmail, cmail, calendar, Telegram, GitHub PRs) is active only when the project names its basename in a =:TRIAGE_SOURCES:= line in =.ai/notes.org= Workflow State (space-separated basenames, e.g. =:TRIAGE_SOURCES: personal-gmail cmail=). A project that declares nothing and owns no project plugin pulls nothing. This is the Phase 0 activation gate — presence is capability, the declaration is activation (see =docs/specs/2026-07-20-triage-source-activation-spec.org=).
+
Distinct from =daily-prep.org=:
- *daily-prep* — heavier, once daily, builds the day's plan + standup brief + meeting prep + time blocks.
- *triage-intake* — fast, repeatable, just answers "what's new since last check?"
-Quick contract — what it does: fans out across source plugins, classifies every item into Action / FYI / Noise-keep / Noise-trash, synthesizes one deduped summary, writes each Action item to =todo.org= as a =:quick:reactive:= task, and executes star/mark-read/trash on confirmation.
+Quick contract — what it does: fans out across source plugins, classifies every item into Action / FYI / Noise-keep / Noise-trash, surfaces one three-section digest (==TASKS== / ==FYI== / ==MISC==) with a two-option close offer, then *closes by default*: files each TASKS item to =todo.org= as a =:quick:reactive:= task, runs the star/mark-read/trash hygiene on every scanned account, advances the sentinel, and tears down anything it started. The close runs unless Craig explicitly holds it.
** When to Use This Workflow
@@ -37,6 +39,8 @@ Typical timing:
Do *not* use when running daily-prep — daily-prep already does this as Phase 3.
+Also runs unattended as sentry's triage pass (=sentry.org=, pass 3): sentry invokes this engine under its no-approvals contract, where destructive actions (deleting, archiving, sending) queue for the morning-approval review instead of firing. The trigger phrases above are unchanged — a manual "triage intake" always routes here directly.
+
* Execution
@@ -56,16 +60,18 @@ ls .ai/workflows/triage-intake.*.org .ai/project-workflows/triage-intake.*.org 2
The glob exclude is automatic: =triage-intake.*.org= matches the plugins but not this engine file (=triage-intake.org= has no second dot-segment), so the engine never loads itself.
After globbing, for each plugin file:
-1. Read it.
-2. Evaluate its =ENABLED= precondition. If false, *announce the skip with its reason* ("skipping linear — mcp__linear not present") and move on.
-3. The surviving set is the source list for Phases A-D.
+1. *Activation gate.* A *general* plugin (from =.ai/workflows/=, template-synced into every project) is active only if its basename appears in the project's =:TRIAGE_SOURCES:= declaration (=.ai/notes.org= Workflow State — a space-separated list of source basenames). If it isn't declared, it is *inactive*: announce it ("inactive: personal-gmail — not in :TRIAGE_SOURCES:") and skip it. A *project-specific* plugin (from =.ai/project-workflows/=, never synced) is always active — dropping it there is itself the per-project declaration. This is what stops the synced general plugins from self-activating in projects that aren't triage targets: presence is capability, the declaration is activation (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). An absent or empty =:TRIAGE_SOURCES:= means no general sources are active; a project with no declaration and no project plugin has no active sources, so triage no-ops there.
+2. Read it.
+3. Evaluate its =ENABLED= precondition. If false, *announce the skip with its reason* ("skipping linear — mcp__linear not present") and move on.
+4. The surviving set — active and enabled — is the source list for Phases A-D.
-*Announce the loaded set before scanning* so the omission can't hide:
+*Announce the loaded set before scanning* so the omission can't hide — inactive (undeclared) plugins are named too, so a general plugin left out of =:TRIAGE_SOURCES:= is a visible choice, not a silent drop:
#+begin_example
-Loaded 5 source plugins:
- general: personal-gmail, personal-calendar, cmail, github-prs
+Loaded 2 source plugins (:TRIAGE_SOURCES: personal-gmail cmail):
+ general: personal-gmail, cmail
project: deepsat-gmail
+ inactive (undeclared): personal-calendar, github-prs, telegram
skipped: linear (mcp__linear not present)
#+end_example
@@ -91,23 +97,27 @@ Every item lands in one bucket. Plugins refine these with source-specific bias a
Per-source bias (a work email account leans keep for audit value; a personal account leans trash on high noise volume) lives in each plugin's =Classify= section. Read it from there; don't re-derive it here.
-*** Phase C: Synthesize a single summary
+*** Phase C: Synthesize — the three-section digest
-One markdown summary surfaced inline to Craig. Order:
+One digest surfaced inline to Craig — notable items only, written as prose bullets a reader can absorb without knowing the source taxonomy. It is *not* a per-source roll-call; the plugins' =Render= shapes feed the classification, they are no longer displayed as blocks. (Format ratified by Craig 2026-07-18 after a sweep where the follow-up "summarize the notable items" digest was the report he actually wanted first.)
-0. *Scan failures — first, loud, always.* Any loaded source whose scan failed, hung, was killed, or was skipped for an operational reason renders at the very top of the summary, before Top signals:
+Order:
+
+0. *Scan failures — first, loud, always.* Any loaded source whose scan failed, hung, was killed, or was skipped for an operational reason renders at the very top of the summary, before everything else:
#+begin_example
⚠ SCAN FAILED: <source> — <reason, one line> — <what's now unknown>
#+end_example
- A failed scan is never folded into "quiet." Quiet means the scan ran and found nothing; a failure means the sweep is blind on that channel, and the reader must know which. The same applies to a precondition skip the user hasn't standing-approved (e.g. a messaging client that needs a temporary server spin-up): run the lifecycle or report the failure — don't silently narrow the sweep.
+ A failed scan is never folded into "quiet." Quiet means the scan ran and found nothing; a failure means the sweep is blind on that channel, and the reader must know which. The same applies to a precondition skip the user hasn't standing-approved (e.g. a messaging client that needs a temporary server spin-up): run the lifecycle or report the failure — don't silently narrow the sweep. Backlog banners (a plugin's pre-anchor-residue probe firing) render here too — loud, above the sections.
+
+1. *==TASKS==* — every work item that needs Craig or his sign-off. Sorted in two groups: *solo-executable first* (see the solo test in Phase D), then the rest; within each group, priority order (blocking someone / deadline inside 48h first). Each item is one short prose bullet naming who, what, and why-now, with the source link or locator.
+2. *==FYI==* — substantive work context worth seeing, no action owed. Priority order.
+3. *==MISC==* — everything outside the project: personal mail, family, household, personal calendar. Priority order. An outside-project item that needs Craig still lands here (with its action-ness stated inline), not in TASKS — TASKS is the work queue. MISC items are *not* filed into this project's =todo.org=; they surface in the digest and are gone after the close unless Craig reroutes them to their owning project (the "and reroute" modifier, below).
-1. *Top signals to act on* — bullet list of 3-7 items, ordered by urgency, *Action only*. Each bullet links to the source (permalink, thread URL, PR number).
-2. *Per-source breakdown* — one short section per loaded source *that has changes*, in =ORDER=, using that plugin's =Render= shape: Action items detailed, FYI items as a short list, Noise as a tally only ("Noise: 12 trash candidates, 4 keep, 0 starred").
-3. *Suggested actions* — explicit list of state changes Craig could take this run (trash these N messages, mark-read these M, star this Action item, respond to this invite, merge PRs #X and #Y, etc.). This line stays whenever there are queued actions, regardless of how quiet the sweep was.
+A section with nothing in it is omitted. After the sections, the offer (see Phase D). The sweep's final line is always the run timestamp (=date "+%A %Y-%m-%d %H:%M %Z"=).
-*Deltas only.* The summary reports what *changed* since the anchor: a new invite, a new/moved/cancelled calendar event, a new message needing attention. A source with no changes gets no block — no "Calendar — quiet", no "PRs — nothing new" roll-call. A sweep where nothing changed anywhere renders as a single line:
+*Deltas only.* The digest reports what *changed* since the anchor: a new invite, a new/moved/cancelled calendar event, a new message needing attention. A source with no changes contributes nothing — no "Calendar — quiet", no "PRs — nothing new" roll-call. A sweep where nothing changed anywhere renders as a single line plus the timestamp:
#+begin_example
17:39 sweep: no changes
@@ -117,11 +127,40 @@ One markdown summary surfaced inline to Craig. Order:
Scan failures are the standing exception: a failed or skipped scan always renders loudly per point 0 above and is never folded into the no-change line — "no changes" is a claim about channels the sweep could actually see.
-Format target: scannable in 30 seconds, full read in 2 minutes. Don't pad.
+Format target: scannable in 30 seconds, full read in 2 minutes. Don't pad. The old long-form report (anchor line, per-source breakdown, itemized suggested-actions list) is available *on request* — it is no longer the default surface.
+
+**** The offer — exactly this, right after the sections
+
+#+begin_example
+1. Close the triage — file todo.org tasks for the TASKS items, run the standard close
+2. Close the triage and execute the solo TASKS now, after filing the rest
+Append "and reroute" to either option to send the outside-project items to their owning projects.
+Or tell me what you'd like handled differently.
+#+end_example
+
+Option 2 renders *only when solo-executable TASKS exist*. The reroute line renders only when MISC is non-empty. No other options, no itemized action menu — the close (Phase D) owns the routine hygiene.
+
+*The reroute modifier.* "1 and reroute" / "2 and reroute" (or a bare "reroute" right after a close) means: in addition to the close, deliver every outside-project item the sweep surfaced to the project that owns it. Routing goes through =inbox-send= per the cross-project rule — a handoff into the owner's =inbox/=, never a direct write to a foreign =todo.org= — and the owner's own inbox processing files it by its conventions. This is the persistence path for MISC: without a reroute, MISC items are surfaced-only.
+
+*** Phase D: Close — the default, not an option
+
+*Closing the triage is the next action after the digest, no exceptions* — unless Craig explicitly says not to ("hold the triage", "don't close yet"). If his reply picks option 1 or 2, close per the option. If his reply is anything else — a question, a redirect, a new task — *close the triage first* (as option 1), then handle what he asked. An unclosed triage strands the noise unprocessed and the sentinel stale; Craig ruled 2026-07-18 that the close, including the mail hygiene on every scanned account, is unconditional.
+
+The close, in order:
+
+1. *File every TASKS item into =todo.org=* as its own =:quick:reactive:= task (format below). Dedupe against existing tasks first — fold into an existing task's body when one already covers the topic.
+2. *Mail and message hygiene on every scanned account*: trash the Noise-trash set, mark-read the Noise-keep set, star what was flagged for keeping. This runs *without itemized confirmation* — the digest's tallies are the notice, and the actions dispatch through each plugin's =Actions= verbs. Trash is recoverable (Gmail 30-day trash; cmail =\Deleted= flag), which is what makes the no-confirmation batch safe.
+3. *Clear unacked items* that the sweep found resolved.
+4. *If option 2: execute the solo TASKS.* The solo test — mechanical, standing-approved, and producing *no prose under Craig's name*: calendar RSVPs, ticket-state moves the publishing overlay already authorizes, mark-read/star/trash. Anything that sends words as Craig (a Slack reply, an email, a PR comment, a Linear comment) is *never* solo — it stays a filed task and goes through the normal draft gate when worked. Destructive or hard-to-reverse actions beyond mail hygiene (branch deletes, PR merges) also stay confirm-gated per their plugins.
+5. *If "and reroute": route the outside-project items.* For each MISC item (and any surfaced item this project doesn't own), send a handoff to the owning project's inbox: =inbox-send <project> --text "..."= carrying what it is, the source locator (message id, thread, event id), and why it routed. Resolve the owner against =inbox-send --list=; when ownership is ambiguous, ask before sending — a wrong-project handoff costs more than one question. Never write another project's =todo.org= directly (cross-project rule). Note in the close's status line which items went where.
+6. *Advance the sentinel* — write the held Phase A capture into the sentinel's *content* (see "Capture the Phase A timestamp"): =echo "$PHASE_A_TS $(date -d "@$PHASE_A_TS" '+%Y-%m-%d %H:%M:%S %z')" > .ai/last-triage-intake=. Do not use plain =touch= (writes mtime to /now/ and strands items posted between Phase A and end of run) and do not use =touch -d "@$PHASE_A_TS"= (correct timestamp but mtime is per-machine — won't survive a fresh clone or cross-machine sync).
+7. *Tear down anything the sweep started* (e.g. the telegram lifecycle's leave-no-trace shutdown).
+
+After the close, report one status line — what shipped, the sentinel time — and stop. The close *is* the exit; there is no separate confirmation loop.
-**** Sub-step: write each Action item into =todo.org= as its own =:quick:= task
+**** Task-filing format (=todo.org=)
-After surfacing the summary inline, append every Action item — regardless of source — to =todo.org= as its own top-level =** TODO= heading carrying the =:quick:= tag plus =:reactive:= and any relevant person/entity tag.
+Append every TASKS item — regardless of source — as its own top-level =** TODO= heading carrying the =:quick:= tag plus =:reactive:= and any relevant person/entity tag.
Each Action item is one task. Don't group items by source under =** Email Response=, =** PR Review=, etc. sub-headings. Each response is its own filterable task so Craig can re-prioritize, =SCHEDULE:= / =DEADLINE:=, or tag individually.
@@ -142,30 +181,113 @@ Rules:
- *Record the source locator in the task body* so a reply can be routed back to where the request came from — the channel + thread id for chat, the repo + PR number, the message id for mail. The general rule: a reply goes back to the *origin* of the request, not a fixed notification channel. (Project plugins may add stricter routing rules in their own files.)
- Placement: append at end of =* Work Open Work= (just before =* Work Incubate=) unless the project's =todo.org= has a designated triage section near the top (=* Triage= or =* Inbox=).
-This sub-step makes triage-intake's findings *persist* in =todo.org= instead of evaporating after the inline summary.
+The filing makes triage-intake's findings *persist* in =todo.org= instead of evaporating after the inline digest. Every close action dispatches to the owning source plugin's =Actions= verb (trash, mark-read, star, respond, merge, comment, attachment-fetch) — the engine doesn't hardcode action commands; it reads them from the loaded plugins.
-*** Phase D: Execute actions on confirmation
+*** Exit Criteria
-Wait for Craig's go-ahead before running any state changes. Default to single-confirmation for the whole batch ("yes" → run everything proposed). Craig may also pick a subset ("trash personal but hold the work account") or hand back a different plan ("trash all but star the expense thread and queue PR merges for after lunch").
+The close is the exit. Once the close completes (tasks filed, hygiene run, sentinel advanced, teardown done), report one status line — what shipped, the sentinel time — and stop. The old stay-open-until-confirmed loop is retired (Craig, 2026-07-18): the digest plus the two-option offer is the whole interaction, and the close runs by default. The only way the workflow stays open is Craig explicitly saying not to close.
-Each action dispatches to the owning source plugin's =Actions= verb (trash, mark-read, star, respond, merge, comment, attachment-fetch). The engine doesn't hardcode action commands — it reads them from the loaded plugins. Read each plugin's =Actions= section for the exact command.
+*** KB capture (only if the sweep surfaced something durable)
-After actions complete, write the Phase A capture into the sentinel's *content* (see "Capture the Phase A timestamp"): =echo "$PHASE_A_TS $(date -d "@$PHASE_A_TS" '+%Y-%m-%d %H:%M:%S %z')" > .ai/last-triage-intake=. Do not use plain =touch= (writes mtime to /now/ and strands items posted between Phase A and end of run) and do not use =touch -d "@$PHASE_A_TS"= (correct timestamp but mtime is per-machine — won't survive a fresh clone or cross-machine sync).
+If this sweep surfaced a durable, cross-project fact — a recurring pattern across sources, a reference pointer worth keeping, an environment gotcha — consider writing it to the agent KB as one =:agent:= node (see the best-practices node and =knowledge-base.md=; personal projects only, work never writes). One line of judgment, not a step: an all-quiet sweep surfaces nothing and writes nothing. Never blocking, never padded onto a no-signal run.
-*Do not close the workflow yet.* See Exit Criteria below.
-*** Exit Criteria
+* Auto mode (unattended monitoring)
+
+Auto mode is a self-running variant of the engine for when Craig is away from the desk but wants tight awareness — a loop that runs the standard sweep on a short interval, *accumulates* findings rather than mutating state, and hands Craig a gated checkpoint to commit the batch. It composes two things: the *delivery* (a =/loop= in the live session) and the *behavior* (accumulate-don't-mutate sweeps with a checkpoint). The one-shot run above is unchanged; auto mode is an additional way to run the same Phase 0 / A-D engine.
+
+** Trigger and delivery
+
+- "auto triage" / "auto triage-intake" / "watch the desk" / "monitor the triage" — start auto mode.
+- Default interval *20 minutes*; Craig sets it.
+
+Auto mode runs as a =/loop= in the *live session*, not a detached cron job:
+
+#+begin_src
+/loop 20m run an auto-mode triage-intake sweep per triage-intake.org
+#+end_src
+
+Running in the live session means MCP auth (Slack, Gmail, Linear) is inherited from the session — the headless-auth wall that blocks a detached cron run does not apply. A durable cross-session schedule is out of scope here; that belongs to the morning-ops orchestrator, which can later invoke auto mode's accumulate behavior as its triage limb. The close/stop commands below require a live session by design.
+
+*** Phone delivery — push each signal sweep via =agent-text=
+
+Auto mode exists for when Craig is away from the desk, so a sweep that surfaces something worth seeing is delivered to his phone, not just printed into a session he isn't watching. After a sweep that renders the full three sections — one with real deltas or an unacked-list change (see "End-of-sweep output" below) — send that same output to his phone over Signal with =agent-text=:
+
+#+begin_src bash
+agent-text "$SWEEP_SUMMARY"
+#+end_src
-The workflow stays open until Craig has *explicitly* either:
+The pushed text is the *fuller* three-section shape, not a terse one-liner: the per-source deltas, the responses-awaiting-acknowledgment list, and the timestamp, led by a ⚠ SCAN FAILED banner if any source failed.
-1. *Confirmed* that the executed actions are sufficient and nothing more is needed this round, or
-2. *Handed back a different plan* (e.g., "actually hold the PR merges, address #131 first").
+*Signal-only — never on a quiet sweep.* An empty sweep (the =triage intake at HH:MM: nothing= heartbeat) does *not* push to the phone. Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=) governs the phone channel too, so the phone stays silent until a sweep has real signal. The in-session heartbeat still prints as proof the loop ran; the phone is reserved for something that actually needs Craig. (Craig's ruling, 2026-07-20: the higher-cost channel doesn't buzz with "nothing.")
-A successful Phase D run is *not* an exit signal. After the action batch returns, surface what shipped and wait. Don't volunteer "done" or "all set" — those are exit-claim phrases that pre-empt Craig's call. Use a status report ("17 actions succeeded, sentinel written at 12:19") and stop.
+If =agent-text= isn't on =PATH=, fall back to inline delivery and say so once.
-If Craig has been silent for a while after Phase D and the surface looks closed-out, *ask*: "Anything else on this triage, or are we good to close out?" Don't auto-terminate.
+*Reply polling is deferred.* The send half ships here; polling the phone for Craig's replies (the =phone-recv= half of the retired ntfy design) waits on the reply-correlation follow-up. With the Signal account linked on more than one device, a reply fans out to every device and neither knows which page it answers — that has to be resolved before auto mode reads replies back. Until then auto mode pushes but does not poll, and Craig acts on a pushed summary from wherever he picks it up.
-This rule prevents the failure mode where the workflow self-declares done and the next exchange has to relitigate what state things are in.
+** Preconditions and Close-out
+
+Auto mode borrows the inbox monitor-mode gates (=inbox.org= monitor mode): do not start on a dirty worktree or a red test suite — a close's batch commit would otherwise sweep up unrelated changes — and leave the tree clean and green when the loop stops. Surface a blocker with inline numbered options per =interaction.md= and wait.
+
+** A sweep: accumulate, don't mutate
+
+Each sweep runs Phase 0 (load *both* plugin dirs — the loud requirement still holds) and Phases A-D's scan / classify / synthesize, but performs *none* of the normal run's mutations:
+
+- Does NOT advance the sentinel. The scan window grows from the last *close* until the next close: every sweep scans from the existing sentinel up to now, so nothing between sweeps is dropped.
+- Does NOT write =todo.org= Action tasks — accumulates them for the close.
+- Does NOT take mail actions (trash / mark-read / star).
+- Does NOT commit.
+- DOES update an active daily-prep in Update mode and re-open it on change (per =daily-prep.org=).
+- DOES report, deltas-only, with loud scan-failure banners (Phase C rules unchanged).
+
+** End-of-sweep output — three sections, or one heartbeat
+
+*Silent-until-signal (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=).* An *empty sweep* — no deltas since the previous sweep and no change to the awaiting-acknowledgment list — collapses to a single heartbeat line and nothing else: =triage intake at HH:MM: nothing= (HH:MM local, from =date=). Detection still runs in full (Phase 0 plus the A-D scan, against the session's inherited MCP auth); only the output collapses, so a long unattended run stops filling the session with identical "no changes" blocks. A sweep with real deltas or an unacked-list change prints the full three sections below, and — when away — pushes them to Craig's phone via =agent-text= (see "Phone delivery" above). The empty-sweep heartbeat is never pushed.
+
+1. *Deltas* — what changed since the *previous sweep* (the standard Phase C summary scoped to the inter-sweep delta).
+2. *Responses awaiting your acknowledgment* — every Slack reply, email, or message directed at Craig that he hasn't acknowledged or had the agent answer. A *running list carried forward across sweeps* until Craig acks each item or closes the triage. An away user's first need is "who's waiting to hear back from me," which a delta-only sweep loses the moment it scrolls past.
+3. *Timestamp* — the current date, time, and timezone on the sweep's own final line, so an away reader sees how fresh the summary is without computing it. Print it on every sweep that prints these three sections. On an *empty* sweep there is no separate timestamp line — the heartbeat (=triage intake at HH:MM: nothing=) is itself the freshness stamp and the proof the loop ran. Generate it with:
+
+ #+begin_src bash
+ date "+%A %Y-%m-%d %H:%M:%S %Z (%z)"
+ #+end_src
+
+** The unacked list — durable state
+
+The awaiting-acknowledgment list lives in =.ai/triage-intake-unacked.org=, so it survives a session crash, a =/clear=, or a restart — the away-from-desk case auto mode exists for. It's project-local state, tracked the same way as the sentinel (=.ai/last-triage-intake=), created on first need.
+
+Shape — one =** = heading per awaiting item:
+
+#+begin_example
+#+TITLE: Triage Intake — Responses Awaiting Acknowledgment
+# Maintained by triage-intake auto mode. One heading per item; acked items are removed.
+
+** Dana — 2pm reschedule invite
+:PROPERTIES:
+:SOURCE: personal-calendar
+:LOCATOR: <event id or thread url — the dedupe key>
+:SINCE: 2026-06-15 10:42
+:END:
+She's waiting on a yes/no to the move.
+#+end_example
+
+- *Add* — a sweep appends any new directed-at-Craig response not already listed, deduped on =LOCATOR=.
+- *Carry forward* — every sweep re-renders the full list in its second section, whether or not it changed this sweep.
+- *Ack* — "ack <item>" (e.g. "ack the Dana thread") removes that heading; "ack all" clears the list.
+- *Close* — a close empties the list as part of processing (each item is either actioned or filed).
+
+** Close and stop — the checkpoint
+
+The mutations are gated behind two commands:
+
+- *"close the triage"* — run the full close per Phase D: take the accumulated mail hygiene, file the accumulated TASKS items to =todo.org=, reroute if asked ("close the triage and reroute"), empty the unacked list, then *advance the sentinel* — capture the close run's Phase A timestamp, do the mutations, write that timestamp to =.ai/last-triage-intake= exactly as a normal run does (per "Capture the Phase A timestamp") — and commit + push the batch. Then *keep looping* (next sweep on the normal interval). This is the "flush the batch and carry on" checkpoint.
+- *"stop the triage"* — the same close processing, then *stop the loop* and revert to manual (on-demand) triage.
+
+A close is the only point auto mode advances the sentinel or commits. Between closes the engine state is untouched — that is what makes a 20-minute sweep cheap and non-destructive, and it preserves the engine invariant: the sentinel still means "everything before this timestamp has been scanned," it just advances once per close instead of once per run.
+
+** Why a separate mode
+
+The standard engine is one-shot and mutating — right for an at-the-desk "what's new?" glance, wrong for unattended polling: run every 20 minutes it would advance the sentinel past unprocessed items, spray reactive todos, take mail actions, and commit noise without review. Auto mode separates the cheap, frequent *watching* from the deliberate, gated *committing*, and adds the away-user's missing primitive — the running unacked-responses list.
* Reference
@@ -185,7 +307,7 @@ A plugin file declares exactly one source through a fixed shape:
*Body sections:*
- =** Scan= — the command(s) that fetch new/unread items since =<anchor>=, emitting raw items.
- =** Classify= — the source's per-bucket bias and noise patterns. *Deltas* from the engine's shared four-bucket model below, not a re-derivation.
-- =** Render= — the source's block in the Phase C summary. "Omit if empty."
+- =** Render= — the source's classification shape. Since the 2026-07-18 digest format, Render blocks are *inputs to the Phase C synthesis*, not displayed sections — the digest is source-agnostic (TASKS / FYI / MISC). Keep the shape: it defines what the source considers reportable, and the long-form breakdown (on request) still uses it.
- =** Actions= — the executable state-changes, one verb per line: =verb :: command template (parameterized by item id)=.
Template:
@@ -263,33 +385,37 @@ If both fail, fall through to the resolution order above (prep doc → session f
** Output Template
-The summary follows this shape (deltas only: a source with no changes gets no block; when *nothing* changed anywhere, the whole summary collapses to the one-line form below — plus any scan-failure banners and the suggested-actions line if actions are queued):
+The digest follows this shape (deltas only: a source with no changes contributes nothing; when *nothing* changed anywhere, the whole digest collapses to the one-line form below plus the timestamp — scan-failure banners always render regardless):
#+begin_example
17:39 sweep: no changes
#+end_example
-When there are changes, render one block per changed source in =ORDER=, using each plugin's =Render= shape:
+When there are changes:
#+begin_example
-**Anchor:** <previous run timestamp> → now (<elapsed> elapsed)
-**Loaded:** <general plugins> + <project plugins> (skipped: <disabled, with reason>)
+<⚠ SCAN FAILED / backlog banners, if any>
+
+==TASKS==
+- <solo-executable items first, then the rest; priority order within each;
+ one prose bullet each: who, what, why-now, source link>
+
+==FYI==
+- <substantive work context, no action owed; priority order>
-**Top signals to act on:**
-1. <terse Action description with link>
-2. ...
+==MISC==
+- <everything outside the project — personal mail, family, household;
+ priority order; action-ness stated inline>
-<one block per loaded source, in ORDER — see each plugin's Render>
+1. Close the triage — file todo.org tasks for the TASKS items, run the standard close
+2. Close the triage and execute the solo TASKS now, after filing the rest
+Append "and reroute" to either option to send the outside-project items to their owning projects.
+Or tell me what you'd like handled differently.
-**Suggested actions:**
-- Trash N noise items
-- Mark-read M keep items
-- Respond to <invite>
-- Merge PRs #X and #Y
-- ...
+<run timestamp — always the final line>
#+end_example
-Order matters: top-signals first because that's what Craig reads in 30 seconds between meetings. Per-source detail second. Suggested actions last because they require a decision.
+Option 2 appears only when solo-executable TASKS exist; the reroute line only when MISC is non-empty. Sections with nothing in them are omitted. No per-source blocks, no itemized suggested-actions list — the close owns the routine hygiene. The long-form per-source breakdown is available on request only.
** Common Mistakes
@@ -297,11 +423,11 @@ Order matters: top-signals first because that's what Craig reads in 30 seconds b
1. *Globbing only =.ai/workflows/= and missing the project plugins.* The single most damaging failure mode — the sweep runs with half its sources and the omission is invisible (a missing source looks identical to a quiet one). Phase 0 globs *both* =.ai/workflows/triage-intake.*.org= and =.ai/project-workflows/triage-intake.*.org=, every run, and announces the loaded set.
2. *Running Phase A sequentially.* Send every enabled source's scan in one message — the whole point is parallelism.
3. *Wiring a source into the engine.* Sources live in plugin files, never here. If you find yourself editing this file to add an account, repo, or channel, stop — write or edit a =triage-intake.<source>.org= plugin instead.
-4. *Executing actions without explicit confirmation.* Phase D runs only after Craig says "yes" or picks a subset.
+4. *Leaving the triage open, or re-asking about the routine hygiene.* The close is the default next action after the digest (Craig's 2026-07-18 ruling — no exceptions unless he says hold). Mail hygiene (trash/mark-read/star) runs at close without itemized confirmation. What still needs explicit confirmation: anything sending prose under Craig's name, and destructive actions beyond mail hygiene (branch deletes, PR merges).
5. *Forgetting to set the sentinel at the end.* Without it, the next run re-scans the same window.
6. *Using mtime instead of content for the sentinel.* Plain =touch= writes /now/ to mtime, stranding items posted between Phase A and end of run. =touch -d "@$PHASE_A_TS"= fixes the time but mtime is per-machine — git tracks content, not metadata, so the anchor doesn't survive a clone or cross-machine sync. Always write the epoch into the file's *content*.
7. *Running this alongside daily-prep.* Daily-prep already does this as Phase 3 — don't duplicate.
-8. *Mixing Action and FYI in the top-signals list.* Top signals = Action only. FYI lives in the per-source detail.
+8. *Mixing sections.* TASKS = work items needing Craig only; work FYIs never appear there. Outside-project items always land in MISC, even when they carry an action. Solo items lead TASKS; priority order inside every section.
9. *Reporting a failed or skipped scan as a quiet source.* A hung receive, a dead daemon, or a skipped spin-up looks identical to "no new messages" in the output unless it's flagged. The 2026-06-10 sweep shipped with Signal silently missing because the scan hung on an account lock. Failures lead the summary, in their own banner line.
10. *Rendering a per-source quiet roll-call.* "Calendar — quiet" / "PRs — nothing new" lines on every silent source bury the one change that matters and pad a no-change sweep into a report. Deltas only: changed sources get blocks, unchanged sources get nothing, and an all-quiet sweep is one line (Craig's 2026-06-11 ruling in Phase C).
@@ -314,6 +440,15 @@ Update the engine as the orchestration pattern evolves; update a plugin as its s
*** Updates and Learnings
+**** 2026-07-20: Phone delivery for signal sweeps (=agent-text=, send half)
+Auto mode now pushes a full-three-section sweep to Craig's phone over Signal via =agent-text=, the away-from-desk delivery the retired ntfy design carried before ntfy was torn down (2026-07-04). Transport is =agent-text= (the renamed Signal pager), not ntfy. Signal-only by Craig's 2026-07-20 ruling: a quiet sweep's =nothing= heartbeat never reaches the phone — silent-until-signal governs the phone channel too, so the higher-cost channel only fires when a sweep has real signal, while the in-session heartbeat stays as proof the loop ran. Falls back to inline when =agent-text= is absent. Only the send half ships; reply polling (the old =phone-recv=) waits on the reply-correlation follow-up, because a Signal reply fans out to every linked device and neither knows which page it answers.
+
+**** 2026-07-18: Three-section digest + close-by-default (Phase C/D rewrite)
+Craig's ruling after a 42h-gap sweep where the long-form report (top signals + per-source breakdown + 7-option action menu) was followed by "summarize the notable items" — and the digest that answered it was the report he wanted first. Phase C now renders ==TASKS== (work items needing Craig, solo-executable first, priority order) / ==FYI== (work context, no action owed) / ==MISC== (everything outside the project, actions stated inline), then exactly two options (close-and-file / close-and-execute-solo, the latter only when solo items exist), timestamp last. Per-source blocks and the itemized action menu are gone from the default surface (long form on request). Phase D became the close: it runs as the next action no matter what Craig replies (unless he explicitly holds), includes the mail hygiene on every scanned account without itemized confirmation, files the TASKS, clears resolved unacked items, advances the sentinel, and tears down started services. Solo = mechanical + standing-approved + no prose under Craig's name; prose sends and destructive non-mail actions stay gated. The stay-open-until-confirmed exit loop is retired. Same-day addendum: the "and reroute" modifier ("1 and reroute") — MISC items are surfaced-only by default (never filed to this project's todo.org); appending the modifier delivers each outside-project item to its owner's inbox via inbox-send per the cross-project rule.
+
+**** 2026-06-15: Auto mode (unattended monitoring)
+Added a self-running mode for when Craig is away but wants tight awareness — a =/loop= in the live session running accumulate-don't-mutate sweeps with "close the triage" / "stop the triage" as the gated checkpoint. Born the morning Craig cleared his day for a family emergency and wanted the desk watched while in and out. Design decisions (work-project proposal, ratified by Craig 2026-06-15): the unacked-responses list is durable in =.ai/triage-intake-unacked.org= (survives a crash/clear, the away-from-desk case it exists for); the sentinel advances only at close, preserving the scanned-before invariant; delivery is an in-session loop so MCP auth is inherited (a detached cron schedule belongs to the morning-ops orchestrator, not here, because of the headless-auth wall); it stays a mode of this engine, distinct from but reusable by that orchestrator. Same-day addendum (work, 2026-06-15): each sweep ends with a date/time/timezone stamp on its own final line (printed on quiet sweeps too, as proof the loop ran) so an away reader gauges freshness at a glance.
+
**** 2026-05-01: Initial creation
Extracted from daily-prep's Phase 3 pattern as a standalone, lightweight, between-meetings sweep.
@@ -327,7 +462,7 @@ The sentinel is checked into git, but git tracks content, not mtime — so an mt
Craig, via the work project's same-day handoff: "we only need to report if anything's changed when we do triage intake." Sweep summaries report deltas only — a new invite, a new/moved/cancelled event, a new message needing attention. Unchanged sources get no block (the "Calendar — quiet" roll-call is retired), and an all-quiet sweep renders as a single "HH:MM sweep: no changes" line. Failures keep their loud banner (never folded into the no-change line) and the suggested-actions line stays when actions are queued. Same ruling: the telegram plugin's dev-community group traffic is dropped from reports entirely unless Craig asks (see that plugin's 2026-06-11 note).
**** 2026-06-10: Loud failure surfacing (Phase C item 0 + Common Mistake 9)
-Craig: "highlight any failures in daily triage loudly. I get important communication from all these channels." Trigger: the 2026-06-10 sweep shipped with Signal silently missing — a standalone receive hung on the account lock while the signel daemon owned it, and the failure looked identical to a quiet source. Failures now lead the summary in a ⚠ SCAN FAILED banner; the telegram plugin's failure path points at this rule.
+Craig: "highlight any failures in daily triage loudly. I get important communication from all these channels." Trigger: the 2026-06-10 sweep shipped with Signal silently missing — a standalone receive hung on the signal-cli account lock while another client held it, and the failure looked identical to a quiet source. Failures now lead the summary in a ⚠ SCAN FAILED banner; the telegram plugin's failure path points at this rule.
**** 2026-05-26: Refactor into engine + source plugins
Split the monolithic workflow into a source-agnostic engine (this file) and per-source plugins named =triage-intake.<source>.org=. The engine carries the anchor/sentinel logic, the four-bucket model, the Phase A-D orchestration, the todo.org persistence convention, and the exit criteria. Each source's scan/classify/render/action knowledge moved to its own plugin. General plugins (personal-gmail, personal-calendar, cmail, github-prs) live in =.ai/workflows/= and are template-synced; project-specific plugins (a work project's Linear, work Gmail, work Slack, enterprise PRs) live in the project's =.ai/project-workflows/= and are never synced. Phase 0 globs *both* directories — the loud requirement, because missing the project dir silently halves the sweep. Naming convention: first dot is the engine/plugin boundary, deeper dots reserved for sub-adapters. This removed all DeepSat/Linear specifics from the engine; they become work-project plugins.
diff --git a/claude-templates/.ai/workflows/triage-intake.personal-calendar.org b/claude-templates/.ai/workflows/triage-intake.personal-calendar.org
index bf7d543..b5ee67a 100644
--- a/claude-templates/.ai/workflows/triage-intake.personal-calendar.org
+++ b/claude-templates/.ai/workflows/triage-intake.personal-calendar.org
@@ -1,5 +1,5 @@
#+TITLE: Triage Intake — Personal Calendar Source
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-26
# Source plugin for the triage-intake engine. See triage-intake.org for the
diff --git a/claude-templates/.ai/workflows/triage-intake.personal-gmail.org b/claude-templates/.ai/workflows/triage-intake.personal-gmail.org
index aa0554d..7fb1231 100644
--- a/claude-templates/.ai/workflows/triage-intake.personal-gmail.org
+++ b/claude-templates/.ai/workflows/triage-intake.personal-gmail.org
@@ -1,5 +1,5 @@
#+TITLE: Triage Intake — Personal Gmail Source
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-05-26
# Source plugin for the triage-intake engine. See triage-intake.org for the
@@ -21,10 +21,29 @@ Personal Gmail unread in the inbox since the anchor:
mcp__google-docs-personal__listMessages q="is:unread in:inbox after:<anchor-epoch>" maxResults=100
#+end_src
-⚠ *Express the cutoff as the literal UNIX epoch* — =after:1778856990=, not =after:YYYY/MM/DD=. Gmail's =after:YYYY/MM/DD= operator only supports day resolution; the =YYYY/MM/DD HH:MM:SS= form is NOT valid syntax — Gmail parses the space as a term separator, treats =HH:MM:SS= as a search term that never matches, and returns 0 results, silently masking unread mail. The engine supplies =<anchor-epoch>= because this source declares =ANCHOR: epoch=.
+⚠ *Express every anchor cutoff as the literal UNIX epoch* — =after:1784177122= and =before:1784177122= for the same anchor, never the =YYYY/MM/DD= form. This governs *both* anchored queries: the scan above and the backlog-residue probe below. They must meet at the same instant or mail falls between them permanently. Gmail's day-resolution operators fail two different ways: =after:YYYY/MM/DD HH:MM:SS= is not valid syntax at all — Gmail parses the space as a term separator, treats =HH:MM:SS= as a search term that never matches, and returns 0 results, silently masking unread mail — while =before:YYYY/MM/DD= is valid but excludes the named day entirely, so pairing it with a second-resolution scan leaves the whole anchor day covered by neither query. The engine supplies =<anchor-epoch>= because this source declares =ANCHOR: epoch=.
+
+The rule binds the *anchor* windows only. The date-slice walk below deliberately uses =before:<oldest-full-day-seen>= at day resolution — safe there because consecutive slices overlap and get deduped by message id.
⚠ *Do NOT add =-category:promotions -category:social=.* That filter masked 67 promo+social messages across two runs (2026-05-04, 2026-05-06), both needing a follow-up sweep. Pull the full unfiltered set; the trash-leaning bias in Classify handles promotions and social directly.
+⚠ *The MCP caps at =maxResults=100= and exposes NO =pageToken= parameter.* The response carries a =nextPageToken=, but the tool can't consume it, so a pile over 100 is silently truncated — the tail below the cap never gets classified, and every later anchored sweep skips it (it predates the new anchor). This is exactly how a 300+ backlog accumulated invisibly by 2026-07-08. Two consequences:
+
+- *Never treat a 100-row result as complete.* When a scan returns exactly 100, walk the tail in *date slices*: re-query with =before:<oldest-full-day-seen>= (day resolution), repeat until a page returns fewer than 100, dedupe by message id across slices (the day-resolution boundary overlaps).
+- *Never report =resultSizeEstimate= as a count.* It's unreliable — observed stuck at "201" across three different queries whose real union exceeded 300.
+
+*** Backlog-residue check (every sweep — cheap, mandatory)
+
+The anchored scan is blind to anything unread from *before* the anchor. After it, run one probe for pre-anchor residue:
+
+#+begin_src text
+mcp__google-docs-personal__listMessages q="is:unread in:inbox before:<anchor-epoch>" maxResults=5
+#+end_src
+
+The cutoff is the epoch, matching the scan's =after:<anchor-epoch>= — see the epoch rule above.
+
+If it returns any messages, surface one loud line in the sweep summary: "Backlog: unread predating the anchor exists (N+ shown; date-slice to inventory)" and offer a backlog sweep. Never fold the residue into a quiet sweep — an anchored "no changes" claim is only true for the window the scan saw. (Added 2026-07-08 after ~300 pre-anchor unread accumulated unseen; the probe returns actual messages, so it works where the estimate lies. Shipped with a day-resolution cutoff that hid the entire anchor day; fixed to epoch 2026-07-16 after a home sweep reported the backlog clear while two July-15 messages sat unread.)
+
** Classify
Bias: *trash-leaning* — personal Gmail is high noise volume.
diff --git a/claude-templates/.ai/workflows/triage-intake.telegram.org b/claude-templates/.ai/workflows/triage-intake.telegram.org
index 9caa4e1..1319da5 100644
--- a/claude-templates/.ai/workflows/triage-intake.telegram.org
+++ b/claude-templates/.ai/workflows/triage-intake.telegram.org
@@ -1,5 +1,5 @@
#+TITLE: Triage Intake — Telegram Source
-#+AUTHOR: Craig Jennings & Claude
+#+AUTHOR: Craig Jennings
#+DATE: 2026-06-09
# Source plugin for the triage-intake engine. See triage-intake.org for the
@@ -30,12 +30,27 @@ Telega does not autostart with the Emacs daemon. "Down" is its normal state
unless Craig has Telegram open in Emacs. The scan therefore runs the full
lifecycle every time, never skips because the server is down:
+⚠ *DOWN / not-loaded is the TRIGGER to launch, never a reason to skip or fail.*
+This is the exact mistake two projects (work + home, 2026-07-24) made: they
+probed telega, saw =(telega-server-live-p)= nil or telega not =featurep=, and
+reported =SCAN FAILED: telegram — not loaded= or a silent SKIP — a *blind*
+sweep — instead of running Step 1 to start it. A down or unloaded telega is the
+normal entry state; =(telega t)= both LOADS the package and STARTS the docker
+server (work confirmed: down → =(telega t)= → Ready, 18 chats). So the plugin
+MUST run Step 1's launch whenever telega is down/unloaded, wait for Ready, then
+scan. =SCAN FAILED= is reserved for a launch that was actually ATTEMPTED and did
+not reach Ready (image missing, server crash on start, daemon unreachable) —
+never for the pre-launch down state itself. The =:ENABLED:= guard above tests
+whether telega is INSTALLED (=fboundp=), not whether the server is up; a down
+server never disables the source.
+
1. Record prior state: TELEGA_WAS_RUNNING via (telega-server-live-p).
2. Launch (only if not running):
emacsclient -e "(progn (setq telega-use-docker t) (telega t) 'started)"
- The setq is mandatory defense: tdlib segfaults outside docker mode
- (2026-06-09), and Craig's daemon currently has telega-use-docker nil.
- Wait ~2s for Ready, then (telega--loadChats 'main) until telega--chats
+ The setq is mandatory defense: tdlib crashed in native mode when this was
+ set up (2026-06-09) — a separate matter from the SEGFAULT gotcha, which is
+ about the loadChats argument — and Craig's daemon defaults to nil.
+ Wait ~2s for Ready, then (telega--loadChats '(:@type "chatListMain")) until telega--chats
is populated.
3. Check messages: the maphash unread scan in ** Scan Step 2 (filters the
messageContactRegistered join-notice noise).
@@ -48,10 +63,13 @@ lifecycle every time, never skips because the server is down:
Verify: telega-server-live-p → nil, no zevlg/telega-server container in
docker ps. If Craig had it running, leave it untouched.
-If any lifecycle step fails (docker image missing, server crash, daemon
-unreachable), the sweep reports it as SCAN FAILED at the top of the summary
-per the engine's failure rule — never as a silent skip. Craig gets real
-traffic here.
+If any lifecycle step fails *after the launch was attempted* (docker image
+missing, server crash on start, daemon unreachable, Ready never reached), the
+sweep reports it as SCAN FAILED at the top of the summary per the engine's
+failure rule — never as a silent skip. This does NOT cover the ordinary
+pre-launch down state: a down server means "run Step 1," not "SCAN FAILED."
+Craig gets real traffic here, so a blind sweep that skipped the launch is worse
+than a clean failure — it hides real unread messages behind a false all-clear.
** Scan
@@ -85,22 +103,58 @@ TELEGA_WAS_RUNNING=$(emacsclient -e "(and (fboundp 'telega-server-live-p) (teleg
*** Step 1 — start (docker mode) if not already running, wait for Ready
#+begin_src bash
-# `(telega t)` starts without popping the root buffer. Docker mode (the stable
-# path — see the SEGFAULT gotcha) reconnects the persisted ~/.telega session in
-# ~2s. Then load the main chat list so telega--chats populates.
+# `(telega t)` starts without popping the root buffer. Docker mode reconnects the
+# persisted ~/.telega session in ~2s. Then load the main chat list so
+# telega--chats populates.
+#
+# The `(setq telega-use-docker t)` is mandatory and must come BEFORE `(telega t)`:
+# tdlib crashed in native mode when this was first set up (2026-06-09), and the
+# daemon's default is nil unless something (e.g. an Emacs-config :custom) has
+# already forced it. It was missing here while the Quick Reference required it —
+# a session that started telega without it on a native-mode daemon would take the
+# untested path. Match the Quick Reference exactly.
+#
+# Note this is a SEPARATE concern from the SEGFAULT gotcha below: that gotcha is
+# about the `loadChats` argument, and the deaths it explains happened in docker
+# mode. Docker mode is not a defense against it, and it is not evidence for
+# docker mode. Keep both.
emacsclient -e "(progn
+ (setq telega-use-docker t)
(unless (and (fboundp 'telega-server-live-p) (telega-server-live-p)) (telega t))
'started)"
# Poll until Ready with chats synced, or a crash/timeout. Background this with an
# until-loop so the wait doesn't block; exit on Ready-with-chats OR an abnormal
# server exit. Then force a chat-list load if the hash is thin:
-emacsclient -e "(progn (ignore-errors (telega--loadChats 'main)) (ignore-errors (telega--loadChats 'main)) 'loaded)"
+# NOTE: the chat-list argument must be a TL object, not the symbol 'main.
+# `telega--loadChats' puts it straight into the request as :chat_list, and a
+# bare symbol kills the server outright (see the SEGFAULT gotcha below).
+#
+# The liveness check on the tail is the load's only failure signal. `ignore-errors'
+# catches nothing here, because a bad argument kills the server process rather than
+# signalling in elisp, so without this the call returns 'loaded either way.
+# The `fboundp' guard matches Step 0: if the launch failed outright telega is not
+# loaded, and that should read as 'server-died like any other failure rather than
+# signalling void-function.
+emacsclient -e "(progn (ignore-errors (telega--loadChats '(:@type \"chatListMain\"))) (ignore-errors (telega--loadChats '(:@type \"chatListMain\"))) (if (and (fboundp 'telega-server-live-p) (telega-server-live-p)) 'loaded 'server-died))"
#+end_src
On a persisted session telega reaches status "Ready" within ~2s; the chat list
loads over a few more. If =(hash-table-count telega--chats)= is 0 or thin,
re-issue =telega--loadChats= and poll until it stabilizes.
+⚠ *=server-died= is SCAN FAILED, never a quiet account.* A server that dies
+during the load leaves a thin =telega--chats= hash, and a thin hash reads exactly
+like an account with little unread. That is the same false all-clear the
+down/not-loaded rule exists to prevent, arriving one step later in the lifecycle.
+It also fits the SCAN FAILED definition above: the launch was attempted and did
+not hold. So on =server-died=, report SCAN FAILED rather than scanning, and never
+report a low unread count from that run.
+
+This is the independent evidence the SEGFAULT gotcha asks for when it says to
+treat a short chat list as a real short list. Without the check there is no way
+to tell the two apart, which is how the =loadChats= crash stayed invisible
+through two investigations.
+
*** Step 2 — read unread, classified by last-message type
The single most important filter: =messageContactRegistered=. Telegram counts a
@@ -157,24 +211,61 @@ stays non-nil). =telega-server-kill= is what actually stops the server. Call
left in =docker ps=. Skipping this whole branch when =TELEGA_WAS_RUNNING= is t is
the point of Step 0: never tear down a session Craig is actively using.
-⚠ *SEGFAULT GOTCHA — crashes are spontaneous; treat server death as routine.*
-The dockerized =telega-server= (=zevlg/telega-server:latest=, image built
-2026-06-04, tdlib 1.8.64) SIGSEGVs (exit 139) *on its own*, minutes-to-hours
-into a session — 11 host coredumps between 2026-06-09 and 2026-06-11, several at
-times when no triage verb was running. The 2026-06-11 investigation reproduced
-the crash-free verbs and the spontaneous deaths side by side: coredump
-backtraces show a corrupted stack (memory corruption in the musl build), and
-no newer image exists upstream. Earlier theories — "native mode is the trigger",
-"toggle-read is the trigger" — were timing coincidences; the verbs are sound.
+⚠ *SEGFAULT GOTCHA — this was our bug, not tdlib's. Root-caused 2026-07-28.*
+=telega-server= dies with =Unexpected char 'm' in plist value= followed by
+=Assertion failed: false (telega-dat.c: tdat_plist_value: 500)=. The cause was
+this workflow: Step 1 called =(telega--loadChats 'main)=.
+
+The chain. =telega--loadChats= is a raw TL wrapper — it drops its argument into
+the request as =:chat_list= with no conversion. =telega-server--send= then
+=prin1='s the whole plist, and =telega--tl-pack= passes atoms through untouched,
+so the symbol goes out on the wire bare as =main=. The C parser
+(=server/telega-dat.c=, =tdat_plist_value=) accepts only =(=, =[=, ="=, =-=, a
+digit, =t=, =:=, or =n= to start a value. It hits =m=, prints that line, and
+calls =assert(false)=, which aborts the process. The =m= in the error is
+literally the first character of =main=.
+
+The symbol shorthand is real but belongs to a different layer:
+=telega-filter.el= and =telega-folders.el= convert =(eq cl-fspec 'main)= into
+='(:@type "chatListMain")=. The raw TL layer never does. telega's own callers
+always pass the object (=telega.el:290=, =telega-tdlib-events.el:516=).
+
+Proved by experiment, not inference (2026-07-28): from a live Ready server,
+=(telega--loadChats 'main)= killed it within seconds and added one coredump,
+with that exact assertion; a restart plus =(telega--loadChats '(:@type
+"chatListMain"))= survived three consecutive calls with no new coredump and no
+assertion.
+
+*The previous entry here was wrong and cost real time.* It recorded the deaths
+as spontaneous musl memory corruption and declared "the verbs are sound", which
+sent later investigations at the docker image and tdlib versions instead of at
+this file. The corrupted stack in the backtraces is what an =assert= abort looks
+like, not independent evidence of a memory bug. If crashes are ever seen again
+with *no* triage verb running, that is a genuinely separate cause and needs its
+own investigation — do not reuse the old spontaneous-crash story to explain it.
+
+*This crash kills a scan; it does not silently shorten one.* An earlier draft of
+this section claimed the reported "19 chats of ~50" was truncation caused by the
+bad call. That was wrong, and work disproved it at the wire level on 2026-07-28:
+with the corrected call their count is 19 before the first load and 19 after five
+(four on =chatListMain=, one on =chatListArchive=). Nineteen is the real size of
+that account. The same reading here — 19 stable across three corrected loads —
+was already sitting in the evidence and should have retired the claim before it
+was written down. Treat a short chat list as a real short list unless something
+independently shows the server died mid-sync.
+
+=ignore-errors= around the call never helped — the failure is the server process
+dying, not an elisp signal, so there is nothing for it to catch. That is why the
+death is easy to miss from inside elisp, and why a caller should check
+=(process-live-p (telega-server--proc))= after a load rather than trusting a
+returned value.
Operationally: docker mode stays mandatory (=telega-use-docker= = t; the setq
before =(telega t)= is still the right defense), and *every action batch checks
the server first* — =(process-live-p (telega-server--proc))= — restarting via
-=(telega t)= when dead and re-checking Ready before firing verbs. A mid-sweep
-death is recoverable, not an abort: restart, confirm Ready, resume. Durable-fix
-candidates if the crashing gets worse: pin a pre-2026-06 image digest, build
-=telega-server= natively against tdlib, or report upstream to zevlg with the
-coredumps (=coredumpctl list /usr/bin/telega-server=).
+=(telega t)= when dead and re-checking Ready before firing verbs. Any argument
+handed to a =telega--*= TL wrapper must be a TL object or a plain
+string/number/list, never a bare symbol.
Defense in depth: even if the server does die, the scan still works because it
reads the cached =telega--chats= hash, not a live query. A dead server is
diff --git a/claude-templates/.ai/workflows/work-the-backlog.org b/claude-templates/.ai/workflows/work-the-backlog.org
new file mode 100644
index 0000000..ea3f402
--- /dev/null
+++ b/claude-templates/.ai/workflows/work-the-backlog.org
@@ -0,0 +1,266 @@
+#+TITLE: Work the Backlog
+#+AUTHOR: Craig Jennings
+#+DATE: 2026-07-02
+
+* Overview
+
+The single home for the autonomous task-execution loop: take a set of marked, solo-doable tasks from the project's =todo.org= and work them unattended, each held to the full quality bar, under a fixed safety contract. Spec: =rulesets/docs/specs/2026-06-16-autonomous-batch-execution-spec.org=.
+
+Two callers feed it, differing only in how they build the task set and which session mode they pass:
+
+- The *inbox auto-loop* (=inbox.org= auto mode) chains here after its routing completes, with a tag/priority query, file-only mode, cap 1.
+- The *no-approvals speedrun* preset feeds an explicit ordered list with autonomous-commit + always-push + paging-on, after a pre-flight Q&A that front-loads every decision.
+
+This workflow owns the execution logic — eligibility gate, defer checklist, quality bar, run cap. Callers own input assembly and mode selection. Capture-routing (inbox surfaces) stays entirely in =inbox.org=; this file never reads an inbox.
+
+* When to Use This Workflow
+
+Invoked by its two callers, or directly by phrase:
+
+- *Speedrun triggers:* "speedrun", "no approvals speedrun", "speedrun these: <task set>" — run the no-approvals speedrun preset (below). The word "speedrun" always routes here, even when the phrase also says "no approvals": plain =no-approvals.org= is the general session mode; the speedrun is this workflow's preset over an explicit task set.
+- *Loop caller:* =inbox.org= auto mode chains here after its routing (below). Not phrase-triggered.
+
+Manual fallback: "work the backlog" / "work the backlog with <task set>" — gather the three inputs below (ask for whichever are missing, defaulting to file-only mode; default cap is the list length for an explicit set, 1 for a query) and run the loop.
+
+* Inputs — the caller contract
+
+A caller hands this workflow three things:
+
+1. *A task set* — an ordered list of candidate task headings from the project's =todo.org=. Either an explicit ordered list (speedrun) or the result of a tag/priority query (the loop). The loop does not care how the set was assembled; it receives an ordered list of candidates.
+2. *A session mode* — two orthogonal flags:
+ - *Commit autonomy:* =file-only= (default) or =autonomous-commit=. See "Commit autonomy" below.
+ - *Paging:* on or off. End-of-set only.
+3. *A run cap* — the hard maximum number of tasks to complete this run.
+
+It returns a per-task outcome and a run summary.
+
+* Outcomes — the per-task vocabulary
+
+Every task in the set ends in exactly one of:
+
+- =implemented-committed= — implemented, committed (and pushed per the project's flow) under =autonomous-commit=.
+- =implemented-diff-surfaced= — implemented, diff surfaced, *not* committed (=file-only=).
+- =deferred-VERIFY= — a defer-checklist hit; a =VERIFY= filed naming what's missing or risky.
+- =dropped-by-craig= — removed from the run at the speedrun pre-flight Q&A ("skip this").
+- =skipped-ineligible= — failed the mechanical eligibility gate.
+- =failed= — implementation was attempted and abandoned: the tree is left working (never commit a broken state), the failure is surfaced in the run summary, and the run continues to the next task.
+
+The run summary lists each task with its outcome, plus the remaining set when the cap stopped the run.
+
+* The loop
+
+For the task set, in order, until the run cap is hit:
+
+1. *Eligibility gate* (below). Ineligible → record =skipped-ineligible=, next task.
+2. *Scope read* of the relevant code. Cheap; just enough to run the defer checklist.
+3. *Defer checklist* (below). Any hit → defer: file the =VERIFY= naming the gap and record =deferred-VERIFY= (or, under the speedrun preset, route a quick-question gap to the pre-flight Q&A), next task.
+4. *Implement* under the project's commit discipline: TDD red→green→refactor, then the isolated adversarial review (=publish= Step 1) with its re-review loop, fix all Critical/Important findings, then close the task per =todo-format.md='s completion rules. Decompose into as many logical commits as the change needs — size is not capped. If implementation fails partway, leave the tree working, record =failed=, surface it, and continue to the next task.
+5. *Commit autonomy branch:*
+ - =file-only= → surface the diff, do *not* commit. Record =implemented-diff-surfaced=.
+ - =autonomous-commit= → =/voice personal= on the message, commit individually, push per the project's flow. Record =implemented-committed=.
+6. *Record metrics* for the task (the JSONL append — see Metrics below).
+7. Decrement the cap. At zero, stop.
+
+After the set: if the paging flag is set, fire the end-of-set page (below). Surface the run summary either way.
+
+* Eligibility gate — mechanical, no judgment
+
+A task is autonomous-safe when *both* hold. This layer is a lookup, not a judgment; all the judgment lives in the defer checklist.
+
+1. *Status is =TODO=* — never =VERIFY=, =DOING=, =DONE=, or =CANCELLED=. =VERIFY= marks "awaiting Craig's input"; auto-implementing one defeats the check it represents. The do-not-implement set is safe-by-omission: anything not plainly =TODO= (plus any project-declared "hold" marker) is out.
+2. *Tagged =:solo:=* — the autonomy tag, resolved against the project's priority/tag scheme header in =todo.org= (never hardcoded). =:solo:= carries the hard definition in =todo-format.md=: completable and verifiable without Craig beyond at most one or two quick decisions answerable up front, no design deliberation. A project whose scheme declares a different autonomous-safe tag set overrides the default.
+
+Terminology: *speedrunnable means tagged =:solo:=*. It does not mean =:quick:= or require =:quick:solo:=. The =TODO= status check above is the execution-state gate over that speedrunnable set.
+
+Priority and =:next:= drive *ordering* within the eligible set, not eligibility ([#A] before [#B] before [#C], then the author's ordering). =:quick:= is an effort hint for batching and duration estimates — never a gate.
+
+Task *size* is deliberately absent from this gate. A large but well-specified, decision-free task is in scope and gets decomposed into per-logical-commit chunks during implementation. Size never sends a task away; only *deliberation* or *risk* does (the checklist below).
+
+*No scheme header → don't run.* The gate reads =:solo:= semantics from the project's scheme header; a =todo.org= without one leaves the tag undefined (=todo-format.md= makes the header mandatory). Surface that the header is missing and stop rather than guessing eligibility.
+
+* The defer checklist — act vs file
+
+After the scope read, run each eligible candidate through the checklist. Each item is a concrete, answerable question, not an adjective. *Any* hit — or any "unsure" — defers the task. Only a task that clears every item is implemented.
+
+1. *Test-writability (the keystone).* Can I write the failing test from the task text — plus any decisions gathered up front — without inventing a requirement? *No / unsure* → underspecified. Under the speedrun preset, if the gap is one or two quick answerable questions, route it to the pre-flight Q&A; otherwise file a =VERIFY= noting what's missing. Under the unattended loop, file the =VERIFY= (no one to ask). *Open-ended goals are a specific, recognizable failure of this item:* a task phrased as an absence ("find bugs until none remain," "refactor until nothing worthwhile is left," "clean it up") has no writable acceptance test and so isn't really =:solo:=, even when tagged. Don't guess a stopping point — defer it and note that it needs measurable acceptance criteria (bound the surface, characterization net, dispositioned findings, objective floor — see =todo-format.md='s "Making an open-ended task measurable"). Once those are in the task body, it becomes runnable.
+2. *Data-loss / irreversible / external operation.* Does implementing it require any of: =rm= of non-scratch data, =git reset --hard= / force-push, =DROP= / =DELETE= / =TRUNCATE=, file truncate/overwrite of persisted content, a schema or data migration, any external or shared-state mutation, any credential touch? *Yes* → do NOT implement; file a =VERIFY= naming the risk. This is the hard safety gate; an upfront answer never overrides it without an explicit checkpoint.
+3. *Already-satisfied.* Does the scope read show the desired end-state already holds? *Yes* → file a =VERIFY= noting it and move on. Don't make a no-op change.
+4. *Design deliberation.* Does the task carry an unresolved design question, a "weigh these approaches" with real tradeoffs, or a TBD that isn't a quick factual answer? *Yes* → under the speedrun preset, if it collapses to one or two quick questions, route to the pre-flight Q&A; otherwise file and surface as a =/start-work= candidate. Under the loop, file. The discriminator is *quick-answerable question* vs *deliberation* — never task size.
+
+When genuinely unsure which side a task falls on, defer — a wrong auto-implement costs a revert *and* the next-session correction.
+
+** Filing the deferral =VERIFY=
+
+Every checklist hit files a =VERIFY= in the project's =todo.org=, per =todo-format.md='s VERIFY rules:
+
+- *Dedup first.* If a =VERIFY= sibling for this deferral already exists (a prior run filed it), don't file another — record the outcome as =deferred-VERIFY= with a "previously filed" note and move on. The deferred task keeps its =TODO= status and tags, so without this check every subsequent run would re-defer and re-file.
+- *Placement:* sibling of the deferred task (the deferred task is the trigger) — a =**= task gets its =VERIFY= at =**=, a =***= sub-task gets it at =***= under the same parent, never deeper.
+- *Heading:* carries the question or risk on its own ("VERIFY <topic> — migration touches persisted rows").
+- *Body:* which checklist item hit, what's missing or risky, and what answer or action would make the task runnable. For an already-satisfied hit, the evidence that the end-state already holds.
+
+** Routing a quick-question gap (speedrun only)
+
+Under the speedrun preset, a checklist-1 or checklist-4 hit that collapses to one or two quick answerable questions routes to the pre-flight Q&A instead of deferring (see the preset section below). The discriminator: a *quick question* is a factual or preference pick answerable in one line without weighing tradeoffs ("cap at 5 or 8?", "which config key name?"); *deliberation* is anything that needs tradeoffs weighed, options explored, or code read by Craig. A task needing three or more questions isn't quick-question-gapped — it's underspecified; file the =VERIFY=. Checklist item 2 (data-loss / irreversible) never routes to the Q&A: an upfront answer doesn't override the hard safety gate.
+
+The unattended loop has no one to ask — every hit defers there.
+
+* Per-task quality bar
+
+Autonomy changes who approves, not what quality means. Per task, non-negotiable:
+
+- *TDD* per =testing.md=: red first, green, refactor. The keystone checklist item already proved the failing test is writable.
+- *Verification* per =verification.md=: fresh evidence, full suite green before any commit.
+- *Isolated adversarial review* before every commit, dispatched per the =publish= skill's Step 1 — never an inline self-review, however small the diff. Critical and Important findings block until fixed, and each fix goes back to the *same* reviewer until it approves. Minor findings never earn another round.
+ - *When the review can't reach approval* — three rounds without it, a finding that recurs after being reported fixed, or a =Needs Discussion= verdict — the unattended run has no one to ask. Record the task =failed= with the standing findings in its result, leave the tree working, and continue to the next task. Never commit past a blocking finding because nobody is awake to adjudicate — an unreviewed commit landing overnight is the outcome this gate exists to prevent.
+- *=/voice personal=* on every commit message on the =autonomous-commit= path (or the patterns walked inline if the skill is unavailable), message printed inline so the log shows what landed.
+- *Task closure* per =todo-format.md=: depth-based completion (keyword + =CLOSED:= at level 2, dated rewrite at level 3+).
+- *One logical change per commit.* A large task becomes several commits, not one omnibus.
+
+* Commit autonomy
+
+=file-only= is the default: surface the diff, never commit. =autonomous-commit= is honored only when the project carries the commit-autonomy waiver, read fresh each run — never from memory of past runs or "this project usually allows it."
+
+The waiver lives in the project's =.ai/notes.org= *Workflow State* section as marker lines, the same shape as the workflow markers already there:
+
+#+begin_example
+:COMMIT_AUTONOMY: yes
+:LOOP_MAY_COMMIT: yes
+#+end_example
+
+- =:COMMIT_AUTONOMY: yes= — the project has the waiver. An =autonomous-commit= request (the speedrun preset, or a manual run asking for it) is honored.
+- =:LOOP_MAY_COMMIT: yes= — the *unattended loop caller* may also commit. It requires =:COMMIT_AUTONOMY:= alongside it; the split exists because "Craig-initiated speedrun may commit" and "the recurring loop may commit unattended" are different levels of trust. Without this flag the loop stays =file-only= even when the project holds the waiver.
+
+An absent marker means no. Anything other than a plain =yes= value also means no. The read is one grep of the Workflow State section — a lookup, not a judgment.
+
+*The degrade contract.* When a caller requests =autonomous-commit= and the required marker is missing, degrade to =file-only= and surface it in both the run intro and the run summary: "autonomous-commit requested, no :COMMIT_AUTONOMY: waiver in notes.org — running file-only." Never honor the request without the marker, and never drop to file-only silently — the first commits into a project that didn't opt in, the second hides why nothing got committed.
+
+* Bounding the run
+
+The cap is a hard per-run task ceiling passed by the caller — the kill switch a runaway can't exceed:
+
+- *Loop caller default: 1.* Implement the highest-priority eligible candidate, record, stop; the next tick continues.
+- *Speedrun: the length of the explicit list*, capped at a ceiling — the human bounded the set by naming it.
+
+Even the speedrun stops at the cap and surfaces (and, with paging on, pages) the remainder. The cap bounds task *count*, not cost; a token budget is logged as vNext.
+
+* Context hygiene — auto-flush between tasks
+
+Task boundaries are clean boundaries by construction: the previous task is closed and committed (or filed), nothing is half-edited. When the context window grows heavy mid-run, run the flush skill's *auto mode* between tasks: checkpoint the session anchor with the remaining task set, session mode, and cap in Next Steps (so the resumed context continues the run blind), arm the self-injection (=.ai/scripts/self-inject.sh= via =tmux run-shell -b=), and end the turn. The fresh context resumes from the anchor and works on. Unattended runs only — the keystroke-collision hazard and the full mechanism live in the flush skill.
+
+* End-of-set page
+
+With paging on, fire one page when the set is done or the cap is hit — end-of-set only, never per-task:
+
+#+begin_src sh
+notify info "Page" "<project>: <N> done, <M> remaining — <one-line summary>" --persist
+#+end_src
+
+=--persist= keeps it on screen until dismissed, and =info= is the notification urgency convention (persistent but never crash-scary). The notification fires when the set completes *or* the cap stops the run, either way exactly once. The message carries the project name, the completed count, and the remaining count (with skipped tasks noted in the run summary) so Craig can confirm ready and name the next project in one reply. =notify= is the desktop channel (the "page me" surface); a run that expects Craig to be away also fires =agent-text= with the same message (the Signal phone channel, "text me"). See protocols.org "Reaching Craig".
+
+* Metrics
+
+Each task outcome appends one JSON line to the project's =.ai/metrics/work-the-backlog.jsonl= — git-tracked, append-only, =jq=-queryable. Create the directory and file on the first append. Logging is a side effect only: a failed append surfaces a warning in the run summary but never blocks, reorders, or aborts execution.
+
+One record per task, written at the moment its outcome is decided:
+
+| Field | Meaning |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =ts= | ISO-8601 timestamp of the task outcome |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =run_id= | UUID shared by every record in one run (=uuidgen= at run start) |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =project= | project basename |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =caller= | =loop= / =speedrun= / =manual= |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =task= | the task heading (slug) |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =outcome= | =implemented-committed= / =implemented-diff= / =deferred-verify= / =skipped-ineligible= / |
+| | =dropped-by-craig= / =failed= |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =defer_reason= | =underspecified= / =data-loss= / =already-satisfied= / =needs-deliberation= — set on |
+| | =deferred-verify= records only |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =upfront_decision= | =true= when a pre-flight answer was recorded and used for this task |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =wall_clock_s= | seconds from task start to outcome |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =commit_sha= | committed tasks: the commit SHA (comma-separated when the task decomposed into several); empty |
+| | otherwise |
+|--------------------+-------------------------------------------------------------------------------------------------|
+| =review_findings= | count of =/review-code= Critical + Important findings on this task |
+|--------------------+-------------------------------------------------------------------------------------------------|
+
+The =outcome= slugs map one-to-one onto the outcome vocabulary above (=implemented-diff= is =implemented-diff-surfaced=; =deferred-verify= is =deferred-VERIFY=). Per-run rollups (attempted / completed / deferred / dropped, wall-clock total, findings per commit) are computed at synthesis, not stored per record. The =commit_sha= field is what the synthesis step's corrections signal keys on — whether a later commit reverted or hand-fixed an autonomous one — so never omit it on a committed task.
+
+* Caller: the inbox auto-loop
+
+=inbox.org= auto mode chains here as an explicit second step *after* its routing completes — never as a phase inside inbox processing. When a cycle files new items and Craig answers "run this batch next?" with yes, auto mode invokes this workflow with:
+
+- *Task set:* the eligibility query over the queued/filed items — status =TODO= + =:solo:= per the scheme header, priority-ordered.
+- *Session mode:* =file-only=, paging off. (A project carrying both =:COMMIT_AUTONOMY:= and =:LOOP_MAY_COMMIT:= markers opts the loop into commits — see Commit autonomy above.)
+- *Cap: 1.* The highest-priority eligible candidate runs, gets recorded, and the loop's next tick (or the next yes) continues from there.
+
+The loop has no human at kickoff of each task, so a needs-quick-decisions task defers with a =VERIFY= — the pre-flight Q&A is a speedrun capability, not a loop one. Startup and wrap-up never invoke this workflow.
+
+* Preset: the no-approvals speedrun
+
+The named preset is a label for one flag combination, not a second code path: *explicit ordered list + =autonomous-commit= + always-push + paging-on*, with every approval front-loaded into a single pre-flight step. "No approvals" means all input first, then hands-off — not no input ever. =autonomous-commit= still requires the =:COMMIT_AUTONOMY:= waiver (Commit autonomy above); without it the preset degrades to =file-only= and says so in the pre-flight intro.
+
+When Craig names a task set and says "speedrun":
+
+1. *Gather* the named task set.
+2. *Scope-read and classify* each task against the eligibility gate + defer checklist: *ready* (clears everything), *needs-quick-decisions* (one or two upfront-answerable questions — checklist item 1 or 4), or *drop* (data-loss/irreversible, or deliberation that isn't a quick question).
+3. *Order* the list — priority, then the author's ordering / =:next:=.
+4. *Intro the work* — present the ordered plan: what will run, what was dropped and why, and the batched questions for the needs-quick-decisions tasks.
+5. *Craig answers each question or says "skip this"* — a skip removes the task (recorded =dropped-by-craig=; the task itself stays =TODO=); an answer is recorded so implementation works from the decision, not a guess.
+6. *Run the finalized list autonomously* — no further approvals until done. Cap = the list length (the human bounded the set by naming it), still one commit per logical change, always-push per the project's flow, auto-flushing between tasks when the context grows heavy (see Context hygiene above).
+7. *End-of-set page* with completed + remaining + skipped.
+
+The batch-ask (step 4-5) is one message: each question names its task, puts the recommended answer at item 1 when there is one (per =interaction.md= — inline numbered, no popup), and offers "skip this" as the last option. Before the run starts, write each answer into its task's body in =todo.org= as a dated line — the implementation works from the recorded decision, and the record survives the session. The Q&A fires only under this preset; the loop caller never asks (its decision-needing tasks defer).
+
+*** Per-item disposition rule
+
+For every item the run picks up (this holds for any executing caller, including an auto-inbox-zero run given a standing yes):
+
+- *Feature-level task* → write a spec first (=spec-create=), don't implement directly. The spec is the run's deliverable for that item.
+- *Needs decisions you can't confidently guess* → file it as a =VERIFY= carrying the question (under this preset, one or two quick questions route to the pre-flight Q&A instead).
+- *Well-defined* → implement it, taking the time it needs.
+
+This extends the defer checklist: the checklist decides *act vs file*; this rule decides the *shape* of the act.
+
+* Synthesis: metrics → org-roam KB
+
+Trigger: "synthesize backlog metrics" (optionally a weekly scheduled run). This is the read side of the metrics log — Craig's ask was "gather data and create org-roam articles we can look at later," and this step is the second half. It is read-only over the logs plus exactly one KB write.
+
+1. *Gather the JSONL union.* Discover =.ai/metrics/work-the-backlog.jsonl= across the project roots (dirs carrying =.ai/protocols.org= under =~/code=, =~/projects=, =~/.emacs.d=). Classify each project per =knowledge-base.md= (work-root denylist, never inference) before reading it into the union.
+2. *Enforce personal-only.* A work-classified or unknown project's metrics never enter the KB write — they stay in that project's own log. Report the exclusion per the KB refusal contract: the classification, a one-line redacted summary, and where the data stayed.
+3. *Compute the rollups and trends.* Per run: attempted / completed / deferred (by reason) / dropped / failed, wall-clock total, commits landed, review findings per commit. Trends across runs: completion rate over time, defer-reason distribution, findings-per-commit trend.
+4. *Compute the corrections signal* — the key metric. For each =commit_sha= in the window, check that project's history for a later commit (within ~14 days) that reverts it or carries a fix touching the same files. A clean run is one whose autonomous commits survive untouched; a flagged run is what Craig reviews by hand. This is a cheap proxy, not proof — it flags candidates, it doesn't convict.
+5. *Write one KB node* at =~/org/roam/agents/YYYYMMDDHHMMSS-backlog-metrics-<window>.org= per =knowledge-base.md=: =:agent:metrics:= filetags, a concise title, the rollup table, the trend narrative, and =[[id:...]]= links to prior synthesis nodes so the series is traceable. Pull before writing, commit and push after — the normal KB session discipline.
+
+The KB node is the artifact Craig reads later: "are the runs completing more and getting corrected less?" should read off the trend table without touching raw logs. Synthesis never mutates the JSONL, todo.org, or any project tree.
+
+* Common Mistakes
+
+1. *Implementing a =VERIFY= or =DOING= task.* The gate is status =TODO= only — a =VERIFY= exists precisely because Craig's input is pending.
+2. *Treating =:quick:= as eligibility.* It's an effort hint. =:solo:= is the gate.
+3. *Deferring on size.* A large, well-specified, decision-free task runs — decomposed into logical commits. Size is not a checklist item.
+4. *Guessing past the keystone.* If the failing test isn't writable from the task text, the task isn't ready. Inventing the requirement is the failure the checklist exists to stop.
+5. *Rationalizing through the data-loss list.* "The migration is small" doesn't clear checklist item 2. Enumerated operations defer, full stop.
+6. *Committing in =file-only= mode.* The diff is the deliverable; the commit is Craig's.
+7. *One omnibus commit for the whole run.* Every logical change is its own reviewed commit.
+8. *Skipping =/review-code= or =/voice= because nobody's watching.* Autonomy removes interaction gates, never engineering-discipline gates (same contract as =no-approvals.org=).
+9. *Running past the cap.* The cap is the kill switch; hitting it means stop and surface, even mid-set.
+10. *Paging per-task.* One page, end of set.
+11. *Honoring =autonomous-commit= from memory.* The waiver is the marker line in =notes.org=, read fresh each run. "This project usually allows it" isn't a read.
+12. *Re-filing the same deferral =VERIFY= every run.* The deferred task stays =TODO=, so a run that skips the existing-sibling check spams =todo.org= with duplicates.
+13. *Routing a data-loss hit to the pre-flight Q&A.* Checklist item 2 is the hard gate — an upfront answer never clears it without an explicit checkpoint.
+
+* Living Document
+
+Refine as the dogfooding signal arrives — the metrics log and the corrections-in-next-session signal are the feedback loop. Fold recurring adjustments in rather than accumulating caller-side workarounds.
+
+* History
+
+Created 2026-07-02 as Phase 1 of the autonomous-batch execution spec, reconciling the inbox-zero "Phase E" proposal and the =.emacs.d= speedrun proposal into one execution loop. The auto-inbox-zero execute step in =inbox.org= reverted to routing-only in the same change so this file is the loop's only home. Phases 2-6 (same day) wired both callers, pinned the commit-autonomy waiver markers, fleshed the defer/Q&A/page mechanics, and added the metrics record + KB synthesis step.
diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org
index 139d612..a9a5895 100644
--- a/claude-templates/.ai/workflows/wrap-it-up.org
+++ b/claude-templates/.ai/workflows/wrap-it-up.org
@@ -1,10 +1,10 @@
#+TITLE: Session Wrap-Up Workflow
-#+AUTHOR: Craig Jennings & Claude
+#+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.
+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.
@@ -24,15 +24,53 @@ The wrap-up is complete when:
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.
+5. *Git state is certified clean.* All changes are committed + pushed to all remotes, =git-worktree-gate certify= succeeded at the current HEAD, and the working tree has no staged, unstaged, untracked, submodule, or in-progress-operation state.
+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 cycle) 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
@@ -90,15 +128,15 @@ Replace =DESCRIPTION= with your picked slug. (=AI_AGENT_ID= should be filename-s
If the project has a =todo.org= at its root, run the cleanup script before committing. Two passes, both fast and idempotent: a hygiene pass and an archive pass.
-*** Roam inbox sweep (inbox-zero)
+*** 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-zero.org][inbox-zero.org]] for the claimed set.
+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 inbox-zero's Phase B–C (file each into =todo.org=, then remove them from the shared inbox in a separate roam commit). Report the total count and how many appeared related to this project, per inbox-zero's scan-summary rule.
+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
@@ -121,6 +159,22 @@ Run the report-only variant first if you want to see what would change without w
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
@@ -135,6 +189,16 @@ Preview the moves without writing:
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
@@ -170,9 +234,14 @@ else
followups=".ai/lint-followups.org"
fi
[ -f todo.org ] && emacs --batch -q -l .ai/scripts/lint-org.el \
- --followups-file="$followups" todo.org
+ --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
@@ -204,7 +273,7 @@ For an interactive walk of the judgments mid-day, run =/lint-org todo.org=.
*** Inbox sanity check (surface unprocessed handoffs)
-If the project has an =inbox/= directory, verify it holds nothing but =.gitkeep=, =lint-followups.org= (the lint-org pipeline file the next morning's daily-prep consumes), and any explicitly-deferred =PROCESSED-*= files before the wrap completes. An inbox that arrived at session start with handoffs from other projects, or that received handoffs mid-session, needs the =process-inbox.org= workflow to run and apply its value-gate dispositions. Wrapping with a dirty inbox silently defers the work to next session and accumulates handoff debt that the sender can't see.
+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 =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 an unprocessed 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 \
@@ -213,7 +282,7 @@ unprocessed=$(find inbox -maxdepth 1 -type f \
! -name 'PROCESSED-*' \
2>/dev/null | wc -l)
if [ "$unprocessed" -gt 0 ]; then
- echo "wrap-up: inbox/ has $unprocessed unprocessed item(s). Run process-inbox.org before wrapping, or explicitly defer each item with a one-line reason in the valediction."
+ echo "wrap-up blocked: inbox/ has $unprocessed unprocessed item(s). Run inbox.org process mode before wrapping."
find inbox -maxdepth 1 -type f \
! -name '.gitkeep' \
! -name 'lint-followups.org' \
@@ -222,11 +291,37 @@ if [ "$unprocessed" -gt 0 ]; then
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.
+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 blocked. Process each item through its value-gate disposition, or delete it only when that workflow's rationale authorizes deletion, before continuing.
The check exempts =lint-followups.org= explicitly because lint-org runs earlier in the same wrap-up workflow and writes its judgment items to that file in =inbox/= by design. The file is a pipeline artifact for the next morning's =daily-prep=, not a handoff that needs the value gate.
-This integrates with =process-inbox.org=, which stamps =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section on completion. Wrap-up doesn't double-stamp. It only ensures the inbox carries nothing but the expected pipeline artifacts at session end.
+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)
@@ -406,17 +501,17 @@ Behavior:
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.
+*Hard policy: end every session with an empty =git status=.* The wrap is incomplete while anything remains dirty. There is no deferral exception and no "wrapped with known changes" state: unresolved dirt means the session remains open and wrap-up does not occur.
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) |
+| Pattern | What it is | Recommended action |
|---+---+---|
| 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. |
+| 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, or (d) move to a feature branch if it's longer-running. Do not silently leave dirty. |
**** Per-file flow
@@ -424,18 +519,40 @@ 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).
+3. Apply the action when it is safe and authorized.
+4. Re-run =git status --short= after each follow-up commit until empty.
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
+**** When cleanup cannot be completed
+
+Stop the wrap. Do not deliver the valediction, print =session wrapped.=, drop a teardown/shutdown sentinel, or describe the session as complete. Report:
-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."
+1. Every remaining path and its exact Git state.
+2. What the file is and why the agent cannot safely resolve it alone.
+3. The concrete action or decision Craig needs to provide to make the tree clean.
+
+An explicit decision to keep a file dirty changes the outcome from "wrapping" to "leaving the session interrupted." It never satisfies this workflow.
+
+*** Final clean-tree certificate — hard gate
+
+After all commits are pushed and every leftover appears resolved, run the shared gate:
+
+#+begin_src bash
+gate="$(command -v git-worktree-gate 2>/dev/null || true)"
+[ -n "$gate" ] || gate="$HOME/code/rulesets/claude-templates/bin/git-worktree-gate"
+if [ ! -x "$gate" ]; then
+ echo "wrap blocked: git-worktree-gate is unavailable; install rulesets tooling and retry"
+ exit 1
+fi
+"$gate" certify "$PWD"
+#+end_src
+
+The certificate lives inside the Git directory, so it does not dirty the worktree. It records the exact verified HEAD. A non-zero result is a hard stop governed by "When cleanup cannot be completed" above. Step 5 is unreachable until certification succeeds.
** Step 5: Valediction
-Brief, warm closing. 3-4 sentences max.
+Only after the final clean-tree certificate succeeds, deliver a brief, warm closing. 3-4 sentences max.
Include:
- What was accomplished (specific, not generic)
@@ -444,6 +561,8 @@ Include:
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
@@ -456,8 +575,49 @@ 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 and the final clean-tree certificate succeeded (Exit Criteria 5 — never tear down over unpushed or dirty 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.
+
+*The sentinel is session-scoped.* If certification fails, the =Stop= hook blocks and leaves the sentinel armed on purpose, so a wrap blocked by a dirty tree retries on a later stop without re-running this workflow. It does *not* survive the session: =session-start-disarm.sh= clears it at =SessionStart=, because a wrap that never certified is not a pending teardown once its session is gone. Before that hook existed, an uncertified sentinel sat armed indefinitely and fired in whatever session next reached a clean tree — work's 2026-07-27 11:37 wrap killed the 13:20 session mid-work, and archsetup's sat armed on a live terminal for two days. If teardown is still wanted in a new session, run this workflow again.
+
+*** 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
@@ -469,7 +629,8 @@ Good session. Talk tomorrow.
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.
+10. *Treating "was dirty at session start, still dirty now" as fine* — that's how a forgotten commit from two sessions ago turns into "carryover" for two weeks. Every pre-existing dirty file must be resolved or the wrap remains blocked.
+11. *Calling a blocked cleanup a wrap* — if the strict gate fails, report the paths and needed decisions; do not valedict, certify completion, or tear down.
* Validation Checklist
@@ -479,19 +640,23 @@ Before considering wrap-up complete:
- [ ] The Summary ends with the =KB: promoted N / consulted yes-no= line (promotion check ran)
- [ ] File renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=
- [ ] =.ai/session-context.org= no longer exists
-- [ ] =todo-cleanup.el= ran — hygiene pass + =--archive-done= + =--sync-child-priority= (if =todo.org= exists at project root)
+- [ ] =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
+- [ ] Inbox carries nothing but expected committed or ignored pipeline artifacts (=.gitkeep=, =lint-followups.org=, =PROCESSED-*= prefixes); any untracked inbox delivery was processed before wrap
- [ ] 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
+- [ ] After wrap-up commit + push, =git-worktree-gate certify "$PWD"= succeeded at the current HEAD
- [ ] 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
+- [ ] Pre-existing dirty files resolved (committed / stashed / discarded / moved to a feature branch); otherwise wrap stopped with an actionable blocker report
- [ ] 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
+- [ ] The teardown hook can re-verify the clean-tree certificate before consuming a sentinel
+- [ ] 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)