diff options
Diffstat (limited to '.ai')
| -rw-r--r-- | .ai/sessions/2026-07-29-14-29-helper-1055-launch-pilot-idle.org | 62 | ||||
| -rw-r--r-- | .ai/sessions/2026-07-31-06-32-helper-agents-mcp-scoping-and-sync-extraction.org | 656 |
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. |
