aboutsummaryrefslogtreecommitdiff
path: root/.ai
diff options
context:
space:
mode:
Diffstat (limited to '.ai')
-rw-r--r--.ai/sessions/2026-07-29-14-29-helper-1055-launch-pilot-idle.org62
-rw-r--r--.ai/sessions/2026-07-31-06-32-helper-agents-mcp-scoping-and-sync-extraction.org656
2 files changed, 718 insertions, 0 deletions
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 <dir>= flag, =_helper_launch_mode=
+pure core, =_helper_id= (=helper-<rand4>=), =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 =<project>:<id>= 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/...-<id>-<description>.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 <c@cjennings.net>=, 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-<project>= 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.