diff options
16 files changed, 1460 insertions, 0 deletions
diff --git a/.ai/session-context.org b/.ai/session-context.org new file mode 100644 index 0000000..e3f539c --- /dev/null +++ b/.ai/session-context.org @@ -0,0 +1,214 @@ +#+TITLE: Session Context — 2026-07-23 +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-23 + +* Summary + +** Active Goal + +Sentry armed for the night on =sentry/2026-07-23-ratio=, hourly. Morning teardown is Craig's. + +** Decisions + +** Data Collected / Findings + +** Files Modified + +** Next Steps + +* Session Log + +** 02:34 — Startup + +Ran startup.org. Rulesets already current; =make install= had nothing new to link; project repo clean and up to date. =.ai/= synced from templates. No prior =session-context.org= — last session (2026-07-20 23:36, signal-pager + triage phone-push) wrapped cleanly. + +Findings surfaced: 13 top-level tasks unreviewed >7 days; roam inbox holds 9 items; 8 unprocessed project inbox handoffs plus =inbox/lint-followups.org=; KB has 101 =:agent:= nodes but no best-practices node resolved and nothing matching "rulesets". Active Reminder from 2026-07-14 (Craig's deep read of the sentry spec) reads stale — sentry has since shipped and dogfooded live in takuzu and archangel. + +Craig's instruction on arrival: finish startup, then begin sentry. + +** 02:35 — Craig-ordered amendments applied before launching sentry + +Two of the eight inbox handoffs are Craig's own orders relayed from the dotfiles session, and both gate a correct sentry run, so they land before the loop starts. The rest of the inbox stays for sentry's own inbox pass. + +Amendment 1 — sentry pass list (=.dotfiles=, 2026-07-21). Sentry never checks email or messengers, and gains a bug-finding pass. The handoff shipped dotfiles' whole edited =sentry.org=, but that copy forked from a pre-silent-until-signal canonical: rsyncing it would have reverted the heartbeat/digest split committed 2026-07-20. Applied their three intended changes onto current canonical by hand instead, and kept the =:TRIAGE_SOURCES:= activation-probe language their Pass 3 rewrite had dropped. + +Reviewed the staged diff before committing and found three defects in my own edit. Pass 11 as Craig worded it ran "the project's linters and suite," which would have violated sentry's own anti-pattern 5 (no per-pass suite run) eleven times a night; it now runs linters and static analysis only and reads the entry baseline's suite result. The Living Document line still said "ten mechanical passes." And the KB-deferral parenthetical called KB promotion "the proposal's eleventh pass," which collided with the real pass 11 — reworded. Suite green (exit 0) before and after. Committed 33949c5, unpushed pending Craig's call. + +** 02:43 — Sentry armed, fire 1 (working) + +Entry gates all passed: =:COMMIT_AUTONOMY: yes=, tracked tree clean, suite green (exit 0, run twice on this content), no prior =sentry/*= branch, zero behind upstream. Branch =sentry/2026-07-23-ratio= created from HEAD. Loop armed hourly at :07 (job ef8eb9d4, session-only, auto-expires in 7 days). Craig's repo working tree belongs to sentry until the morning merge — reclaiming it early means saying "stop sentry". If he has rulesets files open in Emacs, buffers want reverting after the merge. + +Fire 1 digest: + +- P1 roam pull — ran, already up to date. +- P2 inbox zero — ran. Project inbox: 1 of 5 executed (org-drill's memory-sweep completion note, informational, marked PROCESSED + reply sent). 4 park (below). Roam inbox: 9 items, 8 belong to other projects and route at wrap-up, not here; the 1 rulesets item ("every project should have a working and a temp directory") is shipped except for one clause, queued below. +- P3 triage intake — skipped: no project-specific plugin and no =:TRIAGE_SOURCES:= declaration, so no active source. Correct behavior under the activation gate. +- P4 todo cleanup — ran, no-op. +- P5 task audit — deferred to a later fire; takuzu's dogfood note says the full audit is too heavy hourly, and fire 1 already carries the bug-finding load. +- P6 working-files hygiene — skipped: no =working/= directory. +- P7 spec status board — ran. No =DOING= spec with a closed build parent. +- P8 link integrity — ran, and the checker itself turned out to be broken (see P11). +- P9 git health — ran. =main= is 1 ahead of =origin/main= (the amendments commit, unpushed pending Craig's call). Sentry branch correctly has no upstream. No stale branches. +- P10 prep freshness — skipped: no =daily-prep/=. +- P11 bug finding — ran. Rotating area this fire: =lint-org.el= and the synced =.ai/= templates. Three defects verified and filed as graded tasks (commit dc7791d): link resolution against cwd rather than the linted file's directory [#B], todo-format checkers firing on spec files [#C], and the notes.org template tripping four flags in every project [#C]. Two of the three were independently reported by takuzu and smoke tonight, which is what prompted checking them. + +The three bug tasks record their severity x frequency arithmetic in the body, including where the two inputs disagreed, so the grades can be argued rather than just overridden. + +** 02:47 — Finished the park path the fire skipped + +The inbox-boundary Stop hook caught a real gap. Fire 1 wrote the four proposals into the approval queue and stopped there, but =inbox.org='s park path is three things, not one: move the proposal into =working/<slug>/= beside a prepared diff, file a =[#B]= VERIFY carrying the decision package, and reply to the sender. I'd done the queue entry and none of the rest, so the senders were waiting on nothing and the decisions lived only in a session anchor that gets archived. + +Completed all four. Each =working/= dir now holds the original proposal, a =proposed.diff=, and the full proposed file. Both org-file diffs were verified by linting the proposed version: the notes.org template goes from four flags to zero, and the fix turned out to need one thing smoke didn't identify — the =invalid-block= pair isn't the example content confusing the parser generally, it's the literal =** Feature Name or Topic= line inside the block, which org reads as a heading. A comma-escape on that one line clears both. Also found two more column-0 bold lines beyond the two reported. + +Replies sent to takuzu, archangel, and smoke. dotfiles' ack needed nothing back and is marked PROCESSED. Inbox is at zero. + +** 03:35 — Fire 2 (working) + +Lock acquired, branch state verified. Digest: + +- P1 roam pull — ran, already up to date. +- P2 inbox zero — ran, no-op. Project inbox at zero. Roam inbox unchanged at 9; 8 belong to other projects and route at wrap-up, the 1 rulesets item is already queued. +- P3 triage intake — skipped: no active source surviving the mail/messenger exclusion. +- P4 todo cleanup — ran in check mode across all three passes, zero fixes, zero conversions, zero archives. +- P5 task audit — mechanical subset only, per the parked guidance and takuzu's note that the full audit is too heavy hourly. Staleness is 20, up from 13 at startup, which is just tonight's 7 new tasks arriving never-reviewed. Not a defect. +- P6 working-files hygiene — ran, and this fire is the first where its probe fires, since fire 1's park work created =working/=. All four dirs have open backing VERIFY tasks, so nothing to flag. The pass works. +- P7 spec status board — ran. Seven IMPLEMENTED, two READY (inbox-workflow-consolidation, encourage-kb-contribution). No DOING spec with a closed build parent. +- P8 link integrity — ran across todo.org, notes.org, the anchor, and all nine specs. Two findings, both in the docs-lifecycle spec, both prose containing a bare =file:= that org parses as a bracketless link (=file:→id:= in a sentence about converting link types, and =keep-file:-links-through-pilot= in a hyphenated phrase). Org behaving as documented rather than a defect, so a digest line and no task. +- P9 git health — main still 1 ahead of origin, unpushed, awaiting the approval-queue item. Sentry branch correctly has no upstream. No stale branches. +- P10 prep freshness — skipped: no =daily-prep/=. +- P11 bug finding — rotating area this fire: the =.ai/scripts/= shell helpers. Shellcheck clean on all seven of the sentry-critical ones (agent-lock, agent-roster, capture-guard, flashcard-sync, inbox-status, self-inject.sh, session-context-path). One SC2034 pair in task-review-staleness.sh, verified as a false positive — the two names are positional fields in a =read -r= that exist to put =value= in the right slot. Digest line, not a task. + +*** Retraction: the [#B] I filed in fire 1 was wrong + +The main result of this fire is negative. Fire 1 filed a =[#B]= claiming lint-org resolves =file:= links against the process cwd rather than the linted file's directory. It doesn't, and the task is CANCELLED. + +The claimed evidence was that linting the notes.org template from the repo root reports two siblings missing while linting from its own directory doesn't. The first half was never run. Those findings came from the =/tmp= copy I made while preparing smoke's diff, and in =/tmp= the siblings genuinely are missing, so org-lint was right. I compared two different files and read the difference as a bug, then wrote a fix direction for a mechanism I hadn't checked. + +Caught it here only because P8 surfaced =link-to-local-file= again and I went looking for the checker in lint-org.el to fix it. It isn't there — it's org-lint's own, which contradicted my stated fix and forced the retest. Three runs plus a direct =default-directory= probe settled it. + +Correction sent to takuzu, who had been told about it. Their own finding is unaffected and still filed. The useful residue: both =link-to-local-file= and the todo-format checkers are upstream org-lint checkers, so scoping them means filtering org-lint's output, not editing a local checker — which changes the fix direction on the [#C] task too. + +** 04:35 — Fire 3 (working) + +Lock acquired, branch state clean. Digest: + +- P1 roam pull — ran, already up to date. +- P2 inbox zero — ran, no-op. Both inboxes unchanged. +- P3 triage intake — skipped: no active source surviving the mail/messenger exclusion. +- P4 todo cleanup — ran. Hygiene and convert-subtasks no-op; =--archive-done= moved one subtree, the =CANCELLED= lint-org task fire 2 retracted. Committed 962f3c0. +- P5 task audit — mechanical subset. Staleness 19, down one from the archive. +- P6 working-files hygiene — ran. All four =working/= dirs still have open backing VERIFY tasks. Nothing to flag. +- P7 spec status board — ran. Two READY specs, no DOING with a closed parent. +- P8 link integrity — ran across todo.org, notes.org, and all nine specs. Same two known prose false positives in the docs-lifecycle spec, unchanged. No new findings. +- P9 git health — main 1 ahead of origin, unpushed, still queued. No stale branches. +- P10 prep freshness — skipped: no =daily-prep/=, no broken symlinks. +- P11 bug finding — rotating area this fire: the Python scripts under =.ai/scripts/=. All 15 compile; no Python linter on this box, so the pass became a targeted read of =inbox-send.py=, the script this session has exercised hardest. Three defects verified, filed as two tasks (8995016). + +*** The inbox-send finding + +The one that matters: =inbox-send= writes straight to the destination path, and =write_text= truncates on open, so any mid-write failure leaves a zero-byte =.org= in *another project's* inbox. That phantom isn't inert — =inbox-status= counts it, so it trips the receiving project's boundary hook and blocks a turn there over a file with no content and no sender context. Meanwhile the sender saw an error and retries, so the target collects a second one. + +Reproduced end to end with a non-ASCII message under a C locale with UTF-8 mode disabled: send fails, zero-byte file lands in the destination, =inbox-status= there reports it pending. The encoding case is just the trigger I could force; the defect is the non-atomic write, which a full disk or an interrupted process reaches the same way. Graded [#B], fix direction is temp-file-plus-=os.replace= with =encoding="utf-8"= pinned. + +Two smaller ones grouped as [#C]: =send_file= raises an uncaught =PermissionError= traceback because =main= catches only =ValueError= and =FileNotFoundError=, and =discover_projects= doesn't dedupe, so a roots config naming both a parent and its child lists the same project at two indices. + +Notable that this is the first fire where the rotating area produced findings in code the night's own work depended on. Fire 1 read the linter, fire 2 the shell helpers, fire 3 the script that carried every reply I sent. + +** 05:35 — Fire 4 (working) + +Passes 1 through 10 all quiet: roam already current, both inboxes unchanged, todo cleanup no-op across all three passes, all four =working/= dirs still backed by open VERIFY tasks, two READY specs and nothing stuck, the same two known prose false positives in the docs-lifecycle spec, main still 1 ahead and unpushed, no prep dir and no broken symlinks. Staleness 21, up two from fire 3's new tasks. + +P11 rotating area: =claude-templates/bin/= — the launcher and paging scripts. Shellcheck clean on all four. The findings came from reading and from a config-sanity check, and one of them is a live condition on this machine rather than a latent code defect. + +*** ratio's second Signal account has been cold for 8 days + +=signal-cli listAccounts= on ratio warns that messages were last received 8 days ago. The natural read is that the pager channel has gone stale, which would matter — it's the "text me" path. It hasn't. Established by elimination: ratio holds two accounts, =signal-receive.sh= hardcodes the pager (=+15045173983=) as its only target, and the timer's own journal shows it draining that account cleanly every 15 minutes, the last run 2 minutes before the warning printed. So the cold account is Craig's personal number, and nothing on this machine keeps it warm. + +What I can't establish is whether that matters. The session history describes that registration as note-to-self with no push, and Signal's tolerance for a quiet linked device isn't something I verified. Filed as [#C] with the uncertainty stated rather than graded up on a guess. If it does matter, the fix is a second timer instance rather than a code change, since =signal-receive.sh= already takes an account argument — which makes it a dotfiles handoff, that repo owning the unit. Not sent: the content is speculative and a 05:35 handoff asserting a problem I haven't confirmed would land as noise. + +*** agent-text's direct send is unbounded + +The relay path passes =ConnectTimeout=10= to ssh; the local-account path calls =signal-cli send= with no bound. Since agent-text is invoked by agents, a stall blocks the calling turn with no output and no way to tell a hang from a slow send. Not hypothetical contention — the receive timer holds the same account for ~16 seconds every cadence. Filed in the same [#C]. + +** 06:35 — Fire 5 (working, but the bug hunt came up empty) + +Passes 1 through 10 all no-op: roam current, both inboxes unchanged, todo cleanup clean across all three passes, four =working/= dirs still backed, two READY specs, the same two known prose false positives, main 1 ahead unpushed, no broken symlinks. Staleness 22, up one from fire 4's new task. + +P11 rotating area: =hooks/= and =scripts/= — the machine-wide hooks and the repo's own install and maintenance scripts, the last major uncovered surface. *Zero real bugs.* All five shell hooks shellcheck clean, all Python hooks compile, =hooks/tests= present, =hooks/__pycache__= correctly gitignored with nothing tracked. Six shellcheck findings across =scripts/=, every one dispositioned as a non-bug: + +- =SC2094= (read and write the same file in one pipeline) in =install-ai.sh= and =sweep-gitignore-tooling.sh= — false positive both times. The pattern is a =>>= append with a =[ -s "$gi" ]= stat inside the group. Appending doesn't truncate, and a stat isn't a content read, so the leading-blank-line logic is correct in both the file-exists and file-absent cases. +- =SC2088= (tilde doesn't expand in quotes) in =doctor.sh= and =audit.sh= — both are display strings printed to the user, not paths used for I/O. =audit.sh= says so in a comment on the line above. +- =SC2295= (unquoted expansion inside =${..}=) in =audit.sh= and =diff-lang.sh= — technically correct, no live trigger; =$HOME= carries no glob characters. +- =SC2164= (=cd= without =|| exit=) in =lint.sh= and =status.sh= — =status.sh= already guards its path upstream. =lint.sh= is genuinely unguarded and, since it sets =-u= but not =-e=, a failed =cd= would let it lint the invocation directory instead of the repo root. Confirmed =cd ""= fails rather than silently succeeding, so the hazard is real in shape but needs the running script's own parent to be unreachable. Not reachable in practice; a one-line =|| exit= would close it if anyone touches the file. + +This is the first fire whose hunt found nothing, which is the expected shape — takuzu's dogfood report predicted the rotating hunt goes quiet after the first few fires clear the standing defects. Recording the area covered so the next fire doesn't re-read it. + +*** Two measurement errors this fire, both caught before they became findings + +Worth flagging as a pattern, since fire 1's retraction was the same class. First, a probe loop passed =--convert-subtasks --check= through an unquoted variable; zsh doesn't word-split, so it arrived as one argument and =todo-cleanup= printed a bare =normal-top-level()=. That looked like a tool crash and was my loop. Protocols warns about exactly this. + +Second, chasing that, I read =exit=0= from a bad-flag run and nearly filed a silent-pass defect — a cleanup tool that exits 0 on a bad flag would let =wrap-it-up= believe a pass ran when it didn't. The =0= was =tail='s exit code, not emacs'. Measured without the pipeline, both =todo-cleanup= and =lint-org= exit 255 on an unknown flag and on a missing file, and 0 on success. No defect. + +Three times tonight a conclusion came from a bad measurement. Twice it was caught in the same fire; once (fire 1) it reached a filed task and a sent handoff before the retest caught it. + +** 07:35 — Fire 6 (working) — the night's most consequential finding + +Passes 1 through 10 all no-op again: roam current, inboxes unchanged, todo cleanup clean, working dirs backed, two READY specs, the same two prose false positives, main 1 ahead unpushed, no broken symlinks. Staleness 22, flat. + +P11 rotating area: =languages/= and the bundle install machinery — the last major uncovered surface. Not quiet. + +*** The python and typescript bundles ship no secret-scan hook + +=bash=, =elisp=, and =go= each carry =githooks/pre-commit=, =claude/hooks/validate-*.sh=, =claude/settings.json=, and a seed =CLAUDE.md=. =python= and =typescript= carry none of the four. The pre-commit hook is the credential scanner, so installing either of those bundles gives a project no secret scan on commit — while README's Bundle structure section documents all four as what every bundle follows. + +Verified live rather than inferred, by scanning every project with =.claude/rules/=: =work= (python) and =clock-panel= (python + typescript) both have no =githooks/= and no settings. All four elisp projects have both. =work= is the one that matters — a work repo is where a leaked credential costs most and is likeliest to reach a company remote. + +The history settles intent. Both bundles were added 2026-05-31; =go='s githooks landed 2026-06-02 and =bash='s 2026-06-23. The rollout swept the bundles added *after* these two and skipped these two. And =install-lang.sh= guards its copy with =[ -d "$SRC/githooks" ]=, so the install succeeds and reports nothing missing, which is how this stayed invisible for almost two months. + +Filed [#A], SCHEDULED today — the only [#A] of the night. The fix is find-not-fix per the pass contract, so nothing was changed. The task carries a second half worth as much as the port itself: make =install-lang= warn when a bundle lacks a component the README documents, so the next partial bundle announces itself rather than installing quietly. + +*** Same drift shape, smaller + +=bash='s pre-commit has =cd "$REPO_ROOT" || exit 1=; the =go= and =elisp= copies of that line dropped the guard. Impact is genuinely low — git chdirs to the working-tree root before running a hook, so the checks run against the right tree regardless, and neither script sets =-e=. Filed [#C] for the two-character fix, mostly because the shape is the same as the [#A]: a fix landed in one bundle copy and stopped there. + +Two fires' worth of evidence now says bundle-to-bundle propagation is where this repo leaks changes. + +* Sentry approval queue (2026-07-23) + +Five items. Each names what, why, and the exact edit. Items 2 through 5 are also filed as =VERIFY [#B]= tasks in =todo.org= with prepared diffs under =working/=, so they survive this anchor being archived — say "approve the parked <topic>" for any of them. + +** 1. Push main to origin + +What: =git switch main && git push origin main= (then switch back, or leave main checked out if sentry is done). + +Why: commit 33949c5 (the two .dotfiles amendments) is on main and unpushed. The sentry spec called this out — an unpushed commit on main diverges across ratio and velox and breaks the next startup fast-forward. It's ahead-only, so the push is clean. Held because =commits.md= requires explicit confirmation before any push. + +** 2. interaction.md — remove the fenced-code-block carve-out (org-drill) + +What: in =claude-rules/interaction.md=, the "No Reverse-Video Highlighting in Chat Output" rule currently says fenced code blocks "are acceptable when the user explicitly wants a block to copy". Replace that sentence with a plain-text-always statement covering fences as well. + +Why: org-drill relayed Craig's 2026-05-30 direction ("always always list it out without markup"), saved there as a project memory. The rule as written contradicts it, and the directive belongs in the shared rule rather than one project's memory. It's a convention change to an always-on rule, so it parks rather than lands. + +Note: this session violated the tightened form of the rule several times already (fenced blocks in chat), which is evidence for the proposal rather than against it. + +** 3. sentry.org Living Document — fold in the two dogfood runs (takuzu, archangel) + +What: append to sentry.org's pass list and notes: (a) make the rotating-angle bug hunt an official pass — already done tonight as pass 11, so this reduces to noting takuzu's corroboration; (b) add randomized property sweeps as a sanctioned quiet-fire activity; (c) note that =todo-cleanup --archive-done= touches =.gitignore= on its first archive, so an "org-only" pass can produce a real commit and trigger the fire-end suite; (d) note that in a project gitignoring =.ai/=, quiet fires produce zero commits, so =git log main..sentry/*= understates the night and the anchor's heartbeat list is the only record; (e) replace the per-fire full task audit with a mechanical subset hourly plus judgment items queued once nightly. + +Why: two independent first-live-run reports, no engine defects in either. Item (e) matches what fire 1 did by instinct (P5 deferred). sentry.org is a shared synced asset, so the edit parks. + +** 4. notes.org template — clear the four lint flags (smoke) + +What: in =claude-templates/.ai/notes.org=, rephrase the two column-0 =**bold**= lines so neither starts with =**=, and comma-escape the =#+begin_example= block's own markers. Then =scripts/sync-check.sh --fix=. + +Why: filed as a [#C] bug task tonight with the reproduction. The edit itself parks because it changes a template every project inherits. Related finding worth Craig's attention: those two flags are mechanical, not judgment, so =lint-org --fix= run anywhere would rewrite the template and create drift against canonical. + +** 5. wrap-it-up.org — delete temp/ at wrap (from Craig's own roam capture) + +What: add a cleanup sub-step to =.ai/workflows/wrap-it-up.org= that removes the project's =temp/= contents during teardown. + +Why: Craig's roam inbox item asks for exactly this ("temp is ... deleted as a part of the wrap up sequence"). The rest of that capture shipped on 2026-07-20 (=working/= tracked from creation, =temp/= gitignored, the sweep backfilling it), but =wrap-it-up.org= has no mention of =temp/= at all, so this clause was missed. Parks as both a shared-asset edit and a destructive one. + +** 02:35 — Craig-ordered amendments applied before launching sentry (earlier) + +Amendment 2 — KB personal roots (=.dotfiles=, 2026-07-21). =~/.dotfiles= classified Unknown under =knowledge-base.md='s personal-roots list, blocking KB writes from that project twice (the sentry pass-list rule tonight, the xdg-desktop-portal gotcha on 2026-07-04). Grepped for other copies of the enumeration: =knowledge-base.md:25= is the only place that lists roots for *classification*. The other three hits (=triggers.md=, =broadcast.org=, =session-harvest.org=, =work-the-backlog.org=) enumerate roots for project *discovery*, a different question, and =~/.dotfiles= reaches those through the machine-local =~/.claude/inbox-roots.txt=. Edited the one line. diff --git a/inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org b/inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org new file mode 100644 index 0000000..340262a --- /dev/null +++ b/inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org @@ -0,0 +1,5 @@ +#+TITLE: Memory-sweep handoff (2026-06-10) done in org-drill. Counts: +#+SOURCE: from org-drill +#+DATE: 2026-07-21 00:06:44 -0500 + +Memory-sweep handoff (2026-06-10) done in org-drill. Counts: 0 promoted to KB / 1 kept local / 0 deleted. The single memory (no-markup-chat-output — never use code fences, bold, or backtick spans in chat) is cross-project, but its durable home is interaction.md rather than a KB node; a separate proposal to fold it into that rule follows in your inbox. Also processed your 2026-06-12 priority-scheme handoff: scheme section added to org-drill's todo.org (derived from existing usage, pending Craig's ratification). One deviation: no commit — org-drill gitignores todo.org per its code-project convention. diff --git a/inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org b/inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org new file mode 100644 index 0000000..ffd2335 --- /dev/null +++ b/inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org @@ -0,0 +1,5 @@ +#+TITLE: Ack from dotfiles: both amendments verified in the rulesets +#+SOURCE: from .dotfiles +#+DATE: 2026-07-23 02:41:58 -0500 + +Ack from dotfiles: both amendments verified in the rulesets canonical. knowledge-base.md line 25 now lists ~/.dotfiles as a personal root, and claude-templates/.ai/workflows/sentry.org carries the amended overview line, the pass-3 mail/messenger exclusion, and pass 11 bug finding. Good call applying the sentry edits by hand instead of rsyncing my copy — I hadn't noticed my rewrite dropped the :TRIAGE_SOURCES: activation probe, and a straight copy would have taken the heartbeat/digest split out with it. I've updated the dotfiles memory to record that the canonical owns this now, so no local re-apply after a sync. Nothing needed back. @@ -39,6 +39,102 @@ Tags are assigned and refreshed by =task-audit=; =task-review= keeps them honest * Rulesets Open Work +** TODO [#A] Python and TypeScript bundles ship no secret-scan hook :bug: +SCHEDULED: <2026-07-23 Thu> +The =python= and =typescript= language bundles carry rules, a coverage script, and a gitignore fragment. They carry no =githooks/pre-commit=, no =claude/hooks/=, no =claude/settings.json=, and no =CLAUDE.md=. The =bash=, =elisp=, and =go= bundles carry all four. + +The pre-commit hook is the secret scanner. So a project installing the Python or TypeScript bundle gets no credential scan on commit, no validate-on-edit hook, and no settings — while README's "Bundle structure" section documents all four as what each bundle follows, and notes.org describes the bundles as "rules + hooks + settings". + +Live as of 2026-07-23, verified by scanning every project carrying =.claude/rules/=: =work= (python) and =clock-panel= (python + typescript) both have no =githooks/= and no =.claude/settings.json=. The four elisp projects all have both. =work= is the one that matters — a work repo is where a leaked credential is most costly and most likely to reach a company remote. + +Not a design choice. Both bundles were added 2026-05-31; =go='s githooks landed 2026-06-02 and =bash='s 2026-06-23, so the hook rollout swept the two bundles added *after* these and skipped these. =install-lang.sh= guards its copy with =[ -d "$SRC/githooks" ]=, so the install succeeds silently and reports nothing missing — which is why this stayed invisible for nearly two months. + +Grading: Major severity (a documented security control absent, silently, with the install reporting success) x every user, every time (every install of either bundle, standing on 2 of the 6 bundle-using projects) = P1 = [#A]. + +Fix direction: port =githooks/pre-commit= to both bundles, adapting the language-specific half (the secret scan is common; =gofmt=/=shellcheck= becomes a formatter/linter check per language), add =claude/hooks/validate-*.sh= and =claude/settings.json=, and seed =CLAUDE.md=. Then make the gap loud: =install-lang= should warn when a bundle lacks a component the README documents, so the next partial bundle announces itself instead of installing quietly. Re-run =make install-lang= on =work= and =clock-panel= afterward. + +** TODO [#C] Two language bundles' pre-commit hooks lost the cd guard :bug:quick:solo: +=languages/bash/githooks/pre-commit= line 8 reads =cd "$REPO_ROOT" || exit 1=. The =go= and =elisp= copies of the same line read a bare =cd "$REPO_ROOT"=. Three siblings of one file, one hardened and two not — the guard landed in bash and never propagated. + +Low impact, stated honestly: git chdirs to the working-tree root before running a hook, so if =cd= fails the cwd is already correct and the checks still run against the right tree. Neither script sets =-e=, so a failure wouldn't abort them either. It takes a =GIT_WORK_TREE= or bare-repo edge case for =rev-parse --show-toplevel= to disagree with the cwd at all. + +Grading: Minor severity (belt-and-braces guard, no silent-pass path found — the checks run regardless) x rare edge case (needs a git configuration that makes toplevel differ from the hook's cwd) = P4... graded [#C] rather than [#D] because it's a two-character fix in a security-relevant gate and the drift pattern is the real signal: a fix landed in one bundle copy and stopped there, which is the same shape as the [#A] above. + +** VERIFY [#B] Parked: zero markup in chat output, fences included (from org-drill) +What arrived: interaction.md's no-reverse-video rule carves out fenced code blocks as acceptable "when the user explicitly wants a block to copy"; org-drill relays your 2026-05-30 direction that chat output carries no markup at all, fences included. +Recommendation: accept as-is. The carve-out contradicts the direction, and the rule is where it belongs so every project inherits it instead of it living as one project's memory. I kept the rule's factual claim honest — fences genuinely don't invert the way inline spans do, so the new text says they read as markup you didn't ask for rather than inventing a rendering problem. Checked: this is the only copy of the carve-out. +Prepared diff: [[file:working/interaction-no-markup/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked no-markup rule" (or adjust / reject) and it gets applied. + +** VERIFY [#B] Parked: sentry Living Document updates from two dogfood runs (from takuzu + archangel) +What arrived: both projects ran sentry live overnight 2026-07-20/21 and reported back — takuzu 8 fires, archangel 7. No engine defects in either. +Recommendation: accept with changes. Four of the five findings fold in cleanly: the archive-done .gitignore note, the mirror-only "git log understates the night" note, property sweeps as quiet-fire work, and splitting the task audit into a mechanical hourly subset plus a nightly judgment half. The fifth (make the bug hunt an official pass) already landed tonight as pass 11, so it reduces to recording their corroboration. Takuzu's lint-org finding I handled separately as a filed bug task rather than a workflow edit, since it's a defect not a design change. +Prepared diff: [[file:working/sentry-living-document/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked sentry updates" (or adjust / reject) and it gets applied. + +** VERIFY [#B] Parked: clear four lint flags in the notes.org template (from smoke) +What arrived: the synced notes.org template trips misplaced-heading twice on column-0 bold lines and invalid-block twice around the Pending Decisions example, so every project inherits the noise on every sweep. +Recommendation: accept as-is, and I found two more instances smoke didn't list plus the actual cause of the block flags (the example contains a literal =** Feature Name= line that org reads as a heading, breaking block tracking). Verified: the proposed file lints with zero mechanical and zero heading/block findings. Worth your attention separately — those two flags are mechanical, not judgment, so =lint-org --fix= run in any project would rewrite the template and drift it from canonical. +Prepared diff: [[file:working/notes-template-lint/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked template fix" (or adjust / reject) and it gets applied. + +** VERIFY [#B] Parked: clear temp/ during wrap-up teardown (from your roam capture) +What arrived: your roam inbox item asked that every project carry working/ and temp/, and that temp/ be "deleted as a part of the wrap up sequence". The rest shipped 2026-07-20; wrap-it-up.org never got the temp/ clause and has no mention of the directory. +Recommendation: accept as-is. Added as a Step 3 sub-step after the archive pass, with two guards — confirm before deleting anything that reads as in-progress rather than throwaway (that belongs in working/, so move it), and skip in a project where temp/ isn't gitignored. It parks rather than lands because it's both a shared-asset edit and a destructive one. +Prepared diff: [[file:working/wrapup-temp-cleanup/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked temp cleanup" (or adjust / reject) and it gets applied. + +** TODO [#B] inbox-send leaves a phantom empty handoff in the target inbox :bug:solo: +=inbox-send.py= writes straight to the destination path in the target project's =inbox/=. =Path.write_text= opens with mode =w=, which creates and truncates before any content is written, so *any* failure between opening and finishing leaves a zero-byte =.org= file sitting in another project's inbox. + +That file is not inert. =inbox-status= counts it as a pending handoff, so it trips the receiving project's =inbox-boundary-check= Stop hook and blocks a turn there. The receiving agent then has to resolve a handoff with no content and no sender context, which it cannot do from the file. Meanwhile the sender saw an error and will most likely retry, so the target gets a second file too. + +Reproduced 2026-07-23 end to end: a send whose text contains non-ASCII under =LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0= fails on the ASCII codec, leaves a zero-byte file in the destination inbox, and =inbox-status= in that project then reports it as pending. + +The encoding case is one trigger, not the defect. =write_text= and =read_text= are both called with no =encoding= argument, so they follow the locale; passing =encoding="utf-8"= closes that trigger. The defect underneath is the non-atomic write, which a full disk, a revoked permission, or an interrupted process reaches just as easily. + +Grading: Major severity (a phantom handoff in a *different* project's inbox, unresolvable from its own content, that blocks a turn there via the boundary hook) x some users, sometimes (any mid-write failure, of which the locale case is only the one reproduced) = P2 = [#B]. + +Fix direction: write to a temp file in the destination directory and =os.replace= into place, so the inbox only ever sees a complete file. Pin =encoding="utf-8"= on both =write_text= and the =read_text= in =resolve_roots=. Same treatment for =send_file=, whose =shutil.copy2= has the same shape. Test by forcing a mid-write failure and asserting the destination directory is unchanged. + +** TODO [#C] Two Signal-channel gaps: unbounded send, second account never received :bug: +Both found 2026-07-23 reading =claude-templates/bin/agent-text= and =scripts/signal-receive.sh=. Shellcheck is clean on all four =claude-templates/bin/= scripts; these are logic gaps, not style. + +1. *The direct send has no timeout.* =agent-text='s relay path passes =-o ConnectTimeout=10= to ssh, but the local-account path calls =signal-cli send= with no bound at all. A stalled network or a lock contended by the receive timer hangs the call indefinitely. That matters more than it looks: agent-text is invoked *by agents*, so an unbounded call blocks the calling turn with no output, and the caller can't distinguish a hang from a slow send. The receive path already takes ~16s of wall clock every cadence, so the two contending on the same account is not hypothetical. Fix: wrap the direct send in =timeout=, matching the relay's bound, and let the existing non-zero path print the desktop-fallback message. + +2. *ratio's second Signal account is never received.* =signal-cli listAccounts= on ratio warns "Messages have been last received 8 days ago". Established by elimination, not inference: ratio holds two accounts (=+15045173983= pager, =+15103169357= Craig's personal), =signal-receive.sh= hardcodes the pager as its default and takes no others, and the timer's journal shows it draining the pager cleanly every 15 minutes (last run 2 minutes before the warning appeared). So the 8-day-stale account is the personal one, and nothing on the machine keeps it warm. + + Honest limit on item 2: the staleness is verified, the *harm* is not. That registration is described in the session history as note-to-self with no push, so it may be fine to leave cold, and Signal's own tolerance for a quiet linked account isn't something I established. Filed so the question gets answered rather than rediscovered. The script already accepts an account argument, so if it does matter the fix is a second timer instance rather than a code change — which makes it a dotfiles handoff (that repo owns the unit) with rulesets owning only the script. + +Grading: Minor severity for both (one is a latent hang in a tool with a documented fallback, the other a warning on an account nothing currently depends on) x most users, frequently (the stale warning is a standing condition on this host; the hang needs a stall) = P3 = [#C]. + +** TODO [#C] Two smaller inbox-send defects: uncaught traceback, duplicate roots :bug:quick:solo: +Both verified 2026-07-23, both in =.ai/scripts/inbox-send.py=, both low-harm. Grouped because they're the same file and the same fix session. + +1. *Uncaught =PermissionError=.* =main= catches =(ValueError, FileNotFoundError)= around the send, but =send_file= reaches =shutil.copy2=, which raises =PermissionError= on an unreadable source. That is an =OSError=, not caught, so the user gets a raw Python traceback instead of the clean =inbox-send: <message>= error every other failure path produces. Reproduced with a =chmod 000= source. No partial file is left in this case. Fix: catch =OSError= alongside the other two. + +2. *Duplicate roots list a project twice.* =discover_projects= appends without deduping, so a roots config naming both a parent and one of its children (=/x= and =/x/proj=) lists =proj= at two different numeric indices. Reproduced via =INBOX_SEND_ROOTS=. Harmless today — both indices resolve to the same project, so no message goes to the wrong place, and Craig's current =~/.claude/inbox-roots.txt= has no overlap. Fix: dedupe on =resolve()= before returning. + +Grading: Minor severity (one is cosmetic output noise, the other an ugly but accurate failure message; neither loses or misroutes a message) x rare edge case (an unreadable source file, or a roots config with an overlap) = P4... graded up to [#C] rather than [#D] because both fixes are one line each and sit in a script every project depends on for cross-project messaging. + +** TODO [#C] lint-org todo-format checkers fire on spec files :bug:solo: +=level2-done-without-closed= and =level-2-dated-header= encode =todo.org= completion conventions, but they run against every org file, including =docs/specs/=. A spec's Decisions section legitimately uses =** DONE <decision>= with no =CLOSED:= cookie, and its Review-and-iteration-history section legitimately uses =** <dated> — <who> — <role>= headings. Neither is a completion defect there. + +Reproduced 2026-07-23 across rulesets' own specs: 100 findings over 7 of the 9 files in =docs/specs/= (docs-lifecycle 27, sentry-workflow 25, autonomous-batch 11, wrapup-routing 11, inbox-consolidation 10, agent-kb 8, encourage-kb 8; the two 2026-07-20 specs are clean). Every project carrying =docs/specs/= inherits the same noise on every sweep. Reported independently by takuzu's first sentry dogfood run. + +Grading: Minor severity (judgment-kind output, so nothing mutates; the cost is noise that trains the reader to skim) x every user, every time (fires on every sweep in every project with specs) = P2... graded down to [#C] because the two inputs disagree: the frequency row is genuinely universal, but Minor severity with zero mutation risk and a known cause reads as P3. Recorded so the read can be argued. + +Fix direction: scope both checkers away from =docs/specs/=, or gate them on the file carrying a =todo.org=-shaped structure. Note (established 2026-07-23) that these are *org-lint's own* checkers, not ones lint-org.el implements, so the fix filters upstream output by file path rather than editing a checker. Takuzu's alternative was to bless =CLOSED:= dates in =spec-create=, which fixes the symptom in new specs and leaves the nine existing ones noisy. + +** TODO [#C] notes.org template trips four lint-org flags in every project :bug:quick:solo: +The synced =claude-templates/.ai/notes.org= carries two boilerplate lines that open with markdown-style bold at column 0 (=**Session history is NOT in this file.**=, =**For protocols and conventions, see:**=). Org parses a line-initial =**= as a level-2 heading, so =misplaced-heading= flags both, and the =#+begin_example= block in the Pending Decisions instructions trips =invalid-block= twice. Every project inherits all four on every sweep. + +Reproduced 2026-07-23. Confirms smoke's proposal. Additional finding from the same run: these two are =mechanical-fixed=, not judgment, so =lint-org --fix= silently rewrites the template's =**bold**= to org =*bold*=. The rewrite is correct org, but it lands on a synced template, so whichever project runs =--fix= first creates drift against canonical. + +Grading: Minor severity (cosmetic noise; the mechanical rewrite is correct org and harmless in isolation) x every user, every time (every project, every sweep) = P2... same disagreement as the checker task above; graded [#C] on the no-real-harm read. + +Fix direction: rephrase both lines in the canonical template so no line starts with =**= (a list dash, or move the bold off column 0), and comma-escape the example block's own markers. One canonical fix clears it everywhere. + ** TODO Manual testing and validation *** Sentry — entry gates fire with Craig present What we're verifying: the interactive entry gates stop for the right states and start the loop only on a clean, green baseline. @@ -1610,3 +1706,12 @@ Origin: the work project's 2026-06-18 fold-in request; preserved bundle [[file:d ** CANCELLED [#D] Fully-unattended scheduled inbox check (/schedule cron pass) :feature: CLOSED: [2026-07-20 Mon] Merged 2026-07-20 into [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract — no-session variant]]. Same no-session design problem as the sentry /schedule variant (mutation rights, async surfacing, cross-run dedup, cron auth context); its inbox-specific context was absorbed there. +** CANCELLED [#B] lint-org resolves file: links against cwd, not the linted file :bug:solo: +CLOSED: [2026-07-23 Thu] +Not a defect. I filed this on a bad comparison and retracted it the same night. + +The claimed evidence was that linting =claude-templates/.ai/notes.org= from the repo root reports =protocols.org= and =workflows/first-session.org= missing, while linting it from its own directory reports neither. The first half of that was never run. The findings came from a =/tmp= copy of the template made while preparing the smoke diff, and in =/tmp= those two siblings genuinely are missing, so org-lint was right. I compared two different files and read the difference as a cwd bug. + +Retested three ways: the real file from the repo root gives zero link findings, the real file from its own directory gives zero, and only the =/tmp= copy gives two. A direct probe confirms =find-file-noselect= already sets =default-directory= to the linted file's directory, so the mechanism I proposed could not have been the cause either. + +Worth keeping from the episode: =link-to-local-file= is org-lint's own checker, not one lint-org.el implements, so a real fix here would mean pre- or post-filtering upstream output rather than editing a local checker. diff --git a/working/interaction-no-markup/interaction.md.proposed b/working/interaction-no-markup/interaction.md.proposed new file mode 100644 index 0000000..caa4d49 --- /dev/null +++ b/working/interaction-no-markup/interaction.md.proposed @@ -0,0 +1,70 @@ +# Interaction Style + +Applies to: `**/*` + +How the agent communicates with the user during a session — choice prompts, status updates, decision points. + +## No Popup Menus for Choices + +When the agent needs the user to pick between options, **do not** use the AskUserQuestion popup. Present the options inline in chat as a numbered list and ask the user to reply with a number. + +**Why:** The popup menu UI sits at the bottom of the chat window and obscures the chat content directly above it — exactly the area the user needs to read to make the choice. Inline numbered options keep the question, the surrounding context, and the proposed text all visible in the same scrollback. + +**How to apply:** + +For approve / changes / cancel flows (commit-message review, PR-description review, plan approval), draft inline: + +``` +1. Approve — commit now +2. Request changes — tell me what to adjust +3. Open in editor — emacsclient -n /tmp/... + +Pick a number. +``` + +For pick-one decisions, same shape: numbered list, one-line prompt at the end. + +For multi-select decisions, say so explicitly: "Pick any combination — reply with the numbers." + +**Render-merge guard.** GFM renderers merge adjacent ordered lists and renumber them — a message with a 1-3 content list followed by a 1-3 options list rendered as options 4-6 on the user's screen, and his pick ("4") didn't exist in the author's numbering (work session, 2026-06-10). Numerals belong to the options list only: any other enumeration in the same message uses dashes or prose, and a short prose lead-in line sits directly above the options so no renderer can merge them. When the user picks a number outside the offered range, suspect the render-merge and ask which item text they meant rather than treating it as an error. + +Reserve `AskUserQuestion` only when the user explicitly asks for the popup form ("use the popup for this one") or for genuinely free-form input where numbered options don't fit. + +This rule applies to all three approval gates in the `commits.md` publish flow (commit message, PR description, PR review reply): print the draft inline, then offer numbered approve / changes / edit options inline. Do not switch to the popup form for the gate even though the prior protocol referenced it. + +### Order options with the recommendation at item 1 + +When the question has a clear recommended option, put it at item 1. The other paths stay visible so the user can override without having to type a custom answer, but the common case collapses to a single keystroke. + +Default to this pattern whenever the user needs to weigh a genuine choice — task-review actions, brainstorm decisions, judgment calls, approve/edit/cancel gates, "what should I do next" prompts. Skip it only when the question is free-form (open prose, names, dates), when the user has already issued a directive and a single confirmation suffices, or when there is no clearly-recommended option (in which case state that plainly: "I don't have a recommendation here — what's your read?"). + +Include a "Skip" / "Defer" / "Tell me how" as the last option when the user's answer might be "none of these." + +The convention reinforces the popup-denial rule above by giving every inline choice list a single canonical shape, and it forces the model to actually pick a recommendation rather than offering a neutrally-ordered enumerate-and-defer list. A neutrally-ordered list is a covert way to push the decision back; recommendation-first puts skin in the game. + +**Enforcement:** a global `PreToolUse` hook (matcher `AskUserQuestion`) in `~/.claude/settings.json` hard-denies the popup and returns this rule as the reason — the prose alone proved too easy to forget. Because the deny is unconditional, the "use the popup for this one" exception above can't be honored in-turn; to get the popup, disable the hook via `/hooks` (or edit settings) first. + +## No Reverse-Video Highlighting in Chat Output + +In conversational output to the user, do not use Markdown bold (`**...**`) or inline-code spans (backtick `` `...` ``). The user's terminal renders both as reverse video, which is hard to read on the display. + +**Why:** The styling that looks like emphasis in most renderers comes through as inverted foreground/background here. Plain prose, plain identifiers, and plain key chords read cleanly; the "highlight" actively hurts legibility. + +**How to apply:** + +- Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s. +- Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine. +- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path. + +This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation. + +## Showing Craig Visuals + +Craig runs Claude Code inside Emacs EAT (through tmux). EAT renders SendUserFile and inline terminal images as an `[image] path.png` text line — the visual itself never appears. In one session ~20 renders went out that way and Craig approved UI he had never seen (takuzu, 2026-07-11). Never rely on SendUserFile or inline image display to show a visual. SendUserFile stays fine for *delivering* a file; it just doesn't display one. + +Two display lanes, by what the visual is for: + +- **Durable or interactive visuals** — HTML prototypes, full renders, anything Craig will study or click: open with `gui-open <abs-path>` (dotfiles-shipped; detaches through `systemd-run --user` so the agent shell can't reap it, resolves the Hyprland instance after a compositor restart, and confirms the window is actually visible before returning). It auto-detects image vs HTML; `--image` / `--browser` force. Place it off Craig's active workspace and name it, per `desktop-capture.md`. +- **Quick inline glances** — a chart, a did-it-render check: sixel in the terminal. Encode with ImageMagick (`magick <img> sixel:<out>`; img2sixel silently emits zero bytes on some builds) and display in a separate tmux window (`tmux new-window -d -n <name>`, then `tmux send-keys -t <name> "clear; cat <out>" Enter`, tell Craig the window name) rather than the Claude Code pane, whose TUI repaints over anything drawn into it. With native tmux sixel active, the image lives in tmux's grid and survives window switches, scrolling, and resizing. + +**Capability gate.** Native sixel needs two config pieces: EAT answering the XTWINOPS cell-size query (patched eat.el, owned by .emacs.d) and `terminal-features 'xterm*:sixel'` in tmux.conf (owned by dotfiles). Check before relying on it: `tmux display -p '#{client_cell_width}'` — nonzero means go; 0 means the chain is missing a piece, so fall back to the browser lane. (Diagnosed 2026-07-13: tmux won't transmit sixel until it knows the client's cell pixel size, and stock EAT 0.9.4 silently ignores the CSI 14 t query tmux uses to ask.) diff --git a/working/interaction-no-markup/proposal-from-org-drill.org b/working/interaction-no-markup/proposal-from-org-drill.org new file mode 100644 index 0000000..09f0845 --- /dev/null +++ b/working/interaction-no-markup/proposal-from-org-drill.org @@ -0,0 +1,5 @@ +#+TITLE: Proposal: sharpen interaction.md's 'No Reverse-Video Highlig +#+SOURCE: from org-drill +#+DATE: 2026-07-21 00:06:48 -0500 + +Proposal: sharpen interaction.md's 'No Reverse-Video Highlighting in Chat Output' rule. Craig's feedback in org-drill (2026-05-30, saved as a harness memory there): chat output must have zero markup — no code fences, no bold, no backtick spans, ever ('always always list it out without markup'). The current rule bans bold and inline-code spans but says fenced code blocks 'are acceptable when the user explicitly wants a block to copy'. Craig's directive tightens that: default to plain text always; no fences either. Suggest updating the rule text so every project inherits it instead of it living as one project's memory. Evidence: org-drill memory file no-markup-chat-output.md and the 2026-05-30 session log there. diff --git a/working/interaction-no-markup/proposed.diff b/working/interaction-no-markup/proposed.diff new file mode 100644 index 0000000..f5d1830 --- /dev/null +++ b/working/interaction-no-markup/proposed.diff @@ -0,0 +1,11 @@ +--- claude-rules/interaction.md 2026-07-19 15:41:37.382769288 -0500 ++++ /tmp/im.md 2026-07-23 02:47:43.771850521 -0500 +@@ -54,7 +54,7 @@ + + - Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s. + - Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine. +-- Fenced code blocks (triple backtick) are acceptable when the user explicitly wants a block to copy — they don't invert the way inline spans do. Default to plain text otherwise. ++- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path. + + This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation. + diff --git a/working/notes-template-lint/notes.org.proposed b/working/notes-template-lint/notes.org.proposed new file mode 100644 index 0000000..311a86f --- /dev/null +++ b/working/notes-template-lint/notes.org.proposed @@ -0,0 +1,86 @@ +#+TITLE: Claude Code Notes - [Project Name] +#+AUTHOR: Craig Jennings +#+DATE: [Date] + +* About This File + +This file contains project-specific information for this project. + +- When to read this: +- At the start of EVERY session (after reading protocols.org) +- When needing project context or history +- When checking reminders or pending decisions + +- What's in this file: +- Project-specific context and goals +- Pending decisions +- Active reminders + +- Session history is NOT in this file. Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5. + +- For protocols and conventions, see [[file:protocols.org][protocols.org]]. + +* Project-Specific Context + +This is where context regarding this project is placed. + +Examples of what goes here: +- Project overview and goals +- People and their relationships to the user +- Contact information for companies and people +- Current state of the project +- Status report +- Links to important documents +- Technical architecture notes +- Key decisions and rationale + +** If this is the first session + +Run the first-session workflow: [[file:workflows/first-session.org][first-session.org]]. It walks through git/.ai policy, project orientation, and initializing this notes.org. Remove this heading once done. + +* PENDING DECISIONS + +This section tracks decisions that need Craig's input before work can proceed. + +**Instructions:** +- Add pending decisions as they arise during sessions +- Format: =** [Topic/Feature Name]= +- Include: What needs to be decided, options available, why it matters +- Remove decisions once resolved (the resolution is captured in the Session Log of the session where it was resolved) + +- Example format: +#+begin_example +,** Feature Name or Topic + +Craig needs to decide on [specific question]. + +Options: +1. Option A - [brief description, pros/cons] +2. Option B - [brief description, pros/cons] + +Why this matters: [impact on project] + +Implementation is ready - just need Craig's preference. +#+end_example + +** Current Pending Decisions + +(None currently - will be added as they arise) + +* Active Reminders + +** Current Reminders + +(None currently - will be added as needed) + +** Instructions for This Section + +When Craig says "remind me" about something: +1. Add it here with timestamp and description +2. If it's a TODO, also add to =/home/cjennings/org/roam/inbox.org= scheduled for today +3. Check this section at start of every session +4. Remove reminders once addressed + +Format: +- =[YYYY-MM-DD]= Description of what to remind Craig about + diff --git a/working/notes-template-lint/proposal-from-smoke.org b/working/notes-template-lint/proposal-from-smoke.org new file mode 100644 index 0000000..26b458a --- /dev/null +++ b/working/notes-template-lint/proposal-from-smoke.org @@ -0,0 +1,5 @@ +#+TITLE: Proposal: clean four lint-org flags in the notes.org templat +#+SOURCE: from smoke +#+DATE: 2026-07-21 06:49:22 -0500 + +Proposal: clean four lint-org flags in the notes.org template (claude-templates/.ai/notes.org). A sentry pass over smoke's synced copy flagged: (1) two 'possibly misplaced heading' judgments on the boilerplate lines that open with org bold at column 0 ('**When to read this:**' and '**What's in this file:**' in the About This File section) — org can parse a line-initial ** as a level-2 heading; rephrasing (a list dash, or moving the bold off line start) would clear it everywhere. (2) two 'possible incomplete block' judgments around the Pending Decisions instructions' #+begin_example block that shows the entry format — likely the example content confusing block parsing; worth a look at whether the example should escape its own markers (e.g. a comma-escaped ,** or ,#+ inside the block). Both are cosmetic — no behavior breaks — but every project inherits the lint noise on each sweep, so one canonical fix beats N local ones. No local edit made in smoke; its copy stays template-faithful. diff --git a/working/notes-template-lint/proposed.diff b/working/notes-template-lint/proposed.diff new file mode 100644 index 0000000..627741f --- /dev/null +++ b/working/notes-template-lint/proposed.diff @@ -0,0 +1,38 @@ +--- claude-templates/.ai/notes.org 2026-07-09 13:57:29.814830969 -0500 ++++ /tmp/nt.org 2026-07-23 02:48:03.165968767 -0500 +@@ -6,19 +6,19 @@ + + This file contains project-specific information for this project. + +-**When to read this:** ++- When to read this: + - At the start of EVERY session (after reading protocols.org) + - When needing project context or history + - When checking reminders or pending decisions + +-**What's in this file:** ++- What's in this file: + - Project-specific context and goals + - Pending decisions + - Active reminders + +-**Session history is NOT in this file.** Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5. ++- Session history is NOT in this file. Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5. + +-**For protocols and conventions, see:** [[file:protocols.org][protocols.org]] ++- For protocols and conventions, see [[file:protocols.org][protocols.org]]. + + * Project-Specific Context + +@@ -48,9 +48,9 @@ + - Include: What needs to be decided, options available, why it matters + - Remove decisions once resolved (the resolution is captured in the Session Log of the session where it was resolved) + +-**Example format:** ++- Example format: + #+begin_example +-** Feature Name or Topic ++,** Feature Name or Topic + + Craig needs to decide on [specific question]. + diff --git a/working/sentry-living-document/feedback-from-archangel.org b/working/sentry-living-document/feedback-from-archangel.org new file mode 100644 index 0000000..b59ce20 --- /dev/null +++ b/working/sentry-living-document/feedback-from-archangel.org @@ -0,0 +1,5 @@ +#+TITLE: Sentry dogfood feedback (first live run, archangel, night of +#+SOURCE: from archangel +#+DATE: 2026-07-21 07:22:17 -0500 + +Sentry dogfood feedback (first live run, archangel, night of 2026-07-20→21): ran clean end to end — 7 fires (1 working + 6 quiet heartbeats), morning teardown + 8-item approval-queue walk all worked as designed. Two findings for the Living Document: (1) todo-cleanup --archive-done can write a TRACKED file from an 'org-only' pass — on first archive it creates archive/task-archive.org and appends a .gitignore entry, so the pass produced a real commit and correctly triggered the fire-end conditional suite; worth a note in the pass list that todo-cleanup's first archive touches .gitignore. (2) In a project that gitignores .ai/, the whole spine is the mirror-only case — quiet fires produce zero commits, so 'git log main..sentry/*' understates the night's activity; the anchor's heartbeat list is the only record. Both are notes, not defects. diff --git a/working/sentry-living-document/feedback-from-takuzu.org b/working/sentry-living-document/feedback-from-takuzu.org new file mode 100644 index 0000000..fe3ddba --- /dev/null +++ b/working/sentry-living-document/feedback-from-takuzu.org @@ -0,0 +1,5 @@ +#+TITLE: Sentry dogfood feedback from its first live overnight run (t +#+SOURCE: from takuzu +#+DATE: 2026-07-21 06:43:55 -0500 + +Sentry dogfood feedback from its first live overnight run (takuzu, 2026-07-20/21, 8 hourly fires): the engine worked as designed end to end — entry gates, per-fire lock acquire/release, branch isolation, digest-vs-heartbeat split, approval queue, morning squash-merge teardown. Findings for the Living Document: (1) a Craig-granted rotating-angle bug-hunt pass found 3 real fixes in the first 4 fires then went honestly quiet — worth considering as an official opt-in pass; (2) randomized property sweeps (generate-and-verify in both engines) made good quiet-fire work beyond the frozen parity corpus; (3) lint-org's todo-format checkers (level2-done-without-closed, level-2-dated-header) fire on spec files' Decisions sections under docs/specs/ — consider scoping those checkers away from spec files or blessing CLOSED dates in spec-create; (4) the per-fire full task-audit pass is too heavy hourly — a mechanical-subset audit with judgment items queued once per night fit better. No engine defects observed; skip semantics and the quiet-fire heartbeat behaved per spec. diff --git a/working/sentry-living-document/proposed.diff b/working/sentry-living-document/proposed.diff new file mode 100644 index 0000000..90c0db4 --- /dev/null +++ b/working/sentry-living-document/proposed.diff @@ -0,0 +1,32 @@ +--- claude-templates/.ai/workflows/sentry.org 2026-07-23 02:39:42.349838885 -0500 ++++ /tmp/st.org 2026-07-23 02:48:24.104591775 -0500 +@@ -131,9 +131,9 @@ + + 3. *Triage intake — mail and messenger sources excluded.* Run =triage-intake.org=, loading only its non-mail, non-messenger source plugins (calendar, PR/ticketing). The mail and messenger plugins — cmail, any Gmail variant, Telegram, Signal, chat DMs — are never loaded by a sentry fire: Craig ruled 2026-07-21 that sentry doesn't check email or messengers. A manual "triage intake" still scans everything. Probe: the project has at least one *active* triage source that survives that exclusion — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources, or whose only declared sources are mail or messengers, probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended. + +-4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. ++4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. Note that =--archive-done= is not purely an org-file pass on its first run in a project: it creates =archive/task-archive.org= and appends a =.gitignore= entry, so it produces a real tracked-file commit and correctly trips the fire-end conditional suite. (archangel, first live run 2026-07-21.) + +-5. *Task audit* — =task-audit.org=. Probe: a root =todo.org=. Priority regrades and consolidations queue for review; factual staleness fixes that are unambiguous execute. ++5. *Task audit* — the *mechanical subset* of =task-audit.org= hourly (staleness counts, structural checks, cookie recomputation); the judgment half (priority regrades, consolidations, merge candidates) runs *once per night* and queues its findings rather than repeating them every fire. Probe: a root =todo.org=. A full audit every hour is too heavy and re-surfaces the same judgment calls all night. (takuzu, first live run 2026-07-21.) Factual staleness fixes that are unambiguous still execute. + + 6. *Working-files hygiene* — flag =working/<slug>/= directories whose backing task is closed (a filing candidate per =working-files.md=). Probe: a =working/= directory exists. The filing itself queues (it's a judgment move). + +@@ -145,7 +145,7 @@ + + 10. *Prep + symlink freshness* — stale daily-prep docs, broken symlinks. Probe: the prep dir / symlinks exist (work and home only, in practice). + +-11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.) ++11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. Randomized property sweeps (generate-and-verify against an engine's own invariants) are good quiet-fire work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few fires find the standing defects; a quiet bug hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four fires, then quiet.) This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.) + + (KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.) + +@@ -178,6 +178,8 @@ + + A bad night is discarded by deleting one branch — nothing reached main, nothing was pushed. + ++In a project that gitignores =.ai/=, the whole spine is untracked, so quiet fires produce no commits at all and =git log main..sentry/<date>-<host>= understates the night's activity. There the anchor's heartbeat list is the only record of what fired. Read the anchor, not just the log. (archangel, first live run 2026-07-21.) ++ + * Stop Sentry + + Trigger: "stop sentry" (and synonyms above). Sentry owns its own shutdown: diff --git a/working/sentry-living-document/sentry.org.proposed b/working/sentry-living-document/sentry.org.proposed new file mode 100644 index 0000000..62eac12 --- /dev/null +++ b/working/sentry-living-document/sentry.org.proposed @@ -0,0 +1,219 @@ +#+TITLE: Sentry — Overnight Hygiene Supervisor +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-19 + +* Overview + +Sentry is an interval loop that keeps a project's hygiene current while Craig is away. Each fire walks a fixed list of hygiene passes — roam pull, inbox zero, triage (no mail or messengers), todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness, bug finding — and commits each pass's writing to a throwaway daily branch. Nothing pushes. In the morning Craig reviews the branch, squash-merges what he wants, and deletes it. + +The design goal is a project that greets the morning already tidy, with every judgment call and every destructive action parked in an approval queue rather than executed unattended. Sentry does the mechanical sweeping; Craig does the deciding. + +This file is the engine. It owns the entry gates, the branch mechanics, the lock model, the per-fire pass runner, the digest and approval queue, the skip semantics, and the stop-sentry shutdown. The =agent-lock= helper (=.ai/scripts/agent-lock=) provides the locks. The passes reuse existing workflows (=inbox.org=, =triage-intake.org=, =clean-todo.org=, =task-audit.org=) under sentry's unattended contract. + +* When to Use This Workflow + +Craig arms sentry at the end of a session, with the machine left running, to have overnight hygiene done by morning. + +Triggers: + +- "start sentry", "run sentry", "arm sentry", "sentry mode" +- "let sentry watch this overnight", "keep this tidy overnight" +- "start sentry hourly", "start sentry every <interval>" (sets the loop interval) + +Stop trigger (see Stop Sentry below): + +- "stop sentry", "stand down sentry", "sentry off" + +Sentry is deliberately *not* auto-armed. Running it in a project is a per-project grant (the =:COMMIT_AUTONOMY:= marker) plus a deliberate launch with Craig at the terminal for the entry gates. + +* Prerequisite — the autonomy ticket + +Sentry commits unattended. =commits.md= gates commits on Craig's approval, so sentry needs standing, per-project authorization to run at all. Before anything else, read the project's =.ai/notes.org= Workflow State block for: + +: :COMMIT_AUTONOMY: yes + +If the marker is absent or not =yes=, decline to start and name the marker: + +: Sentry needs ":COMMIT_AUTONOMY: yes" in .ai/notes.org Workflow State to run — it commits unattended. Add it to grant, or run the hygiene passes by hand. + +No half-running mode: a project without the grant doesn't run sentry's read-only passes either. The grant is one line away, so this is a deliberate opt-in, not a barrier. + +* Entry — interactive, with Craig present + +Craig types the sentry trigger, so the first moves run with him at the terminal. Do them in order; each gate that fails stops entry until Craig answers. + +1. *Autonomy ticket* — the prerequisite above. Absent → decline and stop. + +2. *Dirty-tree gate.* =git diff --quiet HEAD= (tracked modifications only; untracked and gitignored files never block — an inbox drop or scratch file is not in-progress work). If the tracked tree is dirty, describe what's dirty and offer, inline-numbered per =interaction.md=: + + 1. Finish the job — commit the in-progress work first (recommended if it's a coherent unit) + 2. Stash it — =git stash= and start sentry on a clean tree + 3. Roll back named changes — discard specific files (names them) + + Wait for an answer. Sentry can't start unattended from a dirty state; that's the point. + +3. *Green-suite gate.* Run the project's full suite (=make test=, or the project's equivalent — detect it). Read the output. If anything is red, describe the failures and offer to investigate before arming. The loop starts only on a green baseline, because every unattended fire measures itself against "did I break this?" and a pre-existing red poisons that check. + +4. *Prior sentry branch.* =git branch --list 'sentry/*'=. An unmerged =sentry/*= branch from a previous night means the morning review didn't happen. Surface it and offer to squash-merge or delete it now (Craig is present); don't stack a second sentry branch on the first. + +5. *Reconcile the project branch.* Fetch and fast-forward-only against upstream — the same reconcile =startup= runs: + + : git fetch --all --prune + : git rev-list --left-right --count @{u}...HEAD + + Zero-behind → continue. Behind-only and clean → =git merge --ff-only @{u}=. Diverged → surface to Craig (he's present); don't auto-resolve. + +6. *Create the daily branch.* From HEAD: + + : git switch -c "sentry/$(date +%F)-$(uname -n)" + + The host suffix (=uname -n=) stops a same-date collision between the two daily drivers. The working tree now sits on this branch overnight — the launch hands the repo to sentry until the morning merge. Reclaiming it mid-night means stopping sentry first (see Stop Sentry). Note the Emacs buffer-revert caveat to Craig if he has the repo open: files change on disk under him overnight, so buffers want reverting after the morning merge (see =emacs.md=). + +7. *Arm the loop.* Start =/loop= at the interval (default hourly; Craig's "every <interval>" phrase overrides) with the per-fire body being one sentry fire (the Pass Runner below). Confirm the arming in one line: interval, branch name, project. + +* The lock model + +Two locks, both served by =.ai/scripts/agent-lock= (names only; the helper owns the paths, which live on tmpfs under =$XDG_RUNTIME_DIR/agent-locks/=, host-local and cleared on reboot). + +*Single-runner lock* (=sentry-<project>=, where =<project>= is the repo-root basename: =basename "$(git rev-parse --show-toplevel)"= — the same derivation =wrap-it-up.org='s guard uses, so the two agree on the lock name). Each fire acquires it at fire start and releases it at fire end, and refreshes it between passes (the heartbeat, so a live fire's lock never ages past one pass). If =/loop= fires again while a previous fire still holds it, the new fire's acquire fails and the fire skips with one digest line — no two fires run at once. The bounded wait is short (a few seconds); a live fire means defer, not queue. + +*Roam-write lock* (=roam-write=). A pass that edits a file under =~/org/roam= acquires it, runs =capture-guard --wait= (the human-capture layer stays underneath), edits the working tree, triggers =systemctl --user start roam-sync.service=, and releases. The lock spans only edit-plus-trigger. Sentry never runs =git= against =~/org/roam= — roam-sync stays the repo's only committer (the 2026-06-24 one-git-owner rule). Pass 1's =pull --ff-only= is the sole, read-only exception. + +Every reclaim of a stale lock surfaces in the digest — the helper prints the reclaim note, and the fire records it. A reclaim during a genuinely slow pass is possible, so it's never silent. + +* The Pass Runner — one contract per pass + +Each fire, after acquiring the single-runner lock and verifying branch state (below), walks the pass list in order. Every pass follows the same four-step contract: + +1. *Probe* — a cheap existence check for the pass's target (named per pass below). Absent → the pass is one skip line in the digest and nothing more. This is what makes the pass list portable: passes self-activate where their target exists and stay silent elsewhere, with zero per-project configuration. + +2. *Work* — run the pass under the unattended contract. Quick, solo, already-agreed mechanical actions execute. Anything destructive or requiring judgment does *not* execute — it appends to the morning-approval queue (what, why, the exact command or edit that fires on approval). A pass runs fully or not at all; there is no reduced-form pass. + +3. *Session-context entry* — a pass that does or queues work appends its digest line to the =session-context.org= Session Log (path resolved via =.ai/scripts/session-context-path=) before its commit, so a crash between them still leaves the trail. Per-pass lines for an all-quiet fire (every pass probe-skipped or no-op) are not written one by one — the fire collapses to a single heartbeat at fire-end (below), so an idle fire doesn't spray one skip line per pass. + +4. *Commit* — if the pass wrote to disk, commit it: =chore(sentry): <pass> — <what changed>=. One commit per writing pass. A probe-skip or a no-op pass writes nothing and commits nothing. + +Between passes, refresh the single-runner lock (=agent-lock refresh sentry-<project>=) — the heartbeat. + +** Branch-state verification (fire start, before the passes) + +After acquiring the lock, confirm the fire is safe to run: + +- *On the right branch* — HEAD is =sentry/<today>-<host>=. If the loop was armed on a prior day and crossed midnight, the branch keeps the arming date; that's fine, morning teardown handles it. If HEAD is somehow *not* a sentry branch (an interrupted stop, a manual checkout), skip the whole fire with a digest line rather than committing onto main. +- *Clean of foreign changes* — =git diff --quiet HEAD= excluding the spine set (=session-context.org= / =session-context.d/=, resolved via =session-context-path=). Sentry's own spine writes must not trip this; a genuinely unexpected dirty tree (something outside the spine changed and wasn't committed by a prior pass) poisons the fire — skip it with a digest line, the next fire retries. + +* Unattended safety — skip, never degrade + +With no one at the terminal, any unsafe state makes the affected scope skip with one digest line, and the next fire retries. Unsafe states and their scope: + +- *Unexpected dirty tree* (non-spine) → skip the whole fire. +- *Lost or un-acquirable single-runner lock* → skip the fire (another fire holds it, or the helper is missing). +- *A pass's own precondition unmet* (its probe fails, or a dependency is dirty) → skip that pass only. +- *Red suite at fire-end* (see below) → the commits stay on the branch, flagged in the digest for morning review; the fire doesn't roll back. + +Skips are never silent and never partial. Inside a *working* fire, a pass line means the pass fully ran and a skip line names why it didn't. An *all-quiet* fire is not a silent skip either: its single =sentry at HH:MM: nothing= heartbeat is the explicit record that every pass found nothing, standing in for a wall of identical skip lines. The anti-silence rule targets a pass that hides work it should have surfaced; a quiet fire has surfaced that there was none. + +** Multi-day stall notification + +An unmerged prior =sentry/*= branch at fire start (the morning review never happened) skips the fire. After the *second consecutive* fire skipped for this reason, send one persistent desktop notification naming the project and branch: + +: sentry stalled: <branch> unmerged — merge or delete to resume + +Then repeat at most daily. Persistent notify matches the paging convention — it stays on screen until dismissed. A multi-day stall never stays silent. + +* The pass list (v1) + +In order. Each names its detection probe. A pass whose probe fails is one skip line. + +1. *Roam pull* — =git -C ~/org/roam pull --ff-only=. Probe: =~/org/roam= is a git clone. Skipped when the roam tree is dirty (roam-sync owns that case) or the clone is absent. Read-only and ff-only — the one narrow exception to "don't touch roam git," so later passes read a fresh tree. + +2. *Inbox zero* — run =inbox.org= roam mode under the no-approvals contract: quick+solo+agreed items execute, shared-asset and convention proposals park (prepared diff, =VERIFY= task, sender reply) in the approval queue. Edits to =~/org/roam/inbox.org= take the roam-write lock + =capture-guard=. Probe: the roam clone or a project =inbox/= exists. Tidying the shared roam inbox is allowed from *any* project session, work included — it's housekeeping on a shared resource, not a durable KB-node write, so the work-denylist doesn't gate it (=knowledge-base.md=). Never park it as a cross-project boundary crossing. + +3. *Triage intake — mail and messenger sources excluded.* Run =triage-intake.org=, loading only its non-mail, non-messenger source plugins (calendar, PR/ticketing). The mail and messenger plugins — cmail, any Gmail variant, Telegram, Signal, chat DMs — are never loaded by a sentry fire: Craig ruled 2026-07-21 that sentry doesn't check email or messengers. A manual "triage intake" still scans everything. Probe: the project has at least one *active* triage source that survives that exclusion — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources, or whose only declared sources are mail or messengers, probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended. + +4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. Note that =--archive-done= is not purely an org-file pass on its first run in a project: it creates =archive/task-archive.org= and appends a =.gitignore= entry, so it produces a real tracked-file commit and correctly trips the fire-end conditional suite. (archangel, first live run 2026-07-21.) + +5. *Task audit* — the *mechanical subset* of =task-audit.org= hourly (staleness counts, structural checks, cookie recomputation); the judgment half (priority regrades, consolidations, merge candidates) runs *once per night* and queues its findings rather than repeating them every fire. Probe: a root =todo.org=. A full audit every hour is too heavy and re-surfaces the same judgment calls all night. (takuzu, first live run 2026-07-21.) Factual staleness fixes that are unambiguous still execute. + +6. *Working-files hygiene* — flag =working/<slug>/= directories whose backing task is closed (a filing candidate per =working-files.md=). Probe: a =working/= directory exists. The filing itself queues (it's a judgment move). + +7. *Spec status board* — the =docs-lifecycle= grep for spec keywords, surfacing any =DOING= spec whose bound build parent is closed. Probe: =docs/specs/= exists. + +8. *Link integrity* — broken =file:= links in the project's org files, via =lint-org.el=. Probe: =lint-org.el= present. Report-only into the digest; no unattended rewrites. + +9. *Git health* — uncommitted drift, unpushed commits on other branches, stale branches, main-behind-origin. Probe: =.git=. Report into the digest. + +10. *Prep + symlink freshness* — stale daily-prep docs, broken symlinks. Probe: the prep dir / symlinks exist (work and home only, in practice). + +11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. Randomized property sweeps (generate-and-verify against an engine's own invariants) are good quiet-fire work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few fires find the standing defects; a quiet bug hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four fires, then quiet.) This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.) + +(KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.) + +* Fire-end — conditional suite, then the digest commit + +After the passes: + +1. *Conditional suite run.* If any pass this fire modified files *outside* the org/spine set (a code-touching pass, rare but possible via fixtures), run the full suite once. A green run confirms the fire's commits are safe; a red run flags the digest for morning review — the commits stay on the branch (nothing is pushed, so the morning gate catches it). No per-pass suite runs: the entry run is the green baseline, and hourly per-commit runs would turn a seconds-long fire into minutes all night. Fires that only touched org/spine files skip this. + +2. *Heartbeat or digest, then commit.* Decide quiet vs working. A *quiet* fire — every pass probe-skipped or no-op, nothing added to the approval queue — writes a single heartbeat line to the Session Log, =sentry at HH:MM: nothing= (HH:MM local, from =date=), and no per-pass digest block. A *working* fire — any pass ran, wrote, or queued — writes its full per-pass digest block. Then commit any accumulated spine writes in one sweep: =chore(sentry): digest — <date> <time> fire= for a working fire, =chore(sentry): heartbeat — <date> <time>= for a quiet one, so even a quiet fire leaves a clean tree for the next branch-state check (where the spine is untracked, the mirror-only case, there is nothing to commit and the heartbeat line stays in the working-tree anchor). This is the silent-until-signal policy (see =docs/specs/2026-07-20-silent-until-signal-monitors-spec.org=): an all-quiet night collapses from a wall of no-op digests to a list of one-line heartbeats, while a fire that actually did or queued something still writes the full record. + +3. *Release the single-runner lock.* + +* The digest and the approval queue + +*Digest.* A *working* fire appends its block to the =session-context.org= Session Log (the spine the fire already writes), so it survives a crash, rides the session archive, and is on screen in the running session. One block per working fire: the timestamp, then one line per pass (ran + what, or skipped + why), plus any lock reclaim notes. A *quiet* fire (nothing done or queued) writes no block — just the one heartbeat line =sentry at HH:MM: nothing= (the silent-until-signal policy). The per-pass block is a working-fire artifact; it still carries one line per pass so a real skip inside a working fire is never hidden. + +*Approval queue.* Destructive and judgment actions accumulate under one heading in the same file — =* Sentry approval queue (<date>)= — newest last. Each item carries three things: *what* (the action), *why* (what triggered it), and the *exact command or edit* that fires on approval. The morning review is Craig reading this heading top to bottom and running or discarding each item. + +* Morning teardown — Craig's, documented not automated + +Sentry never merges its own branch. In the morning Craig: + +1. Reviews the digest and the approval queue in =session-context.org=. +2. Runs or discards each approval-queue item. +3. Reviews the branch: =git log main..sentry/<date>-<host>= and the diff. +4. Squash-merges what he wants (=git switch main && git merge --squash sentry/<date>-<host>=, then one clean commit) or cherry-picks selectively. +5. Deletes the branch: =git branch -D sentry/<date>-<host>=. +6. Reverts any Emacs buffers still showing the pre-merge on-disk state (=emacs.md= buffer-revert caveat). + +A bad night is discarded by deleting one branch — nothing reached main, nothing was pushed. + +In a project that gitignores =.ai/=, the whole spine is untracked, so quiet fires produce no commits at all and =git log main..sentry/<date>-<host>= understates the night's activity. There the anchor's heartbeat list is the only record of what fired. Read the anchor, not just the log. (archangel, first live run 2026-07-21.) + +* Stop Sentry + +Trigger: "stop sentry" (and synonyms above). Sentry owns its own shutdown: + +1. *Cancel the loop* — stop the =/loop= (=ScheduleWakeup= stop / the loop's stop path). No further fires. +2. *Release the single-runner lock* if this context holds it. +3. *Branch disposition* — offer, inline-numbered: + 1. Squash-merge the day's branch into main now (walk the morning teardown steps 3-5 interactively) + 2. Leave it named for later review (=sentry/<date>-<host>= stays; review at leisure) +4. *Approval queue* — offer to walk the queued items now, or carry them (they stay under the heading for whenever Craig reviews). + +Stopping sentry is the only way to reclaim the working tree mid-night. The entry gate fronts the handoff; stop-sentry ends it. + +* Wrap-up interaction + +=wrap-it-up.org= refuses while sentry is live: it detects the single-runner lock (=agent-lock status sentry-<project>= → held) and stops with "sentry is active — say 'stop sentry' first." The shutdown logic lives here, not in wrap-up; wrap-up carries only the one guard. + +* Common Mistakes + +1. *Running without the =:COMMIT_AUTONOMY:= grant* — sentry commits unattended; the marker is the entry ticket, and its absence is a hard stop, not a degrade. +2. *Starting from a dirty or red tree* — the entry gates exist because an unattended fire can't tell Craig's in-progress work from a regression. Answer the gate; don't bypass it. +3. *Committing onto main* — every writing pass commits to the daily =sentry/*= branch. A fire that finds HEAD off the sentry branch skips rather than commits. +4. *Running a =git= write against =~/org/roam=* — roam-sync is the only committer. Sentry edits the tree under the roam-write lock and triggers the sync; it never commits or pushes roam. +5. *A per-pass suite run* — the suite runs at entry (baseline) and conditionally at fire-end (only when a pass touched non-org files). Hourly per-commit runs all night is the anti-pattern the suite policy exists to prevent. +6. *Executing a judgment or destructive action unattended* — those queue for the morning with their exact command. The pass did its detection; Craig makes the call. +7. *A silent skip* — inside a working fire, every skip writes a digest line naming why; a missing pass with no line reads as "ran clean" when it didn't. The one exception is not a violation: an all-quiet fire collapses to a single =sentry at HH:MM: nothing= heartbeat instead of one skip line per pass — the heartbeat is the explicit "nothing to do" record, per the silent-until-signal policy. +8. *Degrading a pass to a reduced form* — a pass runs fully or skips. No half-passes. +9. *Letting an unmerged branch stall silently* — after two consecutive unmerged-branch skips, the persistent desktop notify fires. Don't suppress it. +10. *Merging sentry's branch automatically* — the morning teardown is Craig's. Sentry creates and commits; it never merges or deletes its own branch. + +* Living Document + +Sentry ships with eleven mechanical passes and a deferred KB pass. The pass list, the interval default, and the queue-vs-execute line for each pass are the knobs most likely to move with dogfooding. Fold in what the live trial surfaces — a pass that queues too eagerly, a probe that misfires, a digest line that wants more detail. Refine as the signal arrives. + +* History + +Built 2026-07-19 from the sentry spec (=docs/specs/2026-07-14-sentry-workflow-spec.org=, ID f6c51f27-d7a2-4b63-9ff9-5ba005a66dfb) — 10 decisions and 12 review findings resolved before the build. Phase 1 shipped the =agent-lock= helper (commit =a8b6cf4=); this file is Phase 2, the engine. Phase 3 reconciles the roam writers (=inbox.org=, =knowledge-base.md=) to acquire the roam-write lock and adds the =wrap-it-up.org= guard. diff --git a/working/wrapup-temp-cleanup/proposed.diff b/working/wrapup-temp-cleanup/proposed.diff new file mode 100644 index 0000000..5c2375c --- /dev/null +++ b/working/wrapup-temp-cleanup/proposed.diff @@ -0,0 +1,19 @@ +--- claude-templates/.ai/workflows/wrap-it-up.org 2026-07-19 21:07:49.848788027 -0500 ++++ /tmp/wu.org 2026-07-23 02:48:49.759571128 -0500 +@@ -189,6 +189,16 @@ + emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org + #+end_src + ++*** Clear temp/ ++ ++#+begin_src bash ++[ -d temp ] && find temp -mindepth 1 -delete && echo "temp/ cleared" ++#+end_src ++ ++=temp/= holds throwaway artifacts — discarded prototypes, scratch output, intermediate data (see =working-files.md=). It's gitignored in every project, so nothing here rides a commit and nothing is recoverable from git once deleted. Clearing it at wrap is what keeps ephemeral work from silting up across sessions, and it's the counterpart to =working/=, which is tracked and *never* cleared here. ++ ++Two guards. Confirm before deleting if =temp/= holds anything a reasonable reader would call in-progress rather than throwaway — misfiled work belongs in =working/=, so move it there instead of deleting it. And skip the step entirely in a project where =temp/= is not gitignored, since that means the project is using the directory for something else. ++ + *** Sync child priorities + + #+begin_src bash diff --git a/working/wrapup-temp-cleanup/wrap-it-up.org.proposed b/working/wrapup-temp-cleanup/wrap-it-up.org.proposed new file mode 100644 index 0000000..103e225 --- /dev/null +++ b/working/wrapup-temp-cleanup/wrap-it-up.org.proposed @@ -0,0 +1,636 @@ +#+TITLE: Session Wrap-Up Workflow +#+AUTHOR: Craig Jennings +#+DATE: 2026-04-20 + +* Overview + +This workflow defines the process for ending a Claude Code session cleanly. It finalizes the session record, commits + pushes all work, and provides a warm handoff. A bare wrap also tears the session down (kills the ai-term buffer + tmux session, restoring geometry); a qualified wrap keeps the buffer, and a shutdown wrap powers the machine off. The teardown variants are set by the trigger phrase (see Teardown mode below) and act only at the very end, in Step 6. + +Triggered by Craig saying "wrap it up," "that's a wrap," "let's call it a wrap," or similar. + +* The Session Record + +Throughout the session, =.ai/session-context.org= has been maintained with: +- =* Summary= — structured distillation (empty or draft during session) +- =* Session Log= — chronological narrative of what happened, written as you go + +At wrap-up, this file becomes the permanent session record by being renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=. No transcription elsewhere. The file IS the record. + +* Exit Criteria + +The wrap-up is complete when: + +1. *Summary is written.* The =* Summary= section of =.ai/session-context.org= is populated by reading the =* Session Log= — Active Goal, Decisions, Data Collected / Findings, Files Modified, Next Steps. +2. *File is archived.* =.ai/session-context.org= has been renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org=. The old path no longer exists. +3. *todo.org is clean.* Cleanup script ran. Any auto-fixes are staged for the wrap-up commit. Orphan planning lines surfaced for manual fix if there are any. +4. *Linear board is honest* (skip if project doesn't use Linear). Any Dev-Review ticket whose PR has merged was moved to Done or PM Acceptance per the classification rule. +5. *Git state is clean.* All changes committed + pushed to all remotes. Working tree clean. +6. *Valediction delivered.* Brief, warm closing with key accomplishments and reminders, ending with =session wrapped.= on its own line as the signoff marker. + +The absence of =.ai/session-context.org= is the signal that the last session wrapped up cleanly. Its presence at session start means the previous session was interrupted. + +* Teardown mode (set from the trigger phrase) + +The wrap itself — Steps 1 through 5 — is identical in every mode. The trigger phrase only decides what Step 6 does once commit + push and the valediction are done. Resolve the mode from the phrase before starting: + +- *Teardown* (the default) — bare "wrap it up", "that's a wrap", "let's call it a wrap". The full wrap, then Step 6 kills the ai-term buffer + the =aiv-<project>= tmux session (which takes =claude= with it) and restores the saved window geometry. This is Craig's typical end-of-day case. +- *No-teardown* — "wrap it up with summary" or "wrap it up and summarize". The full wrap, but Step 6 leaves the buffer and session intact so the summary stays readable. The explicit qualifier is what opts out of teardown. +- *Shutdown* — "wrap it up and shutdown". The full wrap, then Step 6 gates on this being the only live ai-term session and powers the machine off. Shutdown supersedes teardown (killing the buffer is moot if the box is going down). + +Why teardown waits for Step 6 and runs through a hook, never inline: teardown kills the very tmux session =claude= runs in, so an inline kill would cut the valediction off before it renders. Step 6 instead drops a sentinel after everything else is verified, and the =Stop= hook (=ai-wrap-teardown.sh=) does the actual teardown when this response ends — by which point the valediction has already been delivered. + +This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-quit=, =cj/ai-term-live-count=, =cj/ai-term-shutdown-countdown=) and on the =Stop= hook being wired in =settings.json= (=hooks/settings-snippet.json=). If =emacsclient= or the daemon is unreachable, the sentinel is cleared and the session simply stays up — teardown degrades to a no-op, never a wedge. + +* The Workflow + +** Step 0: Refuse if sentry is live + +Before anything else, check whether sentry is running in this project. Sentry holds the working tree on its =sentry/<date>-<host>= branch and commits unattended; wrapping underneath it would archive the session anchor and tear down the buffer while the loop is still firing into it. If sentry's single-runner lock is held, stop and point at the shutdown path: + +#+begin_src bash +proj="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")" +if [ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock status "sentry-$proj" | grep -q '^held'; then + echo "sentry is active — say 'stop sentry' first" + exit 1 +fi +#+end_src + +The stop-sentry operation (defined in =sentry.org=) owns the shutdown: it cancels the loop, disposes of the branch, and walks the approval queue. Wrap-up carries only this one guard; a =stale= lock (a crashed fire) doesn't block — only a live =held= lock does. + +** Step 1: Finalize the Summary + +*** Work the Before-Close Queue (before the Summary) + +If the session anchor (=.ai/session-context.org=) carries a =* Before-Close Queue= heading with items, work them now, oldest-first, before writing the Summary, so any resulting edits ride this wrap's commit and get described in it. The queue is the "put X on the list" shorthand (see =protocols.org=, Colloquialisms and Expansions): session-scoped work Craig deferred to wrap time. + +Per item: do it if it's clear and bounded, or promote it to a =todo.org= task if it turns out to need its own session. Never drop an item silently. Remove each line as it's handled; if one can't be finished, surface it in the valediction (Step 5) and either leave a follow-up task or state why it's dropped. + +If there's no =* Before-Close Queue= heading, or it's empty, this step is a silent no-op. + +*** Early KB reflection (capture while fresh, before the Summary) + +Before distilling the Summary, while the session is still fresh, ask: what did this session learn worth remembering, for yourself or a future agent? Reflect and stage any candidate durable facts — a decision and its why, an environment gotcha, a reference pointer, a transferable lesson. Self-answer silently; this adds no interactive turn (Craig already authorized the wrap). The candidates flow straight into the KB promotion check below, which does the actual writing and the receipt — this is the capture half, that is the commit half, one pipeline, one receipt. Reflecting here rather than reconstructing learnings after the Summary is the point: the early ask is what keeps the receipt from defaulting to "promoted 0" out of fatigue. + +Read through the =* Session Log= in =.ai/session-context.org=. Populate (or refine) the =* Summary= section: + +- *Active Goal* — one or two sentences describing the session's focus +- *Decisions* — key choices made, with enough context to recall the /why/ +- *Data Collected / Findings* — anything concrete (measurements, root causes, paths, discoveries) +- *Files Modified* — what was changed, with one-line rationale per significant file +- *Next Steps* — what should happen in the next session + +Don't repeat everything from the Log in the Summary. The Summary is distillation — pull out what's load-bearing. The Log stays in the file and is available if a future reader wants detail. + +*** KB promotion check (and the one-line instrumentation receipt) + +Before closing the Summary, ask: did this session learn anything worth promoting to the agent knowledge base? The bar is =knowledge-base.md='s inclusion criteria — durable facts with cross-project or cross-machine value (decisions and their why, environment gotchas, reference pointers, transferable lessons). Promote each qualifying fact as one =agents/= node per the rule's schema (work-classified projects skip the write per the boundary; the check still runs so the receipt below is honest). + +Then add one line at the end of the Summary, always, even when nothing moved: + +#+begin_example +KB: promoted 2 / consulted yes +#+end_example + +"promoted N" counts nodes written this session (0 most sessions); "consulted yes-no" records whether any KB query informed the session's work. The line is the input to the spec's 30-day success-metrics checkpoint — grepping session archives for =KB:= answers "are agents actually using this?" without any other instrumentation. A session that skips the line breaks the metric, so it's part of the Summary contract, not optional. + +** Step 2: Pick a description + rename + +Read the Summary's Active Goal and the prominent entries in the Session Log. Pick a 4-6 word description that would make sense as a git-commit-message-series summary for the whole session. + +Good descriptions are concrete nouns/verbs: +- =docs-ai-migration-and-ai-launcher= +- =mybitch-usb-disconnect-diagnosis= +- =ratio-system-health-check= +- =orchestration-dashboard-bug-triage= + +Avoid vague ones: +- =session-work= (useless) +- =various-improvements= (useless) +- =updates= (useless) + +Get current time and rename: + +#+begin_src bash +mkdir -p .ai/sessions +now=$(date +%Y-%m-%d-%H-%M) +# Resolve the AI_AGENT_ID-aware source path (see protocols.org "Agent-scoped +# path"); fall back to the singleton if the helper isn't present. +sc=$(.ai/scripts/session-context-path 2>/dev/null || echo .ai/session-context.org) +# Under multi-agent, fold the agent id into the archive name so two agents +# wrapping in the same minute don't collide. Single-agent: no segment. +idseg="${AI_AGENT_ID:+${AI_AGENT_ID}-}" +mv "$sc" ".ai/sessions/${now}-${idseg}DESCRIPTION.org" +#+end_src + +Replace =DESCRIPTION= with your picked slug. (=AI_AGENT_ID= should be filename-safe and unique per run; the recommended =host.project.runtime.<epoch>= shape is both. The epoch on the tail keeps a re-run of the same logical agent from resolving to a prior run's leftover anchor. See protocols.org "Agent-scoped path".) + +** Step 3: todo.org cleanup (hygiene + archive completed work) + +If the project has a =todo.org= at its root, run the cleanup script before committing. Two passes, both fast and idempotent: a hygiene pass and an archive pass. + +*** Roam inbox sweep (inbox roam mode) + +Before the cleanup scripts, sweep the roam global inbox (=~/org/roam/inbox.org=) for items that belong to this project, so any imported tasks get linted and ride the wrap commit. Delegate to [[file:inbox.org][inbox.org]] roam mode for the claimed set. + +#+begin_src bash +[ -f "$HOME/org/roam/inbox.org" ] && grep -cE '^\*\* ' "$HOME/org/roam/inbox.org" || true +#+end_src + +Skip-fast when nothing matches: if the roam clone isn't on this machine, or no item is prefixed for this project, this is a silent no-op. When claimed items exist, run roam mode's Phase B–D (file each into =todo.org=, then remove them from the shared inbox and let =roam-sync= commit + push the edit). Report the total count and how many appeared related to this project, per roam mode's scan-summary rule. + +*** Hygiene pass + +It catches a recurring pattern: org sometimes leaves noise lines like =- State "X" from "X" [date]= when a state-change log lands outside a =:LOGBOOK:= drawer and the state didn't actually change. These lines carry no information and they break org's planning-line parser by wedging between the heading and =DEADLINE:=/=SCHEDULED:=, which kicks the entry out of agenda views. + +#+begin_src bash +[ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el todo.org +#+end_src + +The script is fast (under half a second on a 4000-line file) and idempotent — if there's nothing to fix, it reports zero changes and exits clean. + +What it does: + +1. *Auto-deletes* bogus state-log lines (matched on identical from/to states). Any deletions show up in the wrap-up commit's diff, so they get reviewed before push. +2. *Reports* "orphan planning lines" — entries whose body has =DEADLINE:= or =SCHEDULED:= but =org-entry-get= can't read it (some other malformation kept it out of canonical position). The script doesn't auto-rewrite these because the right fix depends on whether real state-log history needs preserving — surface them and fix manually if they matter for the agenda. + +Run the report-only variant first if you want to see what would change without writing: + +#+begin_src bash +emacs --batch -q -l .ai/scripts/todo-cleanup.el --check todo.org +#+end_src + +*** Convert done sub-tasks to dated entries + +#+begin_src bash +[ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks todo.org +#+end_src + +=--convert-subtasks= rewrites every heading at level 3 or deeper whose TODO state is DONE/CANCELLED/FAILED into a dated event-log entry (=<stars> YYYY-MM-DD Day @ HH:MM:SS -ZZZZ <text>=), dropping the keyword, priority cookie, and tags, and removing the now-redundant =CLOSED:= line. This enforces the =todo-format.md= depth rule that a completed *sub-task* (a heading under a parent task) becomes dated history, not a lingering DONE keyword — a shape an interactive org close (=org-log-done= → DONE + CLOSED) never applies and =--archive-done= (level-2 only) never reaches. The timestamp comes from each entry's own =CLOSED= cookie; a date-only close yields =00:00:00=. Heading text is kept verbatim. Idempotent (an already-dated heading has no keyword to match), and a done sub-task with no parseable =CLOSED= is flagged and left alone rather than stamped with a fabricated date. + +Run this *before* =--archive-done= so that when a completed level-2 parent is archived, its sub-tasks already carry their dated form. Any rewrites show up in the wrap-up commit's diff for review before push. + +Preview without writing: + +#+begin_src bash +emacs --batch -q -l .ai/scripts/todo-cleanup.el --convert-subtasks --check todo.org +#+end_src + +*** Archive completed work + +#+begin_src bash +[ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done todo.org +#+end_src + +=--archive-done= moves every level-2 subtree whose TODO state is DONE or CANCELLED out of the project's "Open Work" section and into its "Resolved" section, subtree intact. The two sections are matched by a unique level-1 heading containing "Open Work" (case-insensitive) and one containing "Resolved" — if either is missing or ambiguous, the file is skipped with a message, no crash. Only direct level-2 children move; a DONE entry nested under an open parent stays put. Idempotent; any moves show up in the wrap-up commit's diff for review before push. + +Preview the moves without writing: + +#+begin_src bash +emacs --batch -q -l .ai/scripts/todo-cleanup.el --archive-done --check todo.org +#+end_src + +*** Clear temp/ + +#+begin_src bash +[ -d temp ] && find temp -mindepth 1 -delete && echo "temp/ cleared" +#+end_src + +=temp/= holds throwaway artifacts — discarded prototypes, scratch output, intermediate data (see =working-files.md=). It's gitignored in every project, so nothing here rides a commit and nothing is recoverable from git once deleted. Clearing it at wrap is what keeps ephemeral work from silting up across sessions, and it's the counterpart to =working/=, which is tracked and *never* cleared here. + +Two guards. Confirm before deleting if =temp/= holds anything a reasonable reader would call in-progress rather than throwaway — misfiled work belongs in =working/=, so move it there instead of deleting it. And skip the step entirely in a project where =temp/= is not gitignored, since that means the project is using the directory for something else. + +*** Sync child priorities + +#+begin_src bash +[ -f todo.org ] && emacs --batch -q -l .ai/scripts/todo-cleanup.el --sync-child-priority todo.org +#+end_src + +=--sync-child-priority= walks every heading with a priority cookie =[#A]=–=[#D]= and, for each of its direct child headings whose own priority cookie is /lower/ (later in the alphabet — D is below A), bumps the child to match the parent. Down-only: parents are never bumped up to match a higher-priority child. Children without a priority cookie are left alone, as are parents without one. The walk visits parents before descendants, so a multi-level chain (=[#A]= → =[#B]= → =[#D]=) collapses to the top priority in a single pass. Idempotent. + +Opt-out for deliberately-lower children: tag the heading =:no-sync:= (the literal six-character tag, including the hyphen). The script matches the tag literally on the heading line, so it works whether or not the surrounding emacs config has extended =org-tag-re= to allow hyphens. + +#+begin_example +*** TODO [#D] Follow-up: VAD :no-sync: +#+end_example + +Use this for =Follow-up:=, =Spike:=, =Stretch:= sub-tasks that are deliberately deprioritized below their parent — without the tag, the wrap-up would silently bump them back up. + +Preview the bumps without writing: + +#+begin_src bash +emacs --batch -q -l .ai/scripts/todo-cleanup.el --check-child-priority todo.org +#+end_src + +(=--check-child-priority= is the report-only alias for =--sync-child-priority --check=.) + +*** Lint org files (mechanical sweep, judgments deferred) + +#+begin_src bash +if [ -n "$LINT_ORG_FOLLOWUPS" ]; then + followups="$LINT_ORG_FOLLOWUPS" +elif [ -d "./inbox" ]; then + followups="./inbox/lint-followups.org" +else + followups=".ai/lint-followups.org" +fi +[ -f todo.org ] && emacs --batch -q -l .ai/scripts/lint-org.el \ + --fix --followups-file="$followups" todo.org +#+end_src + +The =--fix= flag is required for the writes: lint-org's CLI default is +report-only (a linter reports, it doesn't write), and this wrap-up pass is +the deliberate exception that applies fixes — its diff rides the wrap-up +commit for review. + +=lint-org= runs =org-lint= over =todo.org=, auto-applies four mechanical +categories (=item-number= counters, bare =#+begin_src= → =#+begin_example=, +multi-line planning-info merged onto one line, =**X.**= → =*X.*=), and +appends every remaining judgment item (broken file links, invalid fuzzy +links, verbatim-asterisk inside body prose, suspicious src-block languages) +to the follow-ups file as a dated org section. Mechanical fixes show up in +the wrap-up commit's diff for review before push. + +The follow-up path defaults to =./inbox/lint-followups.org= in the current +project (where the next morning's daily-prep merges it in). If the project +doesn't have an =inbox/= directory, the script falls back to +=.ai/lint-followups.org= inside the current project. Override with +=LINT_ORG_FOLLOWUPS=<path>= in the environment if needed — useful for +routing all wrap-up output to a single shared inbox across projects. + +Each project's own =inbox/= is the right default because daily-prep reads +that project's inbox at startup. Hardcoding a single project's path +(formerly =~/projects/work/inbox/=) routed every project's wrap-up findings +into the wrong inbox. + +Preview without writing — same flags as =--check= on the other scripts: + +#+begin_src bash +[ -f todo.org ] && emacs --batch -q -l .ai/scripts/lint-org.el --check todo.org +#+end_src + +The wrap-up never blocks on judgment items — they're deferred by design. +For an interactive walk of the judgments mid-day, run =/lint-org todo.org=. + +*** Inbox sanity check (surface unprocessed handoffs) + +If the project has an =inbox/= directory, verify it holds nothing but =.gitkeep=, =lint-followups.org= (the lint-org pipeline file the next morning's daily-prep consumes), and any explicitly-deferred =PROCESSED-*= files before the wrap completes. An inbox that arrived at session start with handoffs from other projects, or that received handoffs mid-session, needs =inbox.org= process mode to run and apply its value-gate dispositions. Wrapping with a dirty inbox silently defers the work to next session and accumulates handoff debt that the sender can't see. + +#+begin_src bash +unprocessed=$(find inbox -maxdepth 1 -type f \ + ! -name '.gitkeep' \ + ! -name 'lint-followups.org' \ + ! -name 'PROCESSED-*' \ + 2>/dev/null | wc -l) +if [ "$unprocessed" -gt 0 ]; then + echo "wrap-up: inbox/ has $unprocessed unprocessed item(s). Run inbox.org process mode before wrapping, or explicitly defer each item with a one-line reason in the valediction." + find inbox -maxdepth 1 -type f \ + ! -name '.gitkeep' \ + ! -name 'lint-followups.org' \ + ! -name 'PROCESSED-*' \ + -printf ' %f\n' +fi +#+end_src + +If the count is zero or the project has no =inbox/= directory, the check is a silent no-op. If non-zero, the wrap is incomplete by default. The user resolves each item (process now, defer with reason in the valediction, or delete with rationale) before the validation checklist passes. + +The check exempts =lint-followups.org= explicitly because lint-org runs earlier in the same wrap-up workflow and writes its judgment items to that file in =inbox/= by design. The file is a pipeline artifact for the next morning's =daily-prep=, not a handoff that needs the value gate. + +This integrates with =inbox.org= process mode, which stamps =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section on completion. Wrap-up doesn't double-stamp. It only ensures the inbox carries nothing but the expected pipeline artifacts at session end. + +*** Cross-project router (optional — route filed keepers to their home projects) + +Runs directly after the inbox sanity check. The split between the two: the sanity check *gates* the wrap (a dirty inbox blocks until resolved); the router is *optional* (skipping it never blocks anything — the candidates just stay local until a future wrap). Spec: =docs/specs/wrapup-routing-spec.org= (D7/D8/D9). + +The candidate set is exactly the local tasks carrying a =:ROUTE_CANDIDATE:= property — keepers that inbox process mode filed this session whose inferred home is another project. Never scan the standing backlog. + +#+begin_src bash +.ai/scripts/route-batch --list +#+end_src + +*Empty set = zero interaction.* =--list= prints nothing when there are no candidates; continue the wrap silently — no prompt, no "0 items" line. + +When candidates exist, surface the batch as one line per task — the task heading, the destination project, the delivery mode (=inbox-send= file handoff), and the engine's confidence — then offer exactly two options: *go* (route the whole batch) or *skip* (leave everything local). Derive each confidence label by running the engine on the task's heading + body (=python3 .ai/scripts/route_recommend.py --item "..." --exclude "$(basename "$PWD")"=); label weak matches visibly ("weak — verify the destination") so a low-confidence route gets a human glance before the keystroke. + +On *go*: + +#+begin_src bash +.ai/scripts/route-batch --go +#+end_src + +Per candidate, the helper writes the task's subtree (children ride along; =:ROUTE_CANDIDATE:= stripped, headings promoted to top level) to a one-task handoff, delivers it via =inbox-send <destination> --file= (so the =from-<this-project>= provenance is stamped and the destination's inbox process mode dispositions it as a single item), and only after a successful send removes the subtree from the local =todo.org= — a single-file local edit the wrap is already committing. A failed send leaves that task in place and exits non-zero; report it and continue the wrap. Never write the destination's =todo.org= directly; its own inbox processing files the task per its conventions. + +On *skip*, leave every candidate in place, marker included — they resurface next wrap. + +Mis-routes are recoverable: the receiving project rejects via inbox process mode's reject-from-another-project flow, which returns the item to this project's inbox with the rationale. That reject path is why removing the local source on send is safe. + +*** Review-habit health check (surface a slipped daily task-review) + +The daily task-review habit walks the open top-level tasks on a rotating cycle, stamping =:LAST_REVIEWED:= as it goes (see =task-review.org=). This check is the watchdog for that habit. When tasks have gone too long unreviewed, the habit has slipped, and the wrap-up says so in one line — it does not re-list the tasks. + +=task-review-staleness.sh= counts top-level =[#A]= / =[#B]= / =[#C]= tasks (TODO/DOING/VERIFY) whose =:LAST_REVIEWED:= is missing or older than the threshold. Threshold 30 days is about 2.5 review cycles of slack at the default batch size — one missed week is fine, three weeks signals a problem. + +#+begin_src bash +if [ -n "$LINT_ORG_FOLLOWUPS" ]; then + followups="$LINT_ORG_FOLLOWUPS" +elif [ -d "./inbox" ]; then + followups="./inbox/lint-followups.org" +else + followups=".ai/lint-followups.org" +fi +if [ -f todo.org ]; then + stale=$(.ai/scripts/task-review-staleness.sh todo.org 30 2>/dev/null || echo 0) + if [ "$stale" -gt 0 ]; then + printf "\n* %s — Task-review health: %s top-level [#A]/[#B]/[#C] tasks unreviewed for >30 days (daily review may have slipped)\n" \ + "$(date '+%Y-%m-%d %a')" "$stale" >> "$followups" + fi +fi +#+end_src + +A non-zero count writes one summary line and nothing else — the per-task walk is the review habit's job, not the wrap-up's. This supersedes the old date-coverage scan, which flagged every dateless =[#A]= / =[#B]= task on the wrong assumption that high-priority work needs a date. No-date is a valid resting state for research and watch-list tasks; staleness, not datelessness, is the real signal. + +** Step 3.5: Linear ticket-state hygiene (skip if project doesn't use Linear) + +If the project uses Linear and has any tickets currently in *Dev Review* assigned to Craig, sweep them before the wrap-up commit. The check is fast and keeps the board honest — tickets stuck in Dev Review after their PR merges hide actual work-in-progress. + +#+begin_src +mcp__linear__list_issues assignee="me" state="Dev Review" limit=50 +#+end_src + +For each result, look up the linked PR (the =gitBranchName= field on the issue maps to a =headRefName= on the project's GitHub remote — use =gh pr list --author <github-login> --state all --json number,state,headRefName,mergedAt,title=). + +*Assumption:* the =gh= lookup expects a GitHub-family host. It holds today because the only Linear-using project (DeepSat) lives on =deepsat.ghe.com=, where =gh= talks to the GHE API. A future Linear-using project on a non-GitHub host (GitLab, Gitea, Bitbucket) would need a provider-agnostic PR lookup here — update this step when that happens. + +If a Dev-Review ticket's PR is *merged*, propose a move: + +- *Done* — chores, refactors, test-coverage backfills, dead-code removal, e2e-flake fixes, anything with no PM-visible behavior change. PR titles prefixed =chore:=, =test:=, =refactor:=, =docs:= almost always belong here. +- *PM Acceptance* — real behavior fixes or new features a PM (or end user) could verify by clicking through the app. PR titles prefixed =fix:=, =feat:= usually belong here unless the change is invisible to users. + +When in doubt, ask Craig per ticket. Don't auto-pick. After Craig confirms, move via =mcp__linear__save_issue= with =state="Done"= or =state="PM Acceptance"=. Several can run in parallel. + +Skip the step entirely if the project doesn't use Linear (e.g. personal projects, the rulesets repo). + +** Step 4: Git commit + push + +*** Step 4.0: Commit template-sync churn first (consuming projects) + +The startup workflow's Phase A rsyncs template updates from rulesets into this project's =.ai/= (=protocols.org=, =workflows/=, =scripts/=) every session that rulesets has advanced. Nothing commits that churn, so without this step it accumulates across sessions and eventually blocks Phase A.0's auto-fast-forward (git refuses to ff a dirty tree). Commit it here, as its own =chore:= commit, before the session-work commit — so the sync stays separate from what the session actually shipped and the tree ends clean. + +The guard is conservative: only auto-commit a dirty synced path when it matches the rulesets canonical byte-for-byte (a modified/new file equals canonical, or a deletion pairs with a file retired upstream). If any synced path is dirty but /doesn't/ match canonical — a local hand-edit to a file that's supposed to be sync-managed — surface it and don't auto-commit. Anything outside the three synced paths is untouched here; the normal Step 4 commit and the worktree-leftover step handle it. + +#+begin_src bash +# Skip in the rulesets repo itself: there .ai/ is a committed mirror of +# claude-templates/.ai/, kept in sync by the pre-commit hook and committed +# alongside template edits — not downstream sync churn. The presence of +# claude-templates/.ai/ in this repo is the tell. +if [ ! -d claude-templates/.ai ] && [ -d "$HOME/code/rulesets/claude-templates/.ai" ]; then + canon="$HOME/code/rulesets/claude-templates/.ai" + safe=1 + commitlist=() + while IFS= read -r line; do + f="${line:3}" # strip the 2-char status + space + rel="${f#.ai/}" + if [ -e "$f" ] && [ -e "$canon/$rel" ] && diff -q "$f" "$canon/$rel" >/dev/null 2>&1; then + commitlist+=("$f") # modified/new here, matches canonical + elif [ ! -e "$f" ] && [ ! -e "$canon/$rel" ]; then + commitlist+=("$f") # deleted here AND retired upstream + else + safe=0 # synced path dirty but != canonical + fi + done < <(git status --porcelain -- .ai/protocols.org .ai/workflows/ .ai/scripts/) + + if [ "$safe" -eq 1 ] && [ "${#commitlist[@]}" -gt 0 ]; then + git add -- "${commitlist[@]}" + git commit -q -m "chore: sync .ai tooling from templates" + echo "wrap-up: committed ${#commitlist[@]} synced .ai file(s) as a template-sync chore." + elif [ "$safe" -eq 0 ]; then + echo "wrap-up: synced .ai paths are dirty but not all match rulesets canonical — NOT auto-committing. Resolve manually:" + git status --porcelain -- .ai/protocols.org .ai/workflows/ .ai/scripts/ | sed 's/^/ /' + fi +fi +#+end_src + +The commit isn't pushed here — the push step below pushes the current branch, which carries both this chore commit and the session-work commit. A crashed session that never reaches wrap-up leaves the churn for the next startup, which surfaces it (see startup.org Phase C) so it never silently accumulates. + +*** Review changes + +#+begin_src bash +git status +git diff --stat +#+end_src + +Decide the scope of the wrap-up commit. Usually everything that changed during the session goes into one commit. If anything is intentionally not part of this session's work (pre-existing WIP, unrelated files), leave it out. + +*** Stage + +Add the renamed session file and all other session changes: + +#+begin_src bash +git add .ai/sessions/ [other modified paths] +#+end_src + +Do NOT blindly =git add .= — review what's being staged so unrelated dirty state isn't dragged in. + +*** Commit + +Commit message rules (also see protocols.org "Git Commit Requirements"): + +- Subject line: concise, describes what /shipped/. Use conventional prefixes (=docs:=, =refactor:=, =fix:=, =feat:=, =chore:=) — NEVER =session:=. +- Body: 1-3 terse sentences describing what was accomplished. +- NO Claude Code attribution. NO =Co-Authored-By=. NO references to =notes.org=, =session-context.org=, =.ai/sessions/=, "session wrap-up", or session timestamps. + +*Wrap-up commits skip the inline-approval gate.* The =commits.md= rule that requires writing the message to =/tmp/commit-<slug>.md=, printing inline, and waiting for an approve / request-changes / open-in-editor response does *not* apply to wrap-up commits. The wrap-up flow is meant to be quick — Craig has already authorized the wrap by triggering the workflow ("wrap it up"), and stopping again to approve a commit message disrupts the cadence. + +Still apply =/voice personal= silently before committing so the message reads cleanly. Just don't print and ask. Commit directly with the cleaned message. + +If a wrap-up commit needs Craig's eyes for a content reason (sensitive change, unusual scope, something he flagged earlier), surface it explicitly. Otherwise commit and move on. + +Example: +#+begin_example +docs: restructure docs/ to .ai/ and unify aix+hey into ai launcher + +Hidden .ai/ now holds Claude tooling; project-level docs/ reserved +for user-facing docs. Single 'ai' launcher (fzf multi + smart tmux ++ git-aware fetch/pull) replaces the aix script and hey alias. +#+end_example + +Use heredoc for multi-line: +#+begin_src bash +git commit -m "$(cat <<'EOF' +subject line here + +body sentences here. +EOF +)" +#+end_src + +*** Push to all remotes + +#+begin_src bash +git remote -v +#+end_src + +Push the current branch to every remote (some repos have multiple remotes — a primary host plus one or more mirrors, or different remotes for different audiences — and the loop keeps all of them current): + +#+begin_src bash +current=$(git symbolic-ref --short HEAD) +for r in $(git remote); do git push "$r" "$current"; done +#+end_src + +Then push every other local branch with a tracking upstream to its tracking remote. This catches feature branches that advanced during the session but aren't the one being wrapped up — without it, work-in-progress branches stay local-only and are at risk if the machine dies before the next wrap-up. + +#+begin_src bash +git for-each-ref --format='%(refname:short) %(upstream:remotename)' refs/heads/ | \ +while read branch remote; do + [ "$branch" = "$current" ] && continue + if [ -z "$remote" ]; then + echo " $branch: no tracking upstream — skipped (push manually with 'git push -u')" + else + git push "$remote" "$branch" + fi +done +#+end_src + +Behavior: +- *Tracked branches* → pushed to their upstream remote. +- *Untracked branches* (no upstream set) → surfaced, not pushed. Craig sets the upstream manually with =git push -u <remote> <branch>= when he's ready. Auto-creating an upstream would commit to a remote choice the workflow can't make safely. +- *Diverged or rejected pushes* → surface and stop. Don't force-push from this workflow; resolve manually. + +*** Resolve every worktree leftover + +#+begin_src bash +git status --short +#+end_src + +*Default policy: end every session with an empty =git status=.* The wrap is incomplete while anything remains dirty. There is no "leave it alone" default — every leftover gets an active resolution. The only way for a file to stay dirty across the wrap is the user explicitly saying "defer this one, leave it dirty." Surface each leftover with a concrete recommendation; the user has to actively opt out for the dirt to persist. + +This inverts the older "intentional carryover" default, which let pre-existing dirty state accumulate across sessions silently. Carryover that lives for days or weeks is almost always one of: a forgotten commit from a prior wrap, a stale change that should be discarded, or genuine in-flight work that needs an explicit stash/branch home. None of those should default to "leave it dirty." + +**** Three kinds of leftover + +| Pattern | What it is | Recommended action (apply unless user defers) | +|---+---+---| +| Generated, runtime, or lock files that no human edits — e.g., =.claude/scheduled_tasks.lock=, =.pytest_cache/=, build outputs, IDE state, editor swap files | *Runtime artifact* — created by tooling or the harness, not by the user, and shouldn't be tracked | Add the matching pattern to =.gitignore= (project-level, not =~/.gitignore_global=). For tracked files, =git rm --cached <path>=. Stage =.gitignore= and any =rm --cached= changes in *one* follow-up commit (=chore: gitignore X=), push. Re-run =git status= to confirm clean. | +| Modified or created during the session but not staged into the wrap-up commit | *Forgotten change* — real session work that should have been in the wrap commit but missed it | Stage and create a follow-up commit. Don't =--amend= the wrap-up commit once pushed (diverging history without a clear win). Push the follow-up to all remotes. | +| Was dirty at session start and still dirty at session end — work this session deliberately didn't touch | *Pre-existing dirt that needs a decision* — could be a missed commit from a prior wrap, stale abandoned work, or real in-flight work without a home | Investigate (show diff + check the originating session). Recommend one of: (a) commit now if the work is complete, (b) stash with a descriptive message if it's genuine WIP, (c) =git checkout -- <path>= / =git clean -f <path>= if stale and unwanted, (d) move to a feature branch if it's longer-running, (e) user explicitly defers and accepts the dirt. Do not silently leave dirty. | + +**** Per-file flow + +For each leftover line in =git status --short=: + +1. Identify which of the three kinds above it matches. +2. State what the file is (one line) and the recommended action. +3. Apply the action unless the user explicitly defers. +4. Re-run =git status --short= after each follow-up commit until empty (or until every remaining line is an explicit user-deferred entry). + +The pre-existing-dirt case (third row) is the one this rule most cares about. Treat each pre-existing-dirty file as a question that must get an answer this session, not as "carryover that's fine to inherit." A file that was dirty for a week before this session probably isn't going to get cleaner by waiting another week. Look at the diff, check the originating session's notes, and recommend a real resolution. + +**** When the user defers + +If the user does say "leave this one dirty for now" after seeing the recommendation, that is fine — log the deferral in the valediction so the next session knows it was an explicit choice, not a miss. Format: "Deferred (per Craig's decision today): =path/to/file= — <one-line reason>". Without that note, the next session can't distinguish "we agreed to defer" from "we forgot again." + +** Step 5: Valediction + +Brief, warm closing. 3-4 sentences max. + +Include: +- What was accomplished (specific, not generic) +- What's ready for next session +- Any critical reminders or deadlines + +Tone: warm but professional. No emoji unless Craig has explicitly requested. Acknowledge effort when session was long or difficult. + +End on a clear signoff: the *last* line of the valediction is always =session wrapped.= on its own line (lowercase, with the period, nothing after it). It's the unmistakable end-of-session marker, so don't trail it with another sentence. This is the last user-facing output — Step 6's teardown is silent. + +Example: +#+begin_example +That's a wrap. Today we restructured the entire claude-templates +ecosystem: docs/ → .ai/ across all 23 projects, unified aix + hey +into a single 'ai' launcher with git-aware fetch/pull, and cleaned +up 4 code projects on velox. Both machines fully in sync. + +Two things to pick up next: the chime README WIP (your inline notes +from earlier) and archsetup's layout-navigate tests. Both are +ratio-local uncommitted state. + +Good session. Talk tomorrow. + +session wrapped. +#+end_example + +** Step 6: Session teardown (mode-dependent) + +The last action of the wrap, and only after Step 4's commit + push is verified and the Step 5 valediction is composed. The teardown itself happens when this response ends (via the =Stop= hook), so the valediction always renders first. Act by the mode resolved up front: + +*** No-teardown mode + +Do nothing. The buffer, the =aiv-<project>= tmux session, and =claude= all stay up so the summary stays readable. The wrap is complete. + +*** Teardown mode (default) + +Confirm commit + push succeeded (Exit Criteria 5 — never tear down over unpushed work), then drop the sentinel: + +#+begin_src bash +touch "/tmp/ai-wrap-teardown-$(basename "$PWD")" +#+end_src + +That is the whole step. Don't run any =tmux kill-session=, =emacsclient=, or buffer kill inline — the =Stop= hook reads the sentinel when this response ends and runs =cj/ai-term-quit=, which kills the =aiv-<project>= session (taking =claude= with it), kills the vterm buffer, and restores geometry. The basename of =$PWD= is the key the hook matches, so the sentinel names the session it tears down. + +*** Shutdown mode + +Confirm commit + push succeeded, then evaluate the safety gate *before* committing to the shutdown — never power the box off out from under another live session: + +#+begin_src bash +emacsclient -e '(cj/ai-term-live-count)' +#+end_src + +- *Count > 1* — another ai-term session is alive. ABORT the shutdown. List the other live =aiv-*= sessions, drop *no* sentinel, and tell Craig in the valediction that it fell back to a normal wrap (no poweroff, no teardown). This gate is the load-bearing safety of the whole feature. +- *Count = 1* — this session is the only one. Drop the shutdown sentinel: + + #+begin_src bash + touch "/tmp/ai-wrap-shutdown-$(basename "$PWD")" + #+end_src + + The =Stop= hook fires =cj/ai-term-shutdown-countdown= when this response ends: it re-checks the gate, runs an abort-able 10→1 countdown in the Emacs echo area (=C-g= cancels), then =sudo shutdown now=. Shutdown supersedes teardown — do *not* also drop the teardown sentinel. + +If =emacsclient= isn't resolvable or the daemon is down, the gate can't run — abort the shutdown, fall back to a normal wrap, and say so. Don't power off on an unverifiable gate. + +* Common Mistakes to Avoid + +1. *Skipping Step 1 (Summary)* — the file becomes the record; an empty Summary makes it hard to scan at catch-up +2. *Vague description in filename* — =2026-04-20-updates.org= is useless next to =2026-04-20-13-45-docs-ai-migration.org= +3. *=git add .= without review* — drags in unrelated dirty state +4. *=session:= prefix in commit message* — explicitly forbidden; use real change categories +5. *Claude-tooling references in commit message* — describes tooling, not what shipped +6. *Forgetting to push to all remotes* — check =git remote -v=, push to each +7. *Leaving =.ai/session-context.org= in place* — its presence means "interrupted session", confuses next startup +8. *Long preachy valediction* — brief beats thorough +9. *Leaving runtime/generated files dirty without gitignoring them* — pollutes every future =git status= and erodes trust in "working tree clean" as a signal. Fix =.gitignore= during the wrap, not later. +10. *Treating "was dirty at session start, still dirty now" as fine by default* — that's how a forgotten commit from two sessions ago turns into "carryover" for two weeks. Every pre-existing dirty file needs an active resolution recommendation this session. Deferral is allowed only with an explicit user choice, logged in the valediction. + +* Validation Checklist + +Before considering wrap-up complete: + +- [ ] =.ai/session-context.org= =* Summary= section populated +- [ ] The Summary ends with the =KB: promoted N / consulted yes-no= line (promotion check ran) +- [ ] File renamed to =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= +- [ ] =.ai/session-context.org= no longer exists +- [ ] =todo-cleanup.el= ran — hygiene pass + =--convert-subtasks= + =--archive-done= + =--sync-child-priority= (if =todo.org= exists at project root) +- [ ] =lint-org.el= ran on =todo.org= — mechanical fixes applied, judgments appended to follow-ups file (if =todo.org= exists) +- [ ] Any orphan-planning-line warnings reviewed (fix or accept) +- [ ] Inbox carries nothing but expected pipeline artifacts (=.gitkeep=, =lint-followups.org=, =PROCESSED-*= prefixes), OR each remaining handoff has an explicit deferral logged in the valediction +- [ ] Linear Dev-Review sweep ran; any merged-PR tickets moved to Done or PM Acceptance (skip if project doesn't use Linear) +- [ ] Template-sync churn committed as its own =chore: sync .ai tooling from templates= (consuming projects only; skipped in rulesets), or surfaced if a synced path didn't match canonical +- [ ] After wrap-up commit + push, =git status --short= is empty OR every remaining line has an explicit user-deferred decision logged in the valediction +- [ ] Each leftover was investigated and the user saw a concrete resolution recommendation +- [ ] Runtime artifacts added to =.gitignore=, follow-up commit pushed, =git status= re-verified +- [ ] Forgotten changes committed in a follow-up and pushed +- [ ] Pre-existing dirty files resolved (committed / stashed / discarded / moved to a feature branch) or explicitly deferred with a one-line reason in the valediction +- [ ] Current branch pushed to ALL remotes (verified with =git remote -v=) +- [ ] All other local branches with a tracking upstream pushed to their remote +- [ ] Any untracked-upstream branches surfaced for manual =git push -u= +- [ ] Step 6 teardown matches the trigger phrase: no-teardown leaves the buffer; teardown drops only =/tmp/ai-wrap-teardown-<project>=; shutdown gates on =cj/ai-term-live-count= = 1 and drops only =/tmp/ai-wrap-shutdown-<project>= +- [ ] No teardown/shutdown sentinel was dropped before commit + push was verified +- [ ] Shutdown aborted (fell back to normal wrap, logged in the valediction) when another =aiv-*= session was live or the gate couldn't run +- [ ] Commit message follows format (no =session:=, no Claude attribution) +- [ ] Valediction delivered (brief, specific, warm) |
