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