From cbcd972fbea65c315713113572f9334efe8d1ca3 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Fri, 31 Jul 2026 06:33:44 -0500 Subject: chore: archive work records and park incoming handoffs Records for the helper-launcher and sync-extraction work, plus four sets of handoffs received from other projects. Each set is parked beside the decision it still needs: sentry's CronCreate correction and its pass-3 reframe, the per-project channel declaration model, the template-sync revert finding, and the nag-event vocabulary. Nothing here is code. The handoffs are kept rather than deleted because two of them turn on the difference between two readings of the same rule, and that difference is the useful part. --- ...6-07-29-14-29-helper-1055-launch-pilot-idle.org | 62 ++ ...lper-agents-mcp-scoping-and-sync-extraction.org | 656 +++++++++++++++++++++ ...rom-home-vocabulary-to-spread-craig-named-a.org | 15 + ...ome-amendment-to-the-nag-event-vocabulary-i.org | 11 + ...om-work-sentry-org-the-arming-mechanism-was.org | 22 + .../2026-07-30-0736-from-work-sentry.org | 244 ++++++++ ...ork-correction-to-the-sentry-org-i-sent-you.org | 19 + .../2026-07-30-0739-from-work-sentry.org | 248 ++++++++ ...acs.d-distribution-failure-on-the-loadchats.org | 25 + ...-work-the-telega-loadchats-fix-regressed-in.org | 15 + ...acs.d-correction-and-escalation-on-the-sync.org | 17 + ...cs.d-propagation-verified-end-to-end-from-a.org | 23 + ...ork-reciprocal-to-the-triage-sources-defect.org | 21 + 13 files changed, 1378 insertions(+) create mode 100644 .ai/sessions/2026-07-29-14-29-helper-1055-launch-pilot-idle.org create mode 100644 .ai/sessions/2026-07-31-06-32-helper-agents-mcp-scoping-and-sync-extraction.org create mode 100644 working/nag-event-vocabulary/2026-07-30-1646-from-home-vocabulary-to-spread-craig-named-a.org create mode 100644 working/nag-event-vocabulary/2026-07-30-1653-from-home-amendment-to-the-nag-event-vocabulary-i.org create mode 100644 working/sentry-arming-correction/2026-07-30-0736-from-work-sentry-org-the-arming-mechanism-was.org create mode 100644 working/sentry-arming-correction/2026-07-30-0736-from-work-sentry.org create mode 100644 working/sentry-arming-correction/2026-07-30-0739-from-work-correction-to-the-sentry-org-i-sent-you.org create mode 100644 working/sentry-arming-correction/2026-07-30-0739-from-work-sentry.org create mode 100644 working/sync-model-revert/2026-07-30-1820-from-.emacs.d-distribution-failure-on-the-loadchats.org create mode 100644 working/sync-model-revert/2026-07-30-1829-from-work-the-telega-loadchats-fix-regressed-in.org create mode 100644 working/sync-model-revert/2026-07-30-1832-from-.emacs.d-correction-and-escalation-on-the-sync.org create mode 100644 working/sync-model-revert/2026-07-30-1911-from-.emacs.d-propagation-verified-end-to-end-from-a.org create mode 100644 working/triage-declaration-model/2026-07-30-1733-from-work-reciprocal-to-the-triage-sources-defect.org diff --git a/.ai/sessions/2026-07-29-14-29-helper-1055-launch-pilot-idle.org b/.ai/sessions/2026-07-29-14-29-helper-1055-launch-pilot-idle.org new file mode 100644 index 0000000..76e856b --- /dev/null +++ b/.ai/sessions/2026-07-29-14-29-helper-1055-launch-pilot-idle.org @@ -0,0 +1,62 @@ +helper-1055 +#+TITLE: Helper Session — helper-1055 +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-29 + +* Summary + +Closed out by the primary session on 2026-07-31 at Craig's direction, rather +than by this helper itself. It was launched as the live pilot of =ai --helper=, +was never given work, and idled from 14:29 on 2026-07-29 onward. + +** Active Goal + +Serve as the first live helper instance in rulesets, validating the =ai --helper= +launch path end to end against a running primary. + +** Decisions + +None. This session made no decisions and was asked for none. + +** Data Collected / Findings + +The helper launch path works. Every step of the contract's light startup +executed as designed: the launcher-exported =AI_AGENT_ID=helper-1055= was +adopted rather than self-assigned, =helper-mode.org= was read as the role +contract, and every primary-only action was skipped — no Phase A.0 pulls, no +=make install=, no =.ai/= rsync, no inbox processing, no git mutation. + +The roster resolved correctly from inside the helper: one other live agent (pid +3707800) in the same project. Anchors did not collide — the primary stayed on +the unscoped =session-context.org= while this session created +=.ai/session-context.d/= and wrote here. + +** Files Modified + +Only this file. The helper touched nothing else, which is itself the finding: +the contract's read/write tiers held with no enforcement beyond the instruction. + +** Next Steps + +None for this session. The unbuilt pieces it surfaced — startup's roster check +and the live-helper gate on file-wide hygiene passes — are recorded in +=helper-mode.org='s Status section and in the primary's session record. + +KB: promoted 0 / consulted no + +* Session Log + +** 2026-07-29 Wed @ 14:29 CDT — Helper light startup + +Launched as a helper into rulesets with =AI_AGENT_ID=helper-1055= already +exported, so I adopted that id rather than self-assigning. Read +=.ai/workflows/helper-mode.org= (the role contract) and =.ai/protocols.org=. +Skipped everything primary-only: no Phase A.0 pulls, no =make install=, no +=.ai/= rsync, no inbox processing, no git mutation. + +Roster confirms I am not alone — one other live agent, pid 3707800, cwd +=/home/cjennings/code/rulesets=. That primary is anchored on the unscoped +=.ai/session-context.org= (last written 10:46 today), so our anchors don't +collide. =.ai/session-context.d/= didn't exist; I created it for this file. + +Awaiting the work Craig spawned this helper for. diff --git a/.ai/sessions/2026-07-31-06-32-helper-agents-mcp-scoping-and-sync-extraction.org b/.ai/sessions/2026-07-31-06-32-helper-agents-mcp-scoping-and-sync-extraction.org new file mode 100644 index 0000000..e2c5903 --- /dev/null +++ b/.ai/sessions/2026-07-31-06-32-helper-agents-mcp-scoping-and-sync-extraction.org @@ -0,0 +1,656 @@ +#+TITLE: Session Context — 2026-07-29 +#+AUTHOR: Craig Jennings + +* Summary + +** Active Goal + +Started as Craig's question about running a second helper agent in one project +for "dogpiling" bugs, and became four arcs: building and piloting the helper +launcher, prototyping worktree-based branch isolation, an MCP account-binding +and CUI investigation, and finally a propagation incident that my own +uncommitted file had been causing all day. + +** Decisions + +- *Linked worktrees, not bare, and the writer gets the isolation.* A bare layout + moves the path that project discovery, the roster's cwd match, =inbox-send= + resolution and =make install= all key on. Craig's framing pinned the diagnoser + to main; inverted it, because the read-only agent needs no isolation and the + committer does. +- *Port work's sentry corrections rather than apply their file.* Both copies + predated =f3f5bfd= and would have reverted the =fire= → =cycle= rename the day + after it landed. +- *Hold sentry's pass 3 at the mail/messenger exclusion* rather than take the + read-everything change. The =[#A]= account-binding guard is still an unapplied + prepared diff, and a category exclusion is the only thing keeping that hazard + unreachable meanwhile. Craig then reframed it further: each project declares + its own channels, so a blanket rule in either direction is the wrong shape. +- *Extraction before guard changes.* The sync is untested inline bash running in + every project's every session, and both intended fixes modify exactly that. +- *=--force= and a cache caveat on the recovery command*, after the reviewer + checked =audit.sh= and found it skips dirty tracked projects and carries none + of the =--exclude= flags. + +** Data Collected / Findings + +- *=ai --helper= shipped and piloted live* (=84bd121=). The roster excludes its + caller's own ancestry, so it must be run from a separate terminal — from + inside an agent session it silently downgrades to a second primary. +- *=google-keep= was broken, not absent.* keep-mcp 0.3.1 imports FastMCP from + =mcp.server.fastmcp=, which =mcp= 2.0.0 removed, so the server died at import + and every client saw connected-with-no-tools. Pinned =mcp<2=; 23 tools + including =pin_note= and =set_note_color=, so the whole nag-note spec + automates and home's manual workaround is unnecessary. +- *Every MCP server is registered globally*, so =slack-deepsat=, + =google-docs-work= and =linear= are callable from every project. The + =:TRIAGE_SOURCES:= declaration governs one consumer while the exposure sits at + the registry. +- *The claude.ai connectors were live despite being "disabled"*, and probing + showed them bound to the *personal* account — which contradicts the =[#A]= + task's premise that =mcp__claude_ai_Gmail= is work-bound. I carried home's + binding claim forward without checking it, the same error I had caught work + making that morning. +- *My uncommitted =sentry.org= blocked template propagation for a full day.* + Five workflow files went stale in =.emacs.d= alone; two were my own commits + from that morning. Then I re-created the identical condition six hours after + diagnosing it, which is the argument that the guard punishes ordinary WIP with + a silent project-wide outage. +- *Two review rounds each found a real defect in my own work*: an =AI_AGENT_ID= + interpolated unquoted into the pane's command line, and a characterization + test that provably tested nothing (mutation-proven — deleting the flags it + guarded left the suite green). + +** Files Modified + +- =84bd121= =feat(ai): add --helper for a second session in a live project= — + launcher, wrap-up Step 0 helper branch, doc corrections, 30 tests. Pushed. +- =f571057= =refactor(startup): extract the template sync into a tested script= — + =sync-templates= plus 15 characterization tests. Pushed. +- =~/.claude.json=: =google-keep= re-registered with =mcp<2= (ratio only; velox + has no entry at all). +- =stash@{0}=: the sentry CronCreate port, held pending the declaration spec. +- =working/=: four parked handoff sets (sentry arming, triage declaration, sync + model, nag-event vocabulary). +- Worktree =~/code/rulesets-wrap-org-table= on =fix/wrap-org-table=, with a + fixer session live in it. + +** Next Steps + +Sentry: the stashed port, the per-project declaration model for both channels +and passes (Craig's reframe — work skips personal, home skips work, =.emacs.d= +skips both), the pass inventory and its gaps, and the "one more thing" he +mentioned and never got to say. + +Sync: steps 2 through 4 — narrow the dirty guard per path, add the last-synced +manifest, and write work's line into =cross-project.md=. The manifest is the +primary fix, not the narrowing: =.emacs.d='s five-file measurement showed silent +drift is the ordinary case and the loud failure the exception. + +Also open: scope the five work-bound MCP servers to work-local; velox needs the +Keep token and the same scoping; home needs telling their Keep caveat was wrong; +the nag-event rule needs its corrections before landing; and the =[#A]= +account-binding task may rest on a wrong premise. + +KB: promoted 0 / consulted no + +* Session Log + +** 2026-07-29 10:25 — Startup + +Ran startup.org. rulesets already current, project tree clean at 73e8c01, .ai/ +synced from templates, nothing new to link, no language-bundle drift. No +session-context.org present, so the 06:36 session wrapped cleanly. Inbox has 0 +pending handoffs (lint-followups.org is the standing lint queue, refreshed by +last night's commit — 16 judgment items). Roam inbox 2 items, both unrelated +product captures. Staleness: 6 top-level tasks unreviewed >7 days. Surfaced the +two active reminders (context-engineering rightsizing; the two-week-old sentry +spec read). + +** 2026-07-29 10:30 — Helper-agent question: what's actually live + +Craig asked whether a second helper agent in the same project is in place, for a +dogpile scenario: one agent reads code on main, diagnoses, files Linear bugs; +the other branches, fixes, and queues PRs. + +Investigated rather than answering from the todo body. Findings: + +- Shipped and working: =.ai/scripts/agent-roster= (detection), =helper-mode.org= + (the role contract), and the =AI_AGENT_ID= + =session-context.d/= split. +- Not shipped: =ai --helper=. Verified directly — the launcher's option parser + handles only =-h/--help=, =--runtime=, =--print-launch=, =--print-runtimes=, + =--attach=. No helper flag exists. +- Also not shipped: startup's roster auto-routing branch, the wrap-it-up helper + branch, the hygiene-pass live-helper gate. helper-mode.org's own Status + section says the wiring is behind the three-ring gate and not yet live; the + manual "you are a helper" instruction is the only live adoption path. + +The more important finding is that Craig's scenario doesn't fit helper mode as +designed, for a reason orthogonal to the missing wiring: two sessions in one +clone share one working tree and one HEAD. The diagnoser cannot read main while +the fixer works a branch — checkout is global to the clone. Helper mode's +concurrency design is about two Edit writers on one shared org file, not about +branch isolation. Separately, helper-mode.org bans all git mutation for the +helper, which does map his roles correctly (fixer = primary, diagnoser = +helper), but only after the branch problem is solved. + +The unbuilt =[#B] Repository publish-lock for two sessions sharing one clone= +task (todo.org:210) is the same collision from the index side. + +Craig picked: finish the helper wiring first (launcher + wrap-up branch, pilot, +then decide on the startup net), then design the worktree split. He'll keep the +diagnosing agent read-only on code, which closes the write-collision half. I +noted that read-only doesn't make it read =main= — it reads whatever branch the +clone has checked out — and that's what the worktree work fixes. + +** 2026-07-29 11:05 — Slice A + B built: ai --helper and the wrap-up branch + +Green baseline first: =make test= exit 0 before touching anything. + +TDD throughout. Wrote =scripts/tests/ai-launcher-helper.bats= (23 tests) red, +then built against it. The Makefile glob-discovers =scripts/tests/*.bats=, so +the new file is actually covered rather than silently skipped. + +Launcher (=claude-templates/bin/ai=): =--helper = flag, =_helper_launch_mode= +pure core, =_helper_id= (=helper-=), =build_helper_instructions=, +=_resolve_helper_launch= (runs the project's =agent-roster=), and =helper_mode=. +Two deliberate departures from =single_mode=: it never focuses an existing +window (a second session is the whole point, and focusing the primary's window +is the one outcome that can't be what was asked for), and it never runs git prep +(pulls are primary-only). =_order_windows= now matches a =:= helper +window on its colon prefix so it sorts beside its project. + +Roster verdict → decision: exit 1 (others live) → helper; exit 0 (alone) → +warn and fall through to a normal primary launch; exit 2 or script absent → +warn and launch a helper anyway. Unverifiable resolves toward helper on purpose: +a helper that turns out to be alone merely does less, while a primary that turns +out not to be alone runs pulls and rsync under a live session. + +*Real bug caught by shellcheck, not by my tests.* =_resolve_helper_launch= built +the roster path in the same =local= statement that assigned =dir=, which bash +does not support — verified directly: =local a="$1" b="$a/x"= yields =b=/x=. +Every one of my tests passed anyway, because both production callers happen to +have their own =$dir= holding the same value, so the wrong lookup returned the +right answer. Split into two =local= statements and added a test that calls the +function with no =dir= in scope; confirmed it fails against the one-local form +and passes against the fix. + +Wrap-up: new Step 0 helper branch in =wrap-it-up.org= (the old sentry guard +became Step 0.5 — no live cross-references to renumber, checked). A helper +re-runs the roster: primary live → archive its own context file to +=sessions/...--.org= and stop, no commit/push/hygiene; +orphaned → full wrap, the git ban lifts; roster unavailable → archive-only, +because leaving work uncommitted is recoverable and committing under a live +primary is not. Exit Criteria gained the helper's shorter list. + +Corrected two docs that claimed this doesn't exist: =helper-mode.org='s Status +section and the =protocols.org= helper paragraph. Both now name what is live and +— more usefully — what isn't: startup has no roster check, so a bare =claude= in +a project that already has a session still runs full primary startup; and the +live-helper gate on file-wide hygiene passes is unbuilt, so a primary's +=todo-cleanup=/=lint-org= run can still clobber a helper's scoped edit. + +Verified: =make test= exit 0, 529 passing. =shellcheck= clean on =bin/ai=. +=sync-check.sh= clean (canonical and mirror both updated). Uncommitted. + +** 2026-07-29 14:30 — Live pilot passed; a helper is running now + +Craig ran =ai --helper ~/code/rulesets= from his own terminal. Everything the +slice was supposed to do, it did: roster found this session (pid 3707800) and +returned the helper verdict, id =helper-1055= assigned and exported alongside +=AI_HELPER=1=, window opened as =rulesets:helper-1055= in the =ai= tmux session, +and the helper opener landed verbatim — the helper read =helper-mode.org= as its +first action rather than running startup. + +Two things checked before calling it a pass, because both looked like defects in +the screenshot and neither was: + +- =.ai/session-context.d/= did not exist when the helper first looked. That's + correct — =session-context-path= is a pure resolver and never mkdirs; creating + the file is step 1 of the helper's own light startup. It had already created + =helper-1055.org= (965 bytes) by the time I checked. +- The helper's first Bash call exited 2. Its own =&&= chain broke on the missing + directory; not a wiring fault. + +The helper's anchor is gitignored (=.gitignore:30= =.ai/session-context.d/=), so +a live helper doesn't dirty my tree. My =git status= still shows exactly the +eight files this session changed. + +*Standing constraint while the helper is live:* I must not run any file-wide +hygiene pass — =todo-cleanup.el=, =lint-org.el=, =wrap-org-table.el=. Nothing +enforces that yet (the live-helper gate is the unbuilt piece), so it's on me, +and it matters most at wrap time when the hygiene step runs by default. + +** 2026-07-29 14:45 — Worktree prototype: the fixer checkout + +Craig's design question: bare worktree so each agent gets its own branch? +Recommended linked worktrees off the existing clone, not bare — a bare layout +moves the path that =ai='s project discovery, =agent-roster='s cwd match, +=inbox-send='s basename resolution, and =make install= all key on, and buys +nothing over a linked worktree. Also recommended inverting his role assignment: +the *writer* needs the isolation, not the reader, so the read-only diagnoser +stays here as the helper and the fixer gets the worktree. + +Created =~/code/rulesets-wrap-org-table= on branch =fix/wrap-org-table= off +73e8c01. My eight uncommitted files stay in this checkout, which is what a fix +branch should look like. + +Verified in the worktree: =.ai/protocols.org= and =.ai/scripts/= present (this +project tracks =.ai/= — 192 files), no session anchor (gitignored, so the fixer +gets a fresh one), the launcher accepts it as a project, HEAD on the new branch, +clean status. + +*Correction to what I told Craig.* I said the worktree's =inbox/= would be empty +because handoffs are untracked. Wrong mechanism: =inbox/lint-followups.org= is +tracked (73e8c01 committed it), so it *does* appear in the worktree. The +conclusion holds for a different reason — =inbox-status= reports 0 pending in +both checkouts, so neither session tries to process it. + +*Scope limit worth naming:* rulesets has no Linear and no GitHub remote, so this +prototype exercises the two-agents-two-branches mechanics but not the +file-a-Linear-bug or open-a-PR half of the scenario. Those live in the work +project. + +** 2026-07-30 — Publish flow: the isolated review earned its keep again + +Step 0 reconcile clean (0/0). Remote is =cjennings.net= only, so the approval +gate applies. Staged eight files and dispatched the adversarial reviewer with +the diff, a one-line claim, and the todo.org task body as the requirement +source — withholding my reasoning, per the Isolation Override. + +Verdict: *Request Changes*, three Important. All three were real, and the first +is the kind of defect the isolation exists to catch — it was invisible in my own +model because I wrote the snippet believing it did what I meant. + +1. *The roster call in my new Step 0 omitted the project root.* =agent-roster= + defaults to =$PWD= and keeps only agents at or inside that root, so a helper + whose shell sits in a subdirectory would not see a primary at the root. That + reports "alone", which my own bullet list reads as *orphaned* — the one + branch that commits and pushes. I had written the sentence calling that the + unrecoverable outcome, and then wrote the call that produces it. Verified + empirically: from =scripts/= the old form was worse than the reviewer said — + the =[ -x .ai/scripts/agent-roster ]= test itself fails there, so it never ran + the roster at all and =$?= was the failed test's 1. Now resolves the root with + =git rev-parse --show-toplevel= and captures rc inside the branch. +2. *helper-mode.org still listed startup's roster check as a live route* in + "When to Use This Workflow" while my new Status block said it doesn't exist. + The stale claim sat above the fold, in the section a session reads to decide + whether it is a helper. Now two routes, with the gap stated inline. +3. *=AI_AGENT_ID= was honored unvalidated and interpolated unquoted* into the + command typed into the pane, so an id carrying =;= or a space would run + something else instead of launching the helper. Worse, the override is + *inherited*: a helper's pane exports the id, so =ai --helper= from inside a + helper reuses the parent's — two agents on one context file, the exact + lost-update shape the contract exists to prevent. Added =_sanitize_agent_id= + and =_resolve_helper_id= (sanitize, then mint fresh when that id's context + file already exists) plus four tests. + +Plus four cheap Minors: the help text claimed the fallback skips git prep (it +doesn't), the rc capture above, =$((RANDOM % 65536))= was a no-op on bash's +15-bit RANDOM, and the help now warns that the roster excludes its caller's +ancestry. + +*My own fix had a bug my own test passed anyway.* The first =_sanitize_agent_id= +used =printf '%s\n'= before =tr -c=, so tr translated the trailing newline into +an underscore and every id gained a trailing =_=. The space test asserted a +substring, which "helper_beef_" satisfies. Caught it only because two other +tests failed. Fixed both the sanitizer and the assertion, which is now anchored +on the following space. + +=make test= exit 0, 532 passing. shellcheck clean. sync-check clean. Sent back +to the same reviewer for round 2; not committed. + +Deliberately not fixed: finding 8, a parked +=working/working-dir-orphan-check/wrap-it-up.org.proposed= still carrying the +old "Step 0: Refuse if sentry is live". Pre-existing, and applying it later +would silently revert this work — surfacing to Craig rather than editing a +parked proposal. + +** 2026-07-30 — Committed 84bd121 + +Craig approved the drafted message. Committed =84bd121= on main, 8 files, 646 +insertions. Author =Craig Jennings =, no attribution. The +pre-commit hook's sync-check passed, so canonical and mirror agree. Tracked tree +clean; the two work handoffs remain as untracked inbox deliveries. Unpushed — +push is its own confirmation. + +** 2026-07-31 — Extraction committed; propagation unblocked + +Committed the extraction after two review rounds. Synced paths verified clean, +so template propagation is unblocked for every project's next startup. + +*Round 1 found a test of mine that provably tested nothing.* The pycache +exclusion test's fixture had no =.gitignore=, so the cache files read as +untracked, guard one fired, the sync never ran, and every "this didn't arrive" +assertion was satisfied by nothing arriving. The reviewer proved it by deleting +all three =--exclude= flags and watching the suite stay green. Fixed by +committing a =.gitignore= into the fixture and asserting the sync *ran* before +asserting what it skipped; re-ran the mutation both ways to confirm it now +bites. + +It also caught my fallback naming a remedy that cannot happen ("the next +successful sync installs it" — the fallback runs *instead of* the sync, so there +is no next one) and found the real out-of-band recovery in =audit.sh=, which I +had not looked for. + +*My own fix for the third finding was wrong on the first attempt.* The +false-success test deleted the canonical inside the live fixture repo, which +registered as staged deletions and tripped guard one — so it exercised the skip +path while claiming to test false success. It failed, which is how I caught it. +Now points =SYNC_RULESETS_DIR= at a nonexistent path. + +Round 2: Approve. The reviewer re-ran the 16-scenario differential +independently, killed every mutant it aimed at the suite including round 1's +survivor, and verified the =audit.sh --apply --force= recovery command actually +does what the message claims. Three Minors, all closed: =--force= added (audit +skips a dirty tracked project without it), the pycache caveat on that recovery +path documented, and the false-success test's negative assertion moved onto a +file that does arrive on a real sync. + +=make test= exit 0, 551 passing. Left uncommitted on purpose: the four parked +handoff directories under =working/=, which want their own commit and do not +block the sync guard. + +** 2026-07-31 — Step one: sync extracted from startup.org and characterized + +Green baseline first (536, exit 0), then extracted the Phase A sync block into +=claude-templates/.ai/scripts/sync-templates=. Faithful extraction — the rough +edges are preserved and characterized rather than fixed in passing, because a +behavior change smuggled into an extraction is unreviewable. + +Proved it faithful *differentially* rather than by reading: ran the old inline +block and the new script against identical fixtures, clean and dirty, and +compared both stdout and a hash of the resulting tree. Byte-identical in both +cases. + +14 characterization tests in =scripts/tests/sync-templates.bats=, glob-discovered +so the gate actually sees them. Two of them pin the incident directly: + +- "ONE dirty file blocks ALL THREE rsyncs" — the 2026-07-30 blast radius in one + assertion. The narrowing change lands by making this test fail on purpose. +- "a locally-edited template is silently overwritten, with no record kept" — + work's regression, pinned. Asserts the output is indistinguishable from an + ordinary sync, that no backup exists, and that nothing anywhere says a local + fix was destroyed. + +=startup.org= now calls the script, with a fallback that *announces* a skip +rather than performing a silent one. First draft of that fallback's prose was +wrong — I wrote that a pre-extraction project "cannot sync itself out of that +state", which is false: the old inline block delivers both the script and the +new startup.org together, so the ordinary rollout never hits the fallback. It +covers only a split rsync. Corrected before it shipped. + +Verified: =make test= exit 0, 550 passing (536 + 14). sync-check clean, exec bit +preserved in the mirror. shellcheck flags one SC2001 style note on the =sed= — +inherited verbatim from the inline block, deliberately not "fixed", since that +would be a behavior-adjacent edit inside an extraction commit. + +*And I immediately re-created today's incident.* =startup.org= is now modified +under the synced paths, so template propagation is blocked again as of this +moment. Same shape as the edit that cost a day, six hours after diagnosing it. +The lesson isn't "remember to commit" — it's that the guard punishes an ordinary +work-in-progress state with a silent, project-wide outage, which is the design +flaw the narrowing fixes. Flagged to Craig rather than left sitting. + +** 2026-07-30 18:35 — My dirty tree was blocking template sync for every project + +Three handoffs: =.emacs.d= at 18:20, work at 18:29, =.emacs.d= again at 18:32 +correcting and escalating its own first framing. Craig is seeing "telega server +died" on every triage run, eight =telega-server= SIGSEGVs on ratio today. + +*My fault, and it ran all day.* The startup guard skips all three rsyncs when +anything under the synced template paths is dirty. My uncommitted =sentry.org= +port sat there through the whole sentry discussion, so *no project received any +template update today* — including home, which started fresh at 08:09 after the +telega fix landed and still didn't get it. Verified: =MM +claude-templates/.ai/workflows/sentry.org= was the only dirty synced path. +Stashed it (=stash@{0}=, named), synced paths now clean, propagation unblocked. + +Holding a synced-path edit uncommitted has a cost I did not price. The +discussion was worth having; leaving the file dirty across it was not. + +*The canonical itself is fine* — =.emacs.d= said so and I confirmed: the +=loadChats= fix is in (=(telega--loadChats '(:@type "chatListMain"))=), the +=telega-server-live-p= dead-server signal is present (8 sites), and the +truncation claim work asked about is *already retracted* at :246-251, crediting +work's own 2026-07-28 wire-level disproof. Both of work's content asks were +already satisfied. + +*The systemic finding, which is the real one and is in my area.* Two failure +modes, and the second is worse: + +1. (=.emacs.d='s first framing) A correctness fix to a synced file has no path + to a running session — sync is startup-only. +2. (work's, from experience) The rsync runs =--delete=, so a local patch to a + rulesets-owned file is *silently reverted* at the next startup by whatever + the canonical held at that moment. A project that did everything right — + hit the bug, diagnosed it, patched locally, logged the patch, restarted — + comes back running the broken version with its log still asserting it was + fixed. That is what happened to work, and it is why three projects believed + this was handled for two days. + +=.emacs.d= is right that a freshness check addresses only its own version. +Nothing currently notices the loss at the moment it happens, because that moment +looks routine and successful. + +=84bd121= still unpushed, so velox will not see the helper work. + +** 2026-07-30 17:35 — The CUI exposure is at the MCP registry, not the declaration + +Work replied to the =:TRIAGE_SOURCES:= defect: fixed their side (cmail dropped +with the reason written in, telegram kept deliberately because Kostya and Vrezh +carry real work traffic there, personal-calendar under review for conflict +checks), and widened the requirement with Craig's words — with CUI in play, +work email, calendar and the rest are off limits to *every other* project. The +reverse direction is the one carrying compliance weight: a personal or tooling +project reading work channels is an exposure question, not a tidiness one. + +They also hit the run-time-read trap the hard way: an auto-triage cron armed +twenty minutes earlier had the source list baked into its prompt, so correcting +the declaration did nothing until they re-armed it. Both their spec suggestions +accepted — deny-by-default for work-account sources, and read the declaration at +run time rather than caching it. + +*Then the sweep turned up something bigger than their note assumed.* Every MCP +server is registered *globally*, with no project scoping anywhere: +=slack-deepsat=, =google-docs-work= and =linear= are directly callable from +home, =.emacs.d=, =.dotfiles=, rulesets, and every other project. This session, +in rulesets, has all three available right now. + +So =:TRIAGE_SOURCES:= governs one consumer — which plugins the triage engine +loads — while the exposure sits at the registry. A correct declaration in every +project would not close it. Any deny-by-default rule for work sources has to +reach the MCP registration layer or it is a rule about the front door written +while the side door stands open. + +Not proposing the fix: project-scoping those servers is a real change with real +tradeoffs (several are useful outside work, and the calendar server manages both +accounts in one place). Craig's call, and bigger than the declaration spec. +Replied to work with the sweep and credited the finding to their handoff. + +** 2026-07-30 — google-keep MCP was broken, not absent; fixed on ratio + +Home's handoff claimed the google-keep MCP "shows as a connected server but +exposes no tools", so a note cannot be created, coloured or pinned +programmatically, and recorded a manual wl-copy workaround in the vocabulary +rule. Craig pushed back: he and home were writing to each other through Keep +notes on vacation last month. He was right. + +*Root cause.* The server is registered globally in =~/.claude.json= as =uvx +--from keep-mcp python -m server.cli=, credentials present. It crashes at import +with =ModuleNotFoundError: No module named 'mcp.server.fastmcp'=. keep-mcp 0.3.1 +imports FastMCP from there; uvx resolves =mcp= to 2.0.0, which dropped that +module (confirmed by listing =mcp.server='s submodules — no =fastmcp=). The +process dies instantly, so a client sees connected-with-no-tools. Not a missing +integration; a dependency regression from =mcp= going 2.0. + +*Fix, applied and verified on ratio.* Pin to =mcp<2=. Re-registered through +=claude mcp remove= / =add --scope user= rather than hand-editing +=~/.claude.json=, because the live session owns that file and would clobber a +manual edit. =claude mcp list= now reports =✔ Connected= where it previously +reported =Failed to connect — Connection closed=. Config backed up first. + +*The capability is fuller than the rule assumed.* 23 tools, including +=create_note=, =update_note=, =find=, =pin_note= and =set_note_color=. So the +entire nag-note spec automates — colour and pin included, the two things home +said had to be handed to Craig by hand. The manual workaround comes out of the +rule. + +*Colour caveat.* Keep has 12 fixed colours: White Red Orange Yellow Green Teal +Blue DarkBlue Purple Pink Brown Gray. There is no "burnt orange" — =Orange= is +the match (Keep's Orange swatch renders burnt). + +*Not done, and why.* MCP servers load at session start, so the tools are not +callable in this session — a restart is needed before any note can be written. +And velox has *no* google-keep entry at all, not a broken one, so it needs +first-time registration including the master token. That is a credential moving +between machines, so I stopped and asked rather than pushing it over tailscale. + +The pin is a stopgap. Upstream keep-mcp needs =mcp= 2.x support, worth filing so +the pin does not become permanent and unexplained. + +** 2026-07-30 — Review of the sentry port: Request Changes, two for Craig + +The isolated reviewer returned *Request Changes* on the port. Verified every +factual anchor myself before surfacing, and all of them hold. + +Fixed (my own defects, both in the paragraph whose subject is a mis-stated +mechanism, which is its own lesson): + +- *I fabricated this file's history.* My line 92 said "an earlier version of this + file claimed a detached schedule hits a headless-auth wall". It never did — + =git log -S'headless'= over both copies returns zero commits. I carried work's + self-description across without checking it against the file it was going + into, which is precisely the error I told work they had made. Now says the + file prescribed =/loop= without stating why, and points at + =triage-intake.org=, where the reasoning actually lives. +- Line 89 claimed the first three constraints "were wrong in this file". Two + were simply absent. Now "none of them were recorded here before 2026-07-30". +- Incomplete rename at :61 and :127, which still called sentry's mechanism a + loop after the diff purged that word everywhere else. + +Two that need Craig, both real: + +1. *Sequencing hazard (Critical).* The new pass 3 turns on unattended mail + hygiene — mark-read, star, trash — across every channel. The =[#A]= + account-binding guard that exists because of the 2026-07-23 wrong-inbox + incident is still a *parked* prepared diff: + =working/triage-account-guard/proposed.diff=, and + =triage-intake.personal-gmail.org= has zero account checks. That incident is + exactly this: a sentry triage cycle bound to the work Gmail account instead + of personal, 201 messages, graded =[#A]= on severity alone. Commit =33949c5= + ("exclude mail and messengers") is what made it structurally unreachable, and + this diff removes that protection while the deliberate replacement sits + unapplied. +2. *A third ruling nobody reconciled.* =todo.org:198= records Craig's + *2026-07-27* correction — scan DeepSat work mail and every messenger, + *excluding only personal email*. Work's 2026-07-30 note reverses to "read + everything" and never mentions 07-27. So there are three rulings, and the + newest may have been given without the middle one in view. + +Not committed. Staged and held. + +** 2026-07-30 — Ported work's sentry corrections; inbox cleared + +Craig confirmed he gave work the pass-3 ruling and asked to see the port before +it lands. Ported rather than applied either file, since both reverted yesterday's +=fire= → =cycle= rename (=f3f5bfd=). + +Six changes to =sentry.org=: overview and interval trigger off the loop +language; step 7 rewritten for =CronCreate= with both shapes; Stop Sentry to +=CronList= / =CronDelete=; the lock model noting overlap can't arise under +self-rescheduling; pass 3 to "read everything, send nothing". + +Three things beyond a straight port. Work's constraint list had three items; I +made it five, adding two facts from =CronCreate='s own docs that neither copy +carried — jobs fire only while the session is idle and never mid-query (so a +long cycle delays its own successor without the lock), and the scheduler's +jitter (up to 10% of period, capped 15 min, so an hourly sentry can land six +minutes late and that is not a stall). Also noted =durable= has no effect, since +someone will reach for it on reading that jobs die with the session. + +And I fixed a real defect from yesterday's rename: canonical pass 3 ended "they +never cycle unattended", which had been "never fire unattended" — the rename +caught the verb. Checked the whole file; it was the only one. + +Verified: lint-org 0 mechanical / 0 judgment, sync-check clean, 80 "cycle" and +zero "fire" as a noun. Uncommitted, awaiting Craig. + +Inbox dispositioned per =inbox.org= park: all four handoffs moved to +=working/sentry-arming-correction/= alongside the prepared diff. Inbox back to +zero. + +** 2026-07-30 07:43 — Work's 07:39 correction: pass 3 is about sending, not reading + +Work sent a second pair at 07:39, three minutes after the first and before my +07:40 reply landed. The correction: Craig told them this morning that the +2026-07-21 pass-3 ruling was that sentry must never *send* via mail or +messenger, never about reading. Their revised pass 3 is "read everything, send +nothing" — full triage across every source, with local-state mail hygiene +(mark-read, star, trash) allowed because it emits nothing, and anything outbound +(a send, a Slack or Signal message, a ticket comment or state move, a posted PR +review, a calendar RSVP) queued for morning approval. + +The framing is good and the outbound/local-state line is the right one. But it +is a policy reversal on a synced shared asset, resting on a ruling given in a +session I was not in and cannot verify from here. Asking Craig directly rather +than applying it. + +The rename conflict persists in the corrected copy: 84 "fire" against 1 "cycle". +They sent before my 07:40 reply, so they had not seen it. Replied acknowledging +the supersession, restating the rename finding, and stating both files are held. + +Net: =CronCreate= substance accepted and verified, pass 3 waiting on Craig, +neither file applied. + +** 2026-07-30 07:40 — Inbox: work's sentry.org arming correction + +Two handoffs from work at 07:36: a prose note plus an edited =sentry.org=, +arguing the arming mechanism was documented wrong (=/loop=, justified by a +headless-auth wall) and should be =CronCreate=. + +Shared-asset proposal, so skeptical review rather than silent application. + +*Substance verified and correct.* Checked all three claims against +=CronCreate='s own tool documentation: jobs are session-only and in-memory and +die with the session; recurring jobs auto-expire after seven days with one +final fire; fires run inside the arming session, hence inherited MCP auth. +Their overnight 2026-07-28/29 evidence matches. The off-minute guidance they +added is verbatim from the tool docs, which is a good sign they wrote from the +source rather than from memory. + +*The file is the problem.* Their copy predates =f3f5bfd= (2026-07-29, +"refactor(sentry): call one loop cycle a cycle, not a fire") — Craig's decision, +72 sites. Their version uses "fire" 85 times and "cycle" once; the canonical is +the inverse. Applying it wholesale reverts yesterday's rename the day it landed. +So: port their step 7, Stop Sentry, and overview changes into the canonical's +=cycle= vocabulary. Parked pending Craig — sentry.org is synced to every +project. + +Two facts from the tool docs neither version carries, worth adding in the port: +jobs fire only while the REPL is idle, never mid-query, so a long cycle delays +its successor on its own; and the scheduler adds jitter (up to 10% of period, +capped at 15 min). + +Agreed with their pass-3 catch — they nearly overturned Craig's 2026-07-21 +mail/messenger exclusion by inferring a technical cause for a policy ruling, and +caught themselves. Left as his call. + +Disagreed with their companion note. =triage-intake.org:210= says auth is +inherited because the sweep runs in the live session, and that the wall applies +to a *detached* cron run — still true of a system cron. Narrower than sentry's +claim was, so not wrong, though it does prescribe =/loop= where =CronCreate= +would also work. Smaller follow-up, not folded in. + +Replied to work with all of the above. Not applied. + +** Pilot prerequisites (recorded 14:30) + +Prerequisites for the pilot, learned by testing rather than assuming: the roster +excludes its caller's own process ancestry, so =ai --helper= must be run from a +separate terminal — from inside this session (or via =!=) it can't see me and +silently downgrades to a second primary. And the =ai= script uses a tmux session +named =ai= while Craig's live agents run in =aiv-= from ai-term.el, so +the helper opens in a different session and switches his client. Pre-existing +launcher behavior; the Emacs surface is a separate handoff to =.emacs.d= per the +task. diff --git a/working/nag-event-vocabulary/2026-07-30-1646-from-home-vocabulary-to-spread-craig-named-a.org b/working/nag-event-vocabulary/2026-07-30-1646-from-home-vocabulary-to-spread-craig-named-a.org new file mode 100644 index 0000000..cd1d190 --- /dev/null +++ b/working/nag-event-vocabulary/2026-07-30-1646-from-home-vocabulary-to-spread-craig-named-a.org @@ -0,0 +1,15 @@ +#+TITLE: Vocabulary to spread: Craig named a pattern today and asked +#+SOURCE: from home +#+DATE: 2026-07-30 16:46:50 -0500 + +Vocabulary to spread: Craig named a pattern today and asked that it reach every project rather than living only in home. + +The words are 'nag event', 'nag deadline', and 'nag me about this'. Any of them means build this exact construction without asking for the shape again. + +The calendar event: an all-day event on the deadline date, in his primary personal calendar (Google Calendar today, but follow whatever the primary personal calendar is in future). Notifications on each of the three preceding days and again on the deadline itself, so four in total. The subject is the action phrased as the thing to do, so 'Return shoes' rather than 'Shoe deadline'. The description carries a link to a Google Keep note of the same title. + +The Keep note: a text note, explicitly not a checklist. Same title as the event so the pair reads as one thing. The goal goes first, before anything else, so opening it cold tells him what he is trying to achieve. Every step on its own line. Detailed enough to finish the task without looking anything else up, which means order numbers, measurements, decision rules, links and dates all live in the note. Where no steps are planned yet, write the recommendation in the same shape instead. + +The reason it is built this way is worth carrying with the rule. His failure mode on deadlines is not forgetting they exist, it is deferring the decision until the window closes and then owning the outcome by default. Four notifications across four days beat one ping, and the linked note removes the 'I would have to work out what this involves' friction that causes the deferral. + +Origin was a shoe return window on 2026-07-30. I have it in home's memory; sending it here so it is available wherever he says the word. diff --git a/working/nag-event-vocabulary/2026-07-30-1653-from-home-amendment-to-the-nag-event-vocabulary-i.org b/working/nag-event-vocabulary/2026-07-30-1653-from-home-amendment-to-the-nag-event-vocabulary-i.org new file mode 100644 index 0000000..3b5162b --- /dev/null +++ b/working/nag-event-vocabulary/2026-07-30-1653-from-home-amendment-to-the-nag-event-vocabulary-i.org @@ -0,0 +1,11 @@ +#+TITLE: Amendment to the nag-event vocabulary I sent earlier today. +#+SOURCE: from home +#+DATE: 2026-07-30 16:53:42 -0500 + +Amendment to the nag-event vocabulary I sent earlier today. Craig added two elements to the Keep-note half of the pattern. + +The note is coloured burnt orange, and the note is pinned. Both are part of the spec rather than decoration. The colour makes nag notes identifiable at a glance among his other Keep notes, and pinning keeps a live deadline at the top where it belongs rather than sinking down the list. + +So the full Keep-note spec is now: text note not checklist, same title as the calendar event, goal stated first, every step on its own line, detailed enough to finish without looking anything else up, burnt orange, pinned. + +One practical note for whoever implements this. The google-keep MCP currently shows as a connected server but exposes no tools, so a note cannot be created, coloured or pinned programmatically. The workaround today was to write the note text, put it on the clipboard with wl-copy, and hand the colour and pin steps to Craig, then ask him for the note URL because the calendar event description needs it. Worth flagging in the rule so nobody assumes the automation exists. diff --git a/working/sentry-arming-correction/2026-07-30-0736-from-work-sentry-org-the-arming-mechanism-was.org b/working/sentry-arming-correction/2026-07-30-0736-from-work-sentry-org-the-arming-mechanism-was.org new file mode 100644 index 0000000..d73a237 --- /dev/null +++ b/working/sentry-arming-correction/2026-07-30-0736-from-work-sentry-org-the-arming-mechanism-was.org @@ -0,0 +1,22 @@ +#+TITLE: sentry.org: the arming mechanism was wrong, and it made sent +#+SOURCE: from work +#+DATE: 2026-07-30 07:36:49 -0500 + +sentry.org: the arming mechanism was wrong, and it made sentry unusable as documented. + +Sending the edited .ai/workflows/sentry.org from the work project. The canonical needs the same change so every sentry-capable project gets it. + +What was wrong. Entry step 7 said to arm with /loop, and justified it by saying a detached schedule hits a headless-auth wall for MCP. That reasoning is sound for a system cron or at job spawning a fresh headless agent, but it is not what we actually use. CronCreate schedules a prompt inside the arming session, so MCP auth is inherited. The work project proved this overnight on 2026-07-28/29: cron-driven triage sweeps read Gmail, Slack, Linear and both calendars for hours. + +The practical cost of the error: I told Craig this morning that I could not arm sentry because /loop had to come from him. That was wrong, and it is the second time the doc's stated mechanism diverged from his actual practice, which has been at or cron with each round scheduling the next. + +What changed: + +1. Step 7 rewritten for CronCreate, documenting both shapes. Recurring, where the single-runner lock covers overlap. And self-rescheduling one-shots, which is Craig's established practice and makes serialization structural rather than lock-dependent, with the interval measured from end-of-fire rather than wall clock. +2. Three constraints added that the file did not carry and that decide how sentry can be used at all. CronCreate jobs are session-only and in-memory, so sentry survives exactly as long as the session that armed it and closing the terminal ends it. Fires run inside that session, hence the inherited auth. Recurring jobs auto-expire after seven days. +3. Stop Sentry now says CronList then CronDelete, with a check that no sentry job survives, rather than 'stop the /loop (ScheduleWakeup stop)'. +4. The overview and the interval trigger reworded off loop language. + +One thing I deliberately did not change, and it is worth a second opinion. Pass 3 excludes mail and messenger sources on Craig's 2026-07-21 ruling. My first draft of the new section asserted that inherited auth means the pass is no longer restricted, which silently overturned that ruling by inferring a technical cause for a policy decision. I caught it and replaced it with a note saying the exclusion is a choice rather than a wall, so nobody reasons from 'sentry cannot reach Gmail'. If the canonical wants the exclusion revisited now that the auth story is clear, that is Craig's call rather than a doc edit. + +Companion file to reconcile: nothing in the same directory references the arming mechanism, but a grep for '/loop' across the templates would be worth running, since triage-intake.org's auto mode carries the same headless-auth claim in its 'Trigger and delivery' section and is likely wrong for the same reason. diff --git a/working/sentry-arming-correction/2026-07-30-0736-from-work-sentry.org b/working/sentry-arming-correction/2026-07-30-0736-from-work-sentry.org new file mode 100644 index 0000000..2a57d17 --- /dev/null +++ b/working/sentry-arming-correction/2026-07-30-0736-from-work-sentry.org @@ -0,0 +1,244 @@ +#+TITLE: Sentry — Overnight Hygiene Supervisor +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-19 + +* Overview + +Sentry is a scheduled interval sweep (=CronCreate=, armed in and bound to the arming session) that keeps a project's hygiene current while Craig is away. Each fire walks a fixed list of passes — roam pull, inbox zero, triage (no mail or messengers), todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness, bug and refactor finding, and (opt-in) solo-task implementation — and commits each pass's writing to a throwaway daily branch. Nothing pushes. In the morning Craig reviews the branch, squash-merges what he wants, and deletes it. + +The design goal is a project that greets the morning already tidy, with every judgment call and every destructive action parked in an approval queue rather than executed unattended. Sentry does the mechanical sweeping; Craig does the deciding. + +This file is the engine. It owns the entry gates, the branch mechanics, the lock model, the per-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 " (sets the fire interval) + +Stop trigger (see Stop Sentry below): + +- "stop sentry", "stand down sentry", "sentry off" + +Sentry is deliberately *not* auto-armed. Running it in a project is a per-project grant (the =:COMMIT_AUTONOMY:= marker) plus a deliberate launch with Craig at the terminal for the entry gates. + +* Prerequisite — the autonomy ticket + +Sentry commits unattended. =commits.md= gates commits on Craig's approval, so sentry needs standing, per-project authorization to run at all. Before anything else, read the project's =.ai/notes.org= Workflow State block for: + +: :COMMIT_AUTONOMY: yes + +If the marker is absent or not =yes=, decline to start and name the marker: + +: Sentry needs ":COMMIT_AUTONOMY: yes" in .ai/notes.org Workflow State to run — it commits unattended. Add it to grant, or run the hygiene passes by hand. + +No half-running mode: a project without the grant doesn't run sentry's read-only passes either. The grant is one line away, so this is a deliberate opt-in, not a barrier. + +A second, *independent* marker gates the solo-task implementation pass (pass 12): + +: :SENTRY_MAY_IMPLEMENT: yes + +=:COMMIT_AUTONOMY:= lets sentry commit its hygiene sweeps to the branch; =:SENTRY_MAY_IMPLEMENT:= additionally lets it implement solo, decision-free backlog tasks on the branch. The split exists because the two carry different morning costs: hygiene is a two-minute merge, implemented code is a review session. A project can run hygiene-only sentry without the implement pass, and most should until sentry has quiet weeks behind it. Absent =:SENTRY_MAY_IMPLEMENT:=, pass 12 skips; sentry still runs every other pass. Requires =:COMMIT_AUTONOMY:= alongside it — implementing implies committing. + +* Entry — interactive, with Craig present + +Craig types the sentry trigger, so the first moves run with him at the terminal. Do them in order; each gate that fails stops entry until Craig answers. + +1. *Autonomy ticket* — the prerequisite above. Absent → decline and stop. + +2. *Dirty-tree gate.* =git diff --quiet HEAD= (tracked modifications only; untracked and gitignored files never block — an inbox drop or scratch file is not in-progress work). If the tracked tree is dirty, describe what's dirty and offer, inline-numbered per =interaction.md=: + + 1. Finish the job — commit the in-progress work first (recommended if it's a coherent unit) + 2. Stash it — =git stash= and start sentry on a clean tree + 3. Roll back named changes — discard specific files (names them) + + Wait for an answer. Sentry can't start unattended from a dirty state; that's the point. + +3. *Green-suite gate.* Run the project's full suite (=make test=, or the project's equivalent — detect it). Read the output. If anything is red, describe the failures and offer to investigate before arming. The loop starts only on a green baseline, because every unattended 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 schedule.* Use *=CronCreate=*, not =/loop=. Default hourly; Craig's "every " phrase overrides. The fire prompt is one sentry fire (the Pass Runner below), written so a fresh context can execute it — it names this file, so the fire re-reads the workflow rather than relying on a conversation it cannot see. + + Two shapes, both valid: + + - *Recurring* (simplest): one =CronCreate= with =recurring: true= and a cron expression at the interval. The single-runner lock below handles the overlap case, so a long fire never gets a second fire on top of it. Stop by deleting the job. + - *Self-rescheduling* (Craig's established practice): =recurring: false=, and the last step of each fire creates the next one-shot job. Serialization is then structural rather than lock-dependent, and the interval measures from end-of-fire instead of wall clock, so a slow fire doesn't compress the next gap. Stopping is simply not rescheduling. + + Pick an off-minute rather than =0= or =30= (=7 * * * *=, not =0 * * * *=). + + Confirm the arming in one line: mechanism, interval, branch name, project. + + *Three constraints that decide how sentry can be used, and they were wrong in this file until 2026-07-30:* + + - *=CronCreate= jobs are session-only and in-memory.* They are not written to disk and they vanish when the Claude session exits. Sentry therefore survives exactly as long as the session that armed it. Tell Craig this at arming time: closing the terminal ends sentry, and the branch is left wherever the last fire got to. + - *Fires run inside the arming session, so MCP auth is inherited.* This is why sentry's triage pass can reach Gmail, Slack, Linear and the calendars at all. An earlier version of this file claimed a detached schedule hits a headless-auth wall and prescribed =/loop= to avoid it; that reasoning applied to a *system* =cron= or =at= job spawning a fresh headless agent, and it does not apply to =CronCreate=. Proven in the work project overnight on 2026-07-28/29, where cron-driven triage sweeps ran mail, Slack, Linear and calendar reads for hours. + - *Recurring jobs auto-expire after seven days*, firing one last time before deletion. Irrelevant for an overnight run; it bounds anything longer. + + Note what this does *not* change. Pass 3 still excludes mail and messenger sources, because that is Craig's ruling of 2026-07-21 and not a capability limit. Inherited auth means the exclusion is a *choice* rather than a wall, so don't reason from "sentry can't reach Gmail" — it can, and it is told not to. Overturning that needs Craig saying so for a given run, not an inference from this section. + +* 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-=, where == 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 the schedule 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. (Under the self-rescheduling shape this cannot arise, since the next job is only created once the current fire ends; the lock is the belt-and-braces for the recurring shape.) 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): =. 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-=) — 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/-=. If the schedule 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: 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//= directories whose backing task is closed (a filing candidate per =working-files.md=). Probe: a =working/= directory exists. The filing itself queues (it's a judgment move). + +7. *Spec status board* — the =docs-lifecycle= grep for spec keywords, surfacing any =DOING= spec whose bound build parent is closed. Probe: =docs/specs/= exists. + +8. *Link integrity* — broken =file:= links in the project's org files, via =lint-org.el=. Probe: =lint-org.el= present. Report-only into the digest; no unattended rewrites. + +9. *Git health* — uncommitted drift, unpushed commits on other branches, stale branches, main-behind-origin. Probe: =.git=. Report into the digest. + +10. *Prep + symlink freshness* — stale daily-prep docs, broken symlinks. Probe: the prep dir / symlinks exist (work and home only, in practice). + +11. *Bug and refactor finding* — hunt for real bugs and worthwhile refactoring opportunities in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per 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 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 bug as a graded task in =todo.org= per the severity × frequency matrix (=todo-format.md=), and each refactoring opportunity as a =:refactor:= task, deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. *Find, never fix in this pass* — the finding files a task and stops. A fix happens only in the opt-in implementation pass below, and only after the finding is a filed task that pass then re-verifies from scratch (see the premise rule there). A freshly-found "bug" can be a misread — one was filed and retracted two fires apart on 2026-07-23 — so the file-then-verify-then-fix pipeline is deliberate: the task is the checkpoint, not a same-breath fix. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles; refactor-finding added 2026-07-24.) + +12. *Solo-task implementation (opt-in — =:SENTRY_MAY_IMPLEMENT:=)* — work the backlog's solo, decision-free tasks on the branch. Probe: =.ai/notes.org= Workflow State carries =:SENTRY_MAY_IMPLEMENT: yes= *and* the project holds =:COMMIT_AUTONOMY:= (the implement pass commits). Absent the marker, skip — this pass is off by default, because it turns the morning from a two-minute merge into a code review, and that's the project owner's call. When on: invoke =work-the-backlog.org= under its unattended-loop contract (no pre-flight Q&A — there's no Craig overnight), eligibility =TODO= + =:solo:=, with the defer checklist deciding act-vs-file. The overnight-only tightening: only the *ready* bucket implements (clears every checklist item with zero open decisions); a task needing even one quick decision defers to a =VERIFY= rather than guessing, exactly as the loop caller already does. Commit each logical change to the sentry branch; *never push* — the morning review and merge is the gate, same as every other pass. The full quality bar holds (TDD, suite green before each commit, =/review-code=, =/voice=), and =/review-code= here runs the *premise check first*: reproduce the bug or confirm the problem is real before judging the diff. The review is the fact-checker that a filed claim never got, and it is what makes fixing-on-a-branch safe (Craig, 2026-07-24). A task that fails its premise check is not implemented — the finding was wrong, and that outcome is a digest line, not a commit. (Added at Craig's direction 2026-07-24: overnight implement-on-branch, gated and never-pushed.) + +(KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.) + +* 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 —