From 01356fa036847f9d216dd92a006049105c2d5461 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Thu, 23 Jul 2026 08:25:40 -0500 Subject: chore: file eight bug findings and stage four shared-asset proposals Overnight hygiene sweep of this repo. Everything here is a finding or a staged proposal. No rule, workflow, script, or bundle file was modified. The one that matters: the python and typescript language bundles ship no pre-commit hook, so a project installing either gets no credential scan on commit. Both bundles predate the hook rollout that swept go and bash, and install-lang skips missing components without a word, so it went unnoticed for two months. Live on two projects, one of them work. Filed [#A]. Seven more findings landed. The largest is a non-atomic write in the cross-project inbox tool, which can strand an empty handoff in another project's inbox and block a turn there. The rest are lint noise and two gaps in the Signal channel. An eighth was filed and then retracted the same night, once a retest showed I had compared two different files and read the difference as a bug. Four proposals from other projects are staged under working/ with verified diffs, each behind a VERIFY task, waiting on a decision. --- .ai/session-context.org | 214 +++++++ ...ill-memory-sweep-handoff-2026-06-10-done-in.org | 5 + ....dotfiles-ack-from-dotfiles-both-amendments.org | 5 + todo.org | 105 ++++ .../interaction-no-markup/interaction.md.proposed | 70 +++ .../proposal-from-org-drill.org | 5 + working/interaction-no-markup/proposed.diff | 11 + working/notes-template-lint/notes.org.proposed | 86 +++ .../notes-template-lint/proposal-from-smoke.org | 5 + working/notes-template-lint/proposed.diff | 38 ++ .../feedback-from-archangel.org | 5 + .../feedback-from-takuzu.org | 5 + working/sentry-living-document/proposed.diff | 32 ++ working/sentry-living-document/sentry.org.proposed | 219 +++++++ working/wrapup-temp-cleanup/proposed.diff | 19 + .../wrapup-temp-cleanup/wrap-it-up.org.proposed | 636 +++++++++++++++++++++ 16 files changed, 1460 insertions(+) create mode 100644 .ai/session-context.org create mode 100644 inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org create mode 100644 inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org create mode 100644 working/interaction-no-markup/interaction.md.proposed create mode 100644 working/interaction-no-markup/proposal-from-org-drill.org create mode 100644 working/interaction-no-markup/proposed.diff create mode 100644 working/notes-template-lint/notes.org.proposed create mode 100644 working/notes-template-lint/proposal-from-smoke.org create mode 100644 working/notes-template-lint/proposed.diff create mode 100644 working/sentry-living-document/feedback-from-archangel.org create mode 100644 working/sentry-living-document/feedback-from-takuzu.org create mode 100644 working/sentry-living-document/proposed.diff create mode 100644 working/sentry-living-document/sentry.org.proposed create mode 100644 working/wrapup-temp-cleanup/proposed.diff create mode 100644 working/wrapup-temp-cleanup/wrap-it-up.org.proposed diff --git a/.ai/session-context.org b/.ai/session-context.org new file mode 100644 index 0000000..e3f539c --- /dev/null +++ b/.ai/session-context.org @@ -0,0 +1,214 @@ +#+TITLE: Session Context — 2026-07-23 +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-23 + +* Summary + +** Active Goal + +Sentry armed for the night on =sentry/2026-07-23-ratio=, hourly. Morning teardown is Craig's. + +** Decisions + +** Data Collected / Findings + +** Files Modified + +** Next Steps + +* Session Log + +** 02:34 — Startup + +Ran startup.org. Rulesets already current; =make install= had nothing new to link; project repo clean and up to date. =.ai/= synced from templates. No prior =session-context.org= — last session (2026-07-20 23:36, signal-pager + triage phone-push) wrapped cleanly. + +Findings surfaced: 13 top-level tasks unreviewed >7 days; roam inbox holds 9 items; 8 unprocessed project inbox handoffs plus =inbox/lint-followups.org=; KB has 101 =:agent:= nodes but no best-practices node resolved and nothing matching "rulesets". Active Reminder from 2026-07-14 (Craig's deep read of the sentry spec) reads stale — sentry has since shipped and dogfooded live in takuzu and archangel. + +Craig's instruction on arrival: finish startup, then begin sentry. + +** 02:35 — Craig-ordered amendments applied before launching sentry + +Two of the eight inbox handoffs are Craig's own orders relayed from the dotfiles session, and both gate a correct sentry run, so they land before the loop starts. The rest of the inbox stays for sentry's own inbox pass. + +Amendment 1 — sentry pass list (=.dotfiles=, 2026-07-21). Sentry never checks email or messengers, and gains a bug-finding pass. The handoff shipped dotfiles' whole edited =sentry.org=, but that copy forked from a pre-silent-until-signal canonical: rsyncing it would have reverted the heartbeat/digest split committed 2026-07-20. Applied their three intended changes onto current canonical by hand instead, and kept the =:TRIAGE_SOURCES:= activation-probe language their Pass 3 rewrite had dropped. + +Reviewed the staged diff before committing and found three defects in my own edit. Pass 11 as Craig worded it ran "the project's linters and suite," which would have violated sentry's own anti-pattern 5 (no per-pass suite run) eleven times a night; it now runs linters and static analysis only and reads the entry baseline's suite result. The Living Document line still said "ten mechanical passes." And the KB-deferral parenthetical called KB promotion "the proposal's eleventh pass," which collided with the real pass 11 — reworded. Suite green (exit 0) before and after. Committed 33949c5, unpushed pending Craig's call. + +** 02:43 — Sentry armed, fire 1 (working) + +Entry gates all passed: =:COMMIT_AUTONOMY: yes=, tracked tree clean, suite green (exit 0, run twice on this content), no prior =sentry/*= branch, zero behind upstream. Branch =sentry/2026-07-23-ratio= created from HEAD. Loop armed hourly at :07 (job ef8eb9d4, session-only, auto-expires in 7 days). Craig's repo working tree belongs to sentry until the morning merge — reclaiming it early means saying "stop sentry". If he has rulesets files open in Emacs, buffers want reverting after the merge. + +Fire 1 digest: + +- P1 roam pull — ran, already up to date. +- P2 inbox zero — ran. Project inbox: 1 of 5 executed (org-drill's memory-sweep completion note, informational, marked PROCESSED + reply sent). 4 park (below). Roam inbox: 9 items, 8 belong to other projects and route at wrap-up, not here; the 1 rulesets item ("every project should have a working and a temp directory") is shipped except for one clause, queued below. +- P3 triage intake — skipped: no project-specific plugin and no =:TRIAGE_SOURCES:= declaration, so no active source. Correct behavior under the activation gate. +- P4 todo cleanup — ran, no-op. +- P5 task audit — deferred to a later fire; takuzu's dogfood note says the full audit is too heavy hourly, and fire 1 already carries the bug-finding load. +- P6 working-files hygiene — skipped: no =working/= directory. +- P7 spec status board — ran. No =DOING= spec with a closed build parent. +- P8 link integrity — ran, and the checker itself turned out to be broken (see P11). +- P9 git health — ran. =main= is 1 ahead of =origin/main= (the amendments commit, unpushed pending Craig's call). Sentry branch correctly has no upstream. No stale branches. +- P10 prep freshness — skipped: no =daily-prep/=. +- P11 bug finding — ran. Rotating area this fire: =lint-org.el= and the synced =.ai/= templates. Three defects verified and filed as graded tasks (commit dc7791d): link resolution against cwd rather than the linted file's directory [#B], todo-format checkers firing on spec files [#C], and the notes.org template tripping four flags in every project [#C]. Two of the three were independently reported by takuzu and smoke tonight, which is what prompted checking them. + +The three bug tasks record their severity x frequency arithmetic in the body, including where the two inputs disagreed, so the grades can be argued rather than just overridden. + +** 02:47 — Finished the park path the fire skipped + +The inbox-boundary Stop hook caught a real gap. Fire 1 wrote the four proposals into the approval queue and stopped there, but =inbox.org='s park path is three things, not one: move the proposal into =working//= beside a prepared diff, file a =[#B]= VERIFY carrying the decision package, and reply to the sender. I'd done the queue entry and none of the rest, so the senders were waiting on nothing and the decisions lived only in a session anchor that gets archived. + +Completed all four. Each =working/= dir now holds the original proposal, a =proposed.diff=, and the full proposed file. Both org-file diffs were verified by linting the proposed version: the notes.org template goes from four flags to zero, and the fix turned out to need one thing smoke didn't identify — the =invalid-block= pair isn't the example content confusing the parser generally, it's the literal =** Feature Name or Topic= line inside the block, which org reads as a heading. A comma-escape on that one line clears both. Also found two more column-0 bold lines beyond the two reported. + +Replies sent to takuzu, archangel, and smoke. dotfiles' ack needed nothing back and is marked PROCESSED. Inbox is at zero. + +** 03:35 — Fire 2 (working) + +Lock acquired, branch state verified. Digest: + +- P1 roam pull — ran, already up to date. +- P2 inbox zero — ran, no-op. Project inbox at zero. Roam inbox unchanged at 9; 8 belong to other projects and route at wrap-up, the 1 rulesets item is already queued. +- P3 triage intake — skipped: no active source surviving the mail/messenger exclusion. +- P4 todo cleanup — ran in check mode across all three passes, zero fixes, zero conversions, zero archives. +- P5 task audit — mechanical subset only, per the parked guidance and takuzu's note that the full audit is too heavy hourly. Staleness is 20, up from 13 at startup, which is just tonight's 7 new tasks arriving never-reviewed. Not a defect. +- P6 working-files hygiene — ran, and this fire is the first where its probe fires, since fire 1's park work created =working/=. All four dirs have open backing VERIFY tasks, so nothing to flag. The pass works. +- P7 spec status board — ran. Seven IMPLEMENTED, two READY (inbox-workflow-consolidation, encourage-kb-contribution). No DOING spec with a closed build parent. +- P8 link integrity — ran across todo.org, notes.org, the anchor, and all nine specs. Two findings, both in the docs-lifecycle spec, both prose containing a bare =file:= that org parses as a bracketless link (=file:→id:= in a sentence about converting link types, and =keep-file:-links-through-pilot= in a hyphenated phrase). Org behaving as documented rather than a defect, so a digest line and no task. +- P9 git health — main still 1 ahead of origin, unpushed, awaiting the approval-queue item. Sentry branch correctly has no upstream. No stale branches. +- P10 prep freshness — skipped: no =daily-prep/=. +- P11 bug finding — rotating area this fire: the =.ai/scripts/= shell helpers. Shellcheck clean on all seven of the sentry-critical ones (agent-lock, agent-roster, capture-guard, flashcard-sync, inbox-status, self-inject.sh, session-context-path). One SC2034 pair in task-review-staleness.sh, verified as a false positive — the two names are positional fields in a =read -r= that exist to put =value= in the right slot. Digest line, not a task. + +*** Retraction: the [#B] I filed in fire 1 was wrong + +The main result of this fire is negative. Fire 1 filed a =[#B]= claiming lint-org resolves =file:= links against the process cwd rather than the linted file's directory. It doesn't, and the task is CANCELLED. + +The claimed evidence was that linting the notes.org template from the repo root reports two siblings missing while linting from its own directory doesn't. The first half was never run. Those findings came from the =/tmp= copy I made while preparing smoke's diff, and in =/tmp= the siblings genuinely are missing, so org-lint was right. I compared two different files and read the difference as a bug, then wrote a fix direction for a mechanism I hadn't checked. + +Caught it here only because P8 surfaced =link-to-local-file= again and I went looking for the checker in lint-org.el to fix it. It isn't there — it's org-lint's own, which contradicted my stated fix and forced the retest. Three runs plus a direct =default-directory= probe settled it. + +Correction sent to takuzu, who had been told about it. Their own finding is unaffected and still filed. The useful residue: both =link-to-local-file= and the todo-format checkers are upstream org-lint checkers, so scoping them means filtering org-lint's output, not editing a local checker — which changes the fix direction on the [#C] task too. + +** 04:35 — Fire 3 (working) + +Lock acquired, branch state clean. Digest: + +- P1 roam pull — ran, already up to date. +- P2 inbox zero — ran, no-op. Both inboxes unchanged. +- P3 triage intake — skipped: no active source surviving the mail/messenger exclusion. +- P4 todo cleanup — ran. Hygiene and convert-subtasks no-op; =--archive-done= moved one subtree, the =CANCELLED= lint-org task fire 2 retracted. Committed 962f3c0. +- P5 task audit — mechanical subset. Staleness 19, down one from the archive. +- P6 working-files hygiene — ran. All four =working/= dirs still have open backing VERIFY tasks. Nothing to flag. +- P7 spec status board — ran. Two READY specs, no DOING with a closed parent. +- P8 link integrity — ran across todo.org, notes.org, and all nine specs. Same two known prose false positives in the docs-lifecycle spec, unchanged. No new findings. +- P9 git health — main 1 ahead of origin, unpushed, still queued. No stale branches. +- P10 prep freshness — skipped: no =daily-prep/=, no broken symlinks. +- P11 bug finding — rotating area this fire: the Python scripts under =.ai/scripts/=. All 15 compile; no Python linter on this box, so the pass became a targeted read of =inbox-send.py=, the script this session has exercised hardest. Three defects verified, filed as two tasks (8995016). + +*** The inbox-send finding + +The one that matters: =inbox-send= writes straight to the destination path, and =write_text= truncates on open, so any mid-write failure leaves a zero-byte =.org= in *another project's* inbox. That phantom isn't inert — =inbox-status= counts it, so it trips the receiving project's boundary hook and blocks a turn there over a file with no content and no sender context. Meanwhile the sender saw an error and retries, so the target collects a second one. + +Reproduced end to end with a non-ASCII message under a C locale with UTF-8 mode disabled: send fails, zero-byte file lands in the destination, =inbox-status= there reports it pending. The encoding case is just the trigger I could force; the defect is the non-atomic write, which a full disk or an interrupted process reaches the same way. Graded [#B], fix direction is temp-file-plus-=os.replace= with =encoding="utf-8"= pinned. + +Two smaller ones grouped as [#C]: =send_file= raises an uncaught =PermissionError= traceback because =main= catches only =ValueError= and =FileNotFoundError=, and =discover_projects= doesn't dedupe, so a roots config naming both a parent and its child lists the same project at two indices. + +Notable that this is the first fire where the rotating area produced findings in code the night's own work depended on. Fire 1 read the linter, fire 2 the shell helpers, fire 3 the script that carried every reply I sent. + +** 05:35 — Fire 4 (working) + +Passes 1 through 10 all quiet: roam already current, both inboxes unchanged, todo cleanup no-op across all three passes, all four =working/= dirs still backed by open VERIFY tasks, two READY specs and nothing stuck, the same two known prose false positives in the docs-lifecycle spec, main still 1 ahead and unpushed, no prep dir and no broken symlinks. Staleness 21, up two from fire 3's new tasks. + +P11 rotating area: =claude-templates/bin/= — the launcher and paging scripts. Shellcheck clean on all four. The findings came from reading and from a config-sanity check, and one of them is a live condition on this machine rather than a latent code defect. + +*** ratio's second Signal account has been cold for 8 days + +=signal-cli listAccounts= on ratio warns that messages were last received 8 days ago. The natural read is that the pager channel has gone stale, which would matter — it's the "text me" path. It hasn't. Established by elimination: ratio holds two accounts, =signal-receive.sh= hardcodes the pager (=+15045173983=) as its only target, and the timer's own journal shows it draining that account cleanly every 15 minutes, the last run 2 minutes before the warning printed. So the cold account is Craig's personal number, and nothing on this machine keeps it warm. + +What I can't establish is whether that matters. The session history describes that registration as note-to-self with no push, and Signal's tolerance for a quiet linked device isn't something I verified. Filed as [#C] with the uncertainty stated rather than graded up on a guess. If it does matter, the fix is a second timer instance rather than a code change, since =signal-receive.sh= already takes an account argument — which makes it a dotfiles handoff, that repo owning the unit. Not sent: the content is speculative and a 05:35 handoff asserting a problem I haven't confirmed would land as noise. + +*** agent-text's direct send is unbounded + +The relay path passes =ConnectTimeout=10= to ssh; the local-account path calls =signal-cli send= with no bound. Since agent-text is invoked by agents, a stall blocks the calling turn with no output and no way to tell a hang from a slow send. Not hypothetical contention — the receive timer holds the same account for ~16 seconds every cadence. Filed in the same [#C]. + +** 06:35 — Fire 5 (working, but the bug hunt came up empty) + +Passes 1 through 10 all no-op: roam current, both inboxes unchanged, todo cleanup clean across all three passes, four =working/= dirs still backed, two READY specs, the same two known prose false positives, main 1 ahead unpushed, no broken symlinks. Staleness 22, up one from fire 4's new task. + +P11 rotating area: =hooks/= and =scripts/= — the machine-wide hooks and the repo's own install and maintenance scripts, the last major uncovered surface. *Zero real bugs.* All five shell hooks shellcheck clean, all Python hooks compile, =hooks/tests= present, =hooks/__pycache__= correctly gitignored with nothing tracked. Six shellcheck findings across =scripts/=, every one dispositioned as a non-bug: + +- =SC2094= (read and write the same file in one pipeline) in =install-ai.sh= and =sweep-gitignore-tooling.sh= — false positive both times. The pattern is a =>>= append with a =[ -s "$gi" ]= stat inside the group. Appending doesn't truncate, and a stat isn't a content read, so the leading-blank-line logic is correct in both the file-exists and file-absent cases. +- =SC2088= (tilde doesn't expand in quotes) in =doctor.sh= and =audit.sh= — both are display strings printed to the user, not paths used for I/O. =audit.sh= says so in a comment on the line above. +- =SC2295= (unquoted expansion inside =${..}=) in =audit.sh= and =diff-lang.sh= — technically correct, no live trigger; =$HOME= carries no glob characters. +- =SC2164= (=cd= without =|| exit=) in =lint.sh= and =status.sh= — =status.sh= already guards its path upstream. =lint.sh= is genuinely unguarded and, since it sets =-u= but not =-e=, a failed =cd= would let it lint the invocation directory instead of the repo root. Confirmed =cd ""= fails rather than silently succeeding, so the hazard is real in shape but needs the running script's own parent to be unreachable. Not reachable in practice; a one-line =|| exit= would close it if anyone touches the file. + +This is the first fire whose hunt found nothing, which is the expected shape — takuzu's dogfood report predicted the rotating hunt goes quiet after the first few fires clear the standing defects. Recording the area covered so the next fire doesn't re-read it. + +*** Two measurement errors this fire, both caught before they became findings + +Worth flagging as a pattern, since fire 1's retraction was the same class. First, a probe loop passed =--convert-subtasks --check= through an unquoted variable; zsh doesn't word-split, so it arrived as one argument and =todo-cleanup= printed a bare =normal-top-level()=. That looked like a tool crash and was my loop. Protocols warns about exactly this. + +Second, chasing that, I read =exit=0= from a bad-flag run and nearly filed a silent-pass defect — a cleanup tool that exits 0 on a bad flag would let =wrap-it-up= believe a pass ran when it didn't. The =0= was =tail='s exit code, not emacs'. Measured without the pipeline, both =todo-cleanup= and =lint-org= exit 255 on an unknown flag and on a missing file, and 0 on success. No defect. + +Three times tonight a conclusion came from a bad measurement. Twice it was caught in the same fire; once (fire 1) it reached a filed task and a sent handoff before the retest caught it. + +** 07:35 — Fire 6 (working) — the night's most consequential finding + +Passes 1 through 10 all no-op again: roam current, inboxes unchanged, todo cleanup clean, working dirs backed, two READY specs, the same two prose false positives, main 1 ahead unpushed, no broken symlinks. Staleness 22, flat. + +P11 rotating area: =languages/= and the bundle install machinery — the last major uncovered surface. Not quiet. + +*** The python and typescript bundles ship no secret-scan hook + +=bash=, =elisp=, and =go= each carry =githooks/pre-commit=, =claude/hooks/validate-*.sh=, =claude/settings.json=, and a seed =CLAUDE.md=. =python= and =typescript= carry none of the four. The pre-commit hook is the credential scanner, so installing either of those bundles gives a project no secret scan on commit — while README's Bundle structure section documents all four as what every bundle follows. + +Verified live rather than inferred, by scanning every project with =.claude/rules/=: =work= (python) and =clock-panel= (python + typescript) both have no =githooks/= and no settings. All four elisp projects have both. =work= is the one that matters — a work repo is where a leaked credential costs most and is likeliest to reach a company remote. + +The history settles intent. Both bundles were added 2026-05-31; =go='s githooks landed 2026-06-02 and =bash='s 2026-06-23. The rollout swept the bundles added *after* these two and skipped these two. And =install-lang.sh= guards its copy with =[ -d "$SRC/githooks" ]=, so the install succeeds and reports nothing missing, which is how this stayed invisible for almost two months. + +Filed [#A], SCHEDULED today — the only [#A] of the night. The fix is find-not-fix per the pass contract, so nothing was changed. The task carries a second half worth as much as the port itself: make =install-lang= warn when a bundle lacks a component the README documents, so the next partial bundle announces itself rather than installing quietly. + +*** Same drift shape, smaller + +=bash='s pre-commit has =cd "$REPO_ROOT" || exit 1=; the =go= and =elisp= copies of that line dropped the guard. Impact is genuinely low — git chdirs to the working-tree root before running a hook, so the checks run against the right tree regardless, and neither script sets =-e=. Filed [#C] for the two-character fix, mostly because the shape is the same as the [#A]: a fix landed in one bundle copy and stopped there. + +Two fires' worth of evidence now says bundle-to-bundle propagation is where this repo leaks changes. + +* Sentry approval queue (2026-07-23) + +Five items. Each names what, why, and the exact edit. Items 2 through 5 are also filed as =VERIFY [#B]= tasks in =todo.org= with prepared diffs under =working/=, so they survive this anchor being archived — say "approve the parked " for any of them. + +** 1. Push main to origin + +What: =git switch main && git push origin main= (then switch back, or leave main checked out if sentry is done). + +Why: commit 33949c5 (the two .dotfiles amendments) is on main and unpushed. The sentry spec called this out — an unpushed commit on main diverges across ratio and velox and breaks the next startup fast-forward. It's ahead-only, so the push is clean. Held because =commits.md= requires explicit confirmation before any push. + +** 2. interaction.md — remove the fenced-code-block carve-out (org-drill) + +What: in =claude-rules/interaction.md=, the "No Reverse-Video Highlighting in Chat Output" rule currently says fenced code blocks "are acceptable when the user explicitly wants a block to copy". Replace that sentence with a plain-text-always statement covering fences as well. + +Why: org-drill relayed Craig's 2026-05-30 direction ("always always list it out without markup"), saved there as a project memory. The rule as written contradicts it, and the directive belongs in the shared rule rather than one project's memory. It's a convention change to an always-on rule, so it parks rather than lands. + +Note: this session violated the tightened form of the rule several times already (fenced blocks in chat), which is evidence for the proposal rather than against it. + +** 3. sentry.org Living Document — fold in the two dogfood runs (takuzu, archangel) + +What: append to sentry.org's pass list and notes: (a) make the rotating-angle bug hunt an official pass — already done tonight as pass 11, so this reduces to noting takuzu's corroboration; (b) add randomized property sweeps as a sanctioned quiet-fire activity; (c) note that =todo-cleanup --archive-done= touches =.gitignore= on its first archive, so an "org-only" pass can produce a real commit and trigger the fire-end suite; (d) note that in a project gitignoring =.ai/=, quiet fires produce zero commits, so =git log main..sentry/*= understates the night and the anchor's heartbeat list is the only record; (e) replace the per-fire full task audit with a mechanical subset hourly plus judgment items queued once nightly. + +Why: two independent first-live-run reports, no engine defects in either. Item (e) matches what fire 1 did by instinct (P5 deferred). sentry.org is a shared synced asset, so the edit parks. + +** 4. notes.org template — clear the four lint flags (smoke) + +What: in =claude-templates/.ai/notes.org=, rephrase the two column-0 =**bold**= lines so neither starts with =**=, and comma-escape the =#+begin_example= block's own markers. Then =scripts/sync-check.sh --fix=. + +Why: filed as a [#C] bug task tonight with the reproduction. The edit itself parks because it changes a template every project inherits. Related finding worth Craig's attention: those two flags are mechanical, not judgment, so =lint-org --fix= run anywhere would rewrite the template and create drift against canonical. + +** 5. wrap-it-up.org — delete temp/ at wrap (from Craig's own roam capture) + +What: add a cleanup sub-step to =.ai/workflows/wrap-it-up.org= that removes the project's =temp/= contents during teardown. + +Why: Craig's roam inbox item asks for exactly this ("temp is ... deleted as a part of the wrap up sequence"). The rest of that capture shipped on 2026-07-20 (=working/= tracked from creation, =temp/= gitignored, the sweep backfilling it), but =wrap-it-up.org= has no mention of =temp/= at all, so this clause was missed. Parks as both a shared-asset edit and a destructive one. + +** 02:35 — Craig-ordered amendments applied before launching sentry (earlier) + +Amendment 2 — KB personal roots (=.dotfiles=, 2026-07-21). =~/.dotfiles= classified Unknown under =knowledge-base.md='s personal-roots list, blocking KB writes from that project twice (the sentry pass-list rule tonight, the xdg-desktop-portal gotcha on 2026-07-04). Grepped for other copies of the enumeration: =knowledge-base.md:25= is the only place that lists roots for *classification*. The other three hits (=triggers.md=, =broadcast.org=, =session-harvest.org=, =work-the-backlog.org=) enumerate roots for project *discovery*, a different question, and =~/.dotfiles= reaches those through the machine-local =~/.claude/inbox-roots.txt=. Edited the one line. diff --git a/inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org b/inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org new file mode 100644 index 0000000..340262a --- /dev/null +++ b/inbox/PROCESSED-2026-07-21-0006-from-org-drill-memory-sweep-handoff-2026-06-10-done-in.org @@ -0,0 +1,5 @@ +#+TITLE: Memory-sweep handoff (2026-06-10) done in org-drill. Counts: +#+SOURCE: from org-drill +#+DATE: 2026-07-21 00:06:44 -0500 + +Memory-sweep handoff (2026-06-10) done in org-drill. Counts: 0 promoted to KB / 1 kept local / 0 deleted. The single memory (no-markup-chat-output — never use code fences, bold, or backtick spans in chat) is cross-project, but its durable home is interaction.md rather than a KB node; a separate proposal to fold it into that rule follows in your inbox. Also processed your 2026-06-12 priority-scheme handoff: scheme section added to org-drill's todo.org (derived from existing usage, pending Craig's ratification). One deviation: no commit — org-drill gitignores todo.org per its code-project convention. diff --git a/inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org b/inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org new file mode 100644 index 0000000..ffd2335 --- /dev/null +++ b/inbox/PROCESSED-2026-07-23-0241-from-.dotfiles-ack-from-dotfiles-both-amendments.org @@ -0,0 +1,5 @@ +#+TITLE: Ack from dotfiles: both amendments verified in the rulesets +#+SOURCE: from .dotfiles +#+DATE: 2026-07-23 02:41:58 -0500 + +Ack from dotfiles: both amendments verified in the rulesets canonical. knowledge-base.md line 25 now lists ~/.dotfiles as a personal root, and claude-templates/.ai/workflows/sentry.org carries the amended overview line, the pass-3 mail/messenger exclusion, and pass 11 bug finding. Good call applying the sentry edits by hand instead of rsyncing my copy — I hadn't noticed my rewrite dropped the :TRIAGE_SOURCES: activation probe, and a straight copy would have taken the heartbeat/digest split out with it. I've updated the dotfiles memory to record that the canonical owns this now, so no local re-apply after a sync. Nothing needed back. diff --git a/todo.org b/todo.org index 507f08d..2b299c9 100644 --- a/todo.org +++ b/todo.org @@ -39,6 +39,102 @@ Tags are assigned and refreshed by =task-audit=; =task-review= keeps them honest * Rulesets Open Work +** TODO [#A] Python and TypeScript bundles ship no secret-scan hook :bug: +SCHEDULED: <2026-07-23 Thu> +The =python= and =typescript= language bundles carry rules, a coverage script, and a gitignore fragment. They carry no =githooks/pre-commit=, no =claude/hooks/=, no =claude/settings.json=, and no =CLAUDE.md=. The =bash=, =elisp=, and =go= bundles carry all four. + +The pre-commit hook is the secret scanner. So a project installing the Python or TypeScript bundle gets no credential scan on commit, no validate-on-edit hook, and no settings — while README's "Bundle structure" section documents all four as what each bundle follows, and notes.org describes the bundles as "rules + hooks + settings". + +Live as of 2026-07-23, verified by scanning every project carrying =.claude/rules/=: =work= (python) and =clock-panel= (python + typescript) both have no =githooks/= and no =.claude/settings.json=. The four elisp projects all have both. =work= is the one that matters — a work repo is where a leaked credential is most costly and most likely to reach a company remote. + +Not a design choice. Both bundles were added 2026-05-31; =go='s githooks landed 2026-06-02 and =bash='s 2026-06-23, so the hook rollout swept the two bundles added *after* these and skipped these. =install-lang.sh= guards its copy with =[ -d "$SRC/githooks" ]=, so the install succeeds silently and reports nothing missing — which is why this stayed invisible for nearly two months. + +Grading: Major severity (a documented security control absent, silently, with the install reporting success) x every user, every time (every install of either bundle, standing on 2 of the 6 bundle-using projects) = P1 = [#A]. + +Fix direction: port =githooks/pre-commit= to both bundles, adapting the language-specific half (the secret scan is common; =gofmt=/=shellcheck= becomes a formatter/linter check per language), add =claude/hooks/validate-*.sh= and =claude/settings.json=, and seed =CLAUDE.md=. Then make the gap loud: =install-lang= should warn when a bundle lacks a component the README documents, so the next partial bundle announces itself instead of installing quietly. Re-run =make install-lang= on =work= and =clock-panel= afterward. + +** TODO [#C] Two language bundles' pre-commit hooks lost the cd guard :bug:quick:solo: +=languages/bash/githooks/pre-commit= line 8 reads =cd "$REPO_ROOT" || exit 1=. The =go= and =elisp= copies of the same line read a bare =cd "$REPO_ROOT"=. Three siblings of one file, one hardened and two not — the guard landed in bash and never propagated. + +Low impact, stated honestly: git chdirs to the working-tree root before running a hook, so if =cd= fails the cwd is already correct and the checks still run against the right tree. Neither script sets =-e=, so a failure wouldn't abort them either. It takes a =GIT_WORK_TREE= or bare-repo edge case for =rev-parse --show-toplevel= to disagree with the cwd at all. + +Grading: Minor severity (belt-and-braces guard, no silent-pass path found — the checks run regardless) x rare edge case (needs a git configuration that makes toplevel differ from the hook's cwd) = P4... graded [#C] rather than [#D] because it's a two-character fix in a security-relevant gate and the drift pattern is the real signal: a fix landed in one bundle copy and stopped there, which is the same shape as the [#A] above. + +** VERIFY [#B] Parked: zero markup in chat output, fences included (from org-drill) +What arrived: interaction.md's no-reverse-video rule carves out fenced code blocks as acceptable "when the user explicitly wants a block to copy"; org-drill relays your 2026-05-30 direction that chat output carries no markup at all, fences included. +Recommendation: accept as-is. The carve-out contradicts the direction, and the rule is where it belongs so every project inherits it instead of it living as one project's memory. I kept the rule's factual claim honest — fences genuinely don't invert the way inline spans do, so the new text says they read as markup you didn't ask for rather than inventing a rendering problem. Checked: this is the only copy of the carve-out. +Prepared diff: [[file:working/interaction-no-markup/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked no-markup rule" (or adjust / reject) and it gets applied. + +** VERIFY [#B] Parked: sentry Living Document updates from two dogfood runs (from takuzu + archangel) +What arrived: both projects ran sentry live overnight 2026-07-20/21 and reported back — takuzu 8 fires, archangel 7. No engine defects in either. +Recommendation: accept with changes. Four of the five findings fold in cleanly: the archive-done .gitignore note, the mirror-only "git log understates the night" note, property sweeps as quiet-fire work, and splitting the task audit into a mechanical hourly subset plus a nightly judgment half. The fifth (make the bug hunt an official pass) already landed tonight as pass 11, so it reduces to recording their corroboration. Takuzu's lint-org finding I handled separately as a filed bug task rather than a workflow edit, since it's a defect not a design change. +Prepared diff: [[file:working/sentry-living-document/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked sentry updates" (or adjust / reject) and it gets applied. + +** VERIFY [#B] Parked: clear four lint flags in the notes.org template (from smoke) +What arrived: the synced notes.org template trips misplaced-heading twice on column-0 bold lines and invalid-block twice around the Pending Decisions example, so every project inherits the noise on every sweep. +Recommendation: accept as-is, and I found two more instances smoke didn't list plus the actual cause of the block flags (the example contains a literal =** Feature Name= line that org reads as a heading, breaking block tracking). Verified: the proposed file lints with zero mechanical and zero heading/block findings. Worth your attention separately — those two flags are mechanical, not judgment, so =lint-org --fix= run in any project would rewrite the template and drift it from canonical. +Prepared diff: [[file:working/notes-template-lint/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked template fix" (or adjust / reject) and it gets applied. + +** VERIFY [#B] Parked: clear temp/ during wrap-up teardown (from your roam capture) +What arrived: your roam inbox item asked that every project carry working/ and temp/, and that temp/ be "deleted as a part of the wrap up sequence". The rest shipped 2026-07-20; wrap-it-up.org never got the temp/ clause and has no mention of the directory. +Recommendation: accept as-is. Added as a Step 3 sub-step after the archive pass, with two guards — confirm before deleting anything that reads as in-progress rather than throwaway (that belongs in working/, so move it), and skip in a project where temp/ isn't gitignored. It parks rather than lands because it's both a shared-asset edit and a destructive one. +Prepared diff: [[file:working/wrapup-temp-cleanup/proposed.diff]] — apply is mechanical on your go. +Say "approve the parked temp cleanup" (or adjust / reject) and it gets applied. + +** TODO [#B] inbox-send leaves a phantom empty handoff in the target inbox :bug:solo: +=inbox-send.py= writes straight to the destination path in the target project's =inbox/=. =Path.write_text= opens with mode =w=, which creates and truncates before any content is written, so *any* failure between opening and finishing leaves a zero-byte =.org= file sitting in another project's inbox. + +That file is not inert. =inbox-status= counts it as a pending handoff, so it trips the receiving project's =inbox-boundary-check= Stop hook and blocks a turn there. The receiving agent then has to resolve a handoff with no content and no sender context, which it cannot do from the file. Meanwhile the sender saw an error and will most likely retry, so the target gets a second file too. + +Reproduced 2026-07-23 end to end: a send whose text contains non-ASCII under =LC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0= fails on the ASCII codec, leaves a zero-byte file in the destination inbox, and =inbox-status= in that project then reports it as pending. + +The encoding case is one trigger, not the defect. =write_text= and =read_text= are both called with no =encoding= argument, so they follow the locale; passing =encoding="utf-8"= closes that trigger. The defect underneath is the non-atomic write, which a full disk, a revoked permission, or an interrupted process reaches just as easily. + +Grading: Major severity (a phantom handoff in a *different* project's inbox, unresolvable from its own content, that blocks a turn there via the boundary hook) x some users, sometimes (any mid-write failure, of which the locale case is only the one reproduced) = P2 = [#B]. + +Fix direction: write to a temp file in the destination directory and =os.replace= into place, so the inbox only ever sees a complete file. Pin =encoding="utf-8"= on both =write_text= and the =read_text= in =resolve_roots=. Same treatment for =send_file=, whose =shutil.copy2= has the same shape. Test by forcing a mid-write failure and asserting the destination directory is unchanged. + +** TODO [#C] Two Signal-channel gaps: unbounded send, second account never received :bug: +Both found 2026-07-23 reading =claude-templates/bin/agent-text= and =scripts/signal-receive.sh=. Shellcheck is clean on all four =claude-templates/bin/= scripts; these are logic gaps, not style. + +1. *The direct send has no timeout.* =agent-text='s relay path passes =-o ConnectTimeout=10= to ssh, but the local-account path calls =signal-cli send= with no bound at all. A stalled network or a lock contended by the receive timer hangs the call indefinitely. That matters more than it looks: agent-text is invoked *by agents*, so an unbounded call blocks the calling turn with no output, and the caller can't distinguish a hang from a slow send. The receive path already takes ~16s of wall clock every cadence, so the two contending on the same account is not hypothetical. Fix: wrap the direct send in =timeout=, matching the relay's bound, and let the existing non-zero path print the desktop-fallback message. + +2. *ratio's second Signal account is never received.* =signal-cli listAccounts= on ratio warns "Messages have been last received 8 days ago". Established by elimination, not inference: ratio holds two accounts (=+15045173983= pager, =+15103169357= Craig's personal), =signal-receive.sh= hardcodes the pager as its default and takes no others, and the timer's journal shows it draining the pager cleanly every 15 minutes (last run 2 minutes before the warning appeared). So the 8-day-stale account is the personal one, and nothing on the machine keeps it warm. + + Honest limit on item 2: the staleness is verified, the *harm* is not. That registration is described in the session history as note-to-self with no push, so it may be fine to leave cold, and Signal's own tolerance for a quiet linked account isn't something I established. Filed so the question gets answered rather than rediscovered. The script already accepts an account argument, so if it does matter the fix is a second timer instance rather than a code change — which makes it a dotfiles handoff (that repo owns the unit) with rulesets owning only the script. + +Grading: Minor severity for both (one is a latent hang in a tool with a documented fallback, the other a warning on an account nothing currently depends on) x most users, frequently (the stale warning is a standing condition on this host; the hang needs a stall) = P3 = [#C]. + +** TODO [#C] Two smaller inbox-send defects: uncaught traceback, duplicate roots :bug:quick:solo: +Both verified 2026-07-23, both in =.ai/scripts/inbox-send.py=, both low-harm. Grouped because they're the same file and the same fix session. + +1. *Uncaught =PermissionError=.* =main= catches =(ValueError, FileNotFoundError)= around the send, but =send_file= reaches =shutil.copy2=, which raises =PermissionError= on an unreadable source. That is an =OSError=, not caught, so the user gets a raw Python traceback instead of the clean =inbox-send: = error every other failure path produces. Reproduced with a =chmod 000= source. No partial file is left in this case. Fix: catch =OSError= alongside the other two. + +2. *Duplicate roots list a project twice.* =discover_projects= appends without deduping, so a roots config naming both a parent and one of its children (=/x= and =/x/proj=) lists =proj= at two different numeric indices. Reproduced via =INBOX_SEND_ROOTS=. Harmless today — both indices resolve to the same project, so no message goes to the wrong place, and Craig's current =~/.claude/inbox-roots.txt= has no overlap. Fix: dedupe on =resolve()= before returning. + +Grading: Minor severity (one is cosmetic output noise, the other an ugly but accurate failure message; neither loses or misroutes a message) x rare edge case (an unreadable source file, or a roots config with an overlap) = P4... graded up to [#C] rather than [#D] because both fixes are one line each and sit in a script every project depends on for cross-project messaging. + +** TODO [#C] lint-org todo-format checkers fire on spec files :bug:solo: +=level2-done-without-closed= and =level-2-dated-header= encode =todo.org= completion conventions, but they run against every org file, including =docs/specs/=. A spec's Decisions section legitimately uses =** DONE = with no =CLOSED:= cookie, and its Review-and-iteration-history section legitimately uses =** = headings. Neither is a completion defect there. + +Reproduced 2026-07-23 across rulesets' own specs: 100 findings over 7 of the 9 files in =docs/specs/= (docs-lifecycle 27, sentry-workflow 25, autonomous-batch 11, wrapup-routing 11, inbox-consolidation 10, agent-kb 8, encourage-kb 8; the two 2026-07-20 specs are clean). Every project carrying =docs/specs/= inherits the same noise on every sweep. Reported independently by takuzu's first sentry dogfood run. + +Grading: Minor severity (judgment-kind output, so nothing mutates; the cost is noise that trains the reader to skim) x every user, every time (fires on every sweep in every project with specs) = P2... graded down to [#C] because the two inputs disagree: the frequency row is genuinely universal, but Minor severity with zero mutation risk and a known cause reads as P3. Recorded so the read can be argued. + +Fix direction: scope both checkers away from =docs/specs/=, or gate them on the file carrying a =todo.org=-shaped structure. Note (established 2026-07-23) that these are *org-lint's own* checkers, not ones lint-org.el implements, so the fix filters upstream output by file path rather than editing a checker. Takuzu's alternative was to bless =CLOSED:= dates in =spec-create=, which fixes the symptom in new specs and leaves the nine existing ones noisy. + +** TODO [#C] notes.org template trips four lint-org flags in every project :bug:quick:solo: +The synced =claude-templates/.ai/notes.org= carries two boilerplate lines that open with markdown-style bold at column 0 (=**Session history is NOT in this file.**=, =**For protocols and conventions, see:**=). Org parses a line-initial =**= as a level-2 heading, so =misplaced-heading= flags both, and the =#+begin_example= block in the Pending Decisions instructions trips =invalid-block= twice. Every project inherits all four on every sweep. + +Reproduced 2026-07-23. Confirms smoke's proposal. Additional finding from the same run: these two are =mechanical-fixed=, not judgment, so =lint-org --fix= silently rewrites the template's =**bold**= to org =*bold*=. The rewrite is correct org, but it lands on a synced template, so whichever project runs =--fix= first creates drift against canonical. + +Grading: Minor severity (cosmetic noise; the mechanical rewrite is correct org and harmless in isolation) x every user, every time (every project, every sweep) = P2... same disagreement as the checker task above; graded [#C] on the no-real-harm read. + +Fix direction: rephrase both lines in the canonical template so no line starts with =**= (a list dash, or move the bold off column 0), and comma-escape the example block's own markers. One canonical fix clears it everywhere. + ** TODO Manual testing and validation *** Sentry — entry gates fire with Craig present What we're verifying: the interactive entry gates stop for the right states and start the loop only on a clean, green baseline. @@ -1610,3 +1706,12 @@ Origin: the work project's 2026-06-18 fold-in request; preserved bundle [[file:d ** CANCELLED [#D] Fully-unattended scheduled inbox check (/schedule cron pass) :feature: CLOSED: [2026-07-20 Mon] Merged 2026-07-20 into [[file:todo.org::*Unattended /schedule cron contract][Unattended /schedule cron contract — no-session variant]]. Same no-session design problem as the sentry /schedule variant (mutation rights, async surfacing, cross-run dedup, cron auth context); its inbox-specific context was absorbed there. +** CANCELLED [#B] lint-org resolves file: links against cwd, not the linted file :bug:solo: +CLOSED: [2026-07-23 Thu] +Not a defect. I filed this on a bad comparison and retracted it the same night. + +The claimed evidence was that linting =claude-templates/.ai/notes.org= from the repo root reports =protocols.org= and =workflows/first-session.org= missing, while linting it from its own directory reports neither. The first half of that was never run. The findings came from a =/tmp= copy of the template made while preparing the smoke diff, and in =/tmp= those two siblings genuinely are missing, so org-lint was right. I compared two different files and read the difference as a cwd bug. + +Retested three ways: the real file from the repo root gives zero link findings, the real file from its own directory gives zero, and only the =/tmp= copy gives two. A direct probe confirms =find-file-noselect= already sets =default-directory= to the linted file's directory, so the mechanism I proposed could not have been the cause either. + +Worth keeping from the episode: =link-to-local-file= is org-lint's own checker, not one lint-org.el implements, so a real fix here would mean pre- or post-filtering upstream output rather than editing a local checker. diff --git a/working/interaction-no-markup/interaction.md.proposed b/working/interaction-no-markup/interaction.md.proposed new file mode 100644 index 0000000..caa4d49 --- /dev/null +++ b/working/interaction-no-markup/interaction.md.proposed @@ -0,0 +1,70 @@ +# Interaction Style + +Applies to: `**/*` + +How the agent communicates with the user during a session — choice prompts, status updates, decision points. + +## No Popup Menus for Choices + +When the agent needs the user to pick between options, **do not** use the AskUserQuestion popup. Present the options inline in chat as a numbered list and ask the user to reply with a number. + +**Why:** The popup menu UI sits at the bottom of the chat window and obscures the chat content directly above it — exactly the area the user needs to read to make the choice. Inline numbered options keep the question, the surrounding context, and the proposed text all visible in the same scrollback. + +**How to apply:** + +For approve / changes / cancel flows (commit-message review, PR-description review, plan approval), draft inline: + +``` +1. Approve — commit now +2. Request changes — tell me what to adjust +3. Open in editor — emacsclient -n /tmp/... + +Pick a number. +``` + +For pick-one decisions, same shape: numbered list, one-line prompt at the end. + +For multi-select decisions, say so explicitly: "Pick any combination — reply with the numbers." + +**Render-merge guard.** GFM renderers merge adjacent ordered lists and renumber them — a message with a 1-3 content list followed by a 1-3 options list rendered as options 4-6 on the user's screen, and his pick ("4") didn't exist in the author's numbering (work session, 2026-06-10). Numerals belong to the options list only: any other enumeration in the same message uses dashes or prose, and a short prose lead-in line sits directly above the options so no renderer can merge them. When the user picks a number outside the offered range, suspect the render-merge and ask which item text they meant rather than treating it as an error. + +Reserve `AskUserQuestion` only when the user explicitly asks for the popup form ("use the popup for this one") or for genuinely free-form input where numbered options don't fit. + +This rule applies to all three approval gates in the `commits.md` publish flow (commit message, PR description, PR review reply): print the draft inline, then offer numbered approve / changes / edit options inline. Do not switch to the popup form for the gate even though the prior protocol referenced it. + +### Order options with the recommendation at item 1 + +When the question has a clear recommended option, put it at item 1. The other paths stay visible so the user can override without having to type a custom answer, but the common case collapses to a single keystroke. + +Default to this pattern whenever the user needs to weigh a genuine choice — task-review actions, brainstorm decisions, judgment calls, approve/edit/cancel gates, "what should I do next" prompts. Skip it only when the question is free-form (open prose, names, dates), when the user has already issued a directive and a single confirmation suffices, or when there is no clearly-recommended option (in which case state that plainly: "I don't have a recommendation here — what's your read?"). + +Include a "Skip" / "Defer" / "Tell me how" as the last option when the user's answer might be "none of these." + +The convention reinforces the popup-denial rule above by giving every inline choice list a single canonical shape, and it forces the model to actually pick a recommendation rather than offering a neutrally-ordered enumerate-and-defer list. A neutrally-ordered list is a covert way to push the decision back; recommendation-first puts skin in the game. + +**Enforcement:** a global `PreToolUse` hook (matcher `AskUserQuestion`) in `~/.claude/settings.json` hard-denies the popup and returns this rule as the reason — the prose alone proved too easy to forget. Because the deny is unconditional, the "use the popup for this one" exception above can't be honored in-turn; to get the popup, disable the hook via `/hooks` (or edit settings) first. + +## No Reverse-Video Highlighting in Chat Output + +In conversational output to the user, do not use Markdown bold (`**...**`) or inline-code spans (backtick `` `...` ``). The user's terminal renders both as reverse video, which is hard to read on the display. + +**Why:** The styling that looks like emphasis in most renderers comes through as inverted foreground/background here. Plain prose, plain identifiers, and plain key chords read cleanly; the "highlight" actively hurts legibility. + +**How to apply:** + +- Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s. +- Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine. +- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path. + +This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation. + +## Showing Craig Visuals + +Craig runs Claude Code inside Emacs EAT (through tmux). EAT renders SendUserFile and inline terminal images as an `[image] path.png` text line — the visual itself never appears. In one session ~20 renders went out that way and Craig approved UI he had never seen (takuzu, 2026-07-11). Never rely on SendUserFile or inline image display to show a visual. SendUserFile stays fine for *delivering* a file; it just doesn't display one. + +Two display lanes, by what the visual is for: + +- **Durable or interactive visuals** — HTML prototypes, full renders, anything Craig will study or click: open with `gui-open ` (dotfiles-shipped; detaches through `systemd-run --user` so the agent shell can't reap it, resolves the Hyprland instance after a compositor restart, and confirms the window is actually visible before returning). It auto-detects image vs HTML; `--image` / `--browser` force. Place it off Craig's active workspace and name it, per `desktop-capture.md`. +- **Quick inline glances** — a chart, a did-it-render check: sixel in the terminal. Encode with ImageMagick (`magick sixel:`; img2sixel silently emits zero bytes on some builds) and display in a separate tmux window (`tmux new-window -d -n `, then `tmux send-keys -t "clear; cat " Enter`, tell Craig the window name) rather than the Claude Code pane, whose TUI repaints over anything drawn into it. With native tmux sixel active, the image lives in tmux's grid and survives window switches, scrolling, and resizing. + +**Capability gate.** Native sixel needs two config pieces: EAT answering the XTWINOPS cell-size query (patched eat.el, owned by .emacs.d) and `terminal-features 'xterm*:sixel'` in tmux.conf (owned by dotfiles). Check before relying on it: `tmux display -p '#{client_cell_width}'` — nonzero means go; 0 means the chain is missing a piece, so fall back to the browser lane. (Diagnosed 2026-07-13: tmux won't transmit sixel until it knows the client's cell pixel size, and stock EAT 0.9.4 silently ignores the CSI 14 t query tmux uses to ask.) diff --git a/working/interaction-no-markup/proposal-from-org-drill.org b/working/interaction-no-markup/proposal-from-org-drill.org new file mode 100644 index 0000000..09f0845 --- /dev/null +++ b/working/interaction-no-markup/proposal-from-org-drill.org @@ -0,0 +1,5 @@ +#+TITLE: Proposal: sharpen interaction.md's 'No Reverse-Video Highlig +#+SOURCE: from org-drill +#+DATE: 2026-07-21 00:06:48 -0500 + +Proposal: sharpen interaction.md's 'No Reverse-Video Highlighting in Chat Output' rule. Craig's feedback in org-drill (2026-05-30, saved as a harness memory there): chat output must have zero markup — no code fences, no bold, no backtick spans, ever ('always always list it out without markup'). The current rule bans bold and inline-code spans but says fenced code blocks 'are acceptable when the user explicitly wants a block to copy'. Craig's directive tightens that: default to plain text always; no fences either. Suggest updating the rule text so every project inherits it instead of it living as one project's memory. Evidence: org-drill memory file no-markup-chat-output.md and the 2026-05-30 session log there. diff --git a/working/interaction-no-markup/proposed.diff b/working/interaction-no-markup/proposed.diff new file mode 100644 index 0000000..f5d1830 --- /dev/null +++ b/working/interaction-no-markup/proposed.diff @@ -0,0 +1,11 @@ +--- claude-rules/interaction.md 2026-07-19 15:41:37.382769288 -0500 ++++ /tmp/im.md 2026-07-23 02:47:43.771850521 -0500 +@@ -54,7 +54,7 @@ + + - Write command names, file paths, key chords, and code identifiers as plain text — `pearl-save-issue` becomes pearl-save-issue, `C-; L s s` becomes C-; L s s. + - Use structure that doesn't invert colors: headers, numbered lists, dashes, parentheses, and double quotes for labels are all fine. +-- Fenced code blocks (triple backtick) are acceptable when the user explicitly wants a block to copy — they don't invert the way inline spans do. Default to plain text otherwise. ++- Fenced code blocks (triple backtick) are not an exception. Craig's direction is zero markup in chat output, always: "always always list it out without markup" (2026-05-30). Fences don't invert the way inline spans do, but they still read as markup he didn't ask for, and the carve-out kept reintroducing them. When he needs something to copy, give it as plain indented text, or write it to a file and name the path. + + This governs **chat output**, not the Markdown source of rule files, specs, or docs the user reads in an editor — those keep normal Markdown formatting. The constraint is the terminal rendering of the live conversation. + diff --git a/working/notes-template-lint/notes.org.proposed b/working/notes-template-lint/notes.org.proposed new file mode 100644 index 0000000..311a86f --- /dev/null +++ b/working/notes-template-lint/notes.org.proposed @@ -0,0 +1,86 @@ +#+TITLE: Claude Code Notes - [Project Name] +#+AUTHOR: Craig Jennings +#+DATE: [Date] + +* About This File + +This file contains project-specific information for this project. + +- When to read this: +- At the start of EVERY session (after reading protocols.org) +- When needing project context or history +- When checking reminders or pending decisions + +- What's in this file: +- Project-specific context and goals +- Pending decisions +- Active reminders + +- Session history is NOT in this file. Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5. + +- For protocols and conventions, see [[file:protocols.org][protocols.org]]. + +* Project-Specific Context + +This is where context regarding this project is placed. + +Examples of what goes here: +- Project overview and goals +- People and their relationships to the user +- Contact information for companies and people +- Current state of the project +- Status report +- Links to important documents +- Technical architecture notes +- Key decisions and rationale + +** If this is the first session + +Run the first-session workflow: [[file:workflows/first-session.org][first-session.org]]. It walks through git/.ai policy, project orientation, and initializing this notes.org. Remove this heading once done. + +* PENDING DECISIONS + +This section tracks decisions that need Craig's input before work can proceed. + +**Instructions:** +- Add pending decisions as they arise during sessions +- Format: =** [Topic/Feature Name]= +- Include: What needs to be decided, options available, why it matters +- Remove decisions once resolved (the resolution is captured in the Session Log of the session where it was resolved) + +- Example format: +#+begin_example +,** Feature Name or Topic + +Craig needs to decide on [specific question]. + +Options: +1. Option A - [brief description, pros/cons] +2. Option B - [brief description, pros/cons] + +Why this matters: [impact on project] + +Implementation is ready - just need Craig's preference. +#+end_example + +** Current Pending Decisions + +(None currently - will be added as they arise) + +* Active Reminders + +** Current Reminders + +(None currently - will be added as needed) + +** Instructions for This Section + +When Craig says "remind me" about something: +1. Add it here with timestamp and description +2. If it's a TODO, also add to =/home/cjennings/org/roam/inbox.org= scheduled for today +3. Check this section at start of every session +4. Remove reminders once addressed + +Format: +- =[YYYY-MM-DD]= Description of what to remind Craig about + diff --git a/working/notes-template-lint/proposal-from-smoke.org b/working/notes-template-lint/proposal-from-smoke.org new file mode 100644 index 0000000..26b458a --- /dev/null +++ b/working/notes-template-lint/proposal-from-smoke.org @@ -0,0 +1,5 @@ +#+TITLE: Proposal: clean four lint-org flags in the notes.org templat +#+SOURCE: from smoke +#+DATE: 2026-07-21 06:49:22 -0500 + +Proposal: clean four lint-org flags in the notes.org template (claude-templates/.ai/notes.org). A sentry pass over smoke's synced copy flagged: (1) two 'possibly misplaced heading' judgments on the boilerplate lines that open with org bold at column 0 ('**When to read this:**' and '**What's in this file:**' in the About This File section) — org can parse a line-initial ** as a level-2 heading; rephrasing (a list dash, or moving the bold off line start) would clear it everywhere. (2) two 'possible incomplete block' judgments around the Pending Decisions instructions' #+begin_example block that shows the entry format — likely the example content confusing block parsing; worth a look at whether the example should escape its own markers (e.g. a comma-escaped ,** or ,#+ inside the block). Both are cosmetic — no behavior breaks — but every project inherits the lint noise on each sweep, so one canonical fix beats N local ones. No local edit made in smoke; its copy stays template-faithful. diff --git a/working/notes-template-lint/proposed.diff b/working/notes-template-lint/proposed.diff new file mode 100644 index 0000000..627741f --- /dev/null +++ b/working/notes-template-lint/proposed.diff @@ -0,0 +1,38 @@ +--- claude-templates/.ai/notes.org 2026-07-09 13:57:29.814830969 -0500 ++++ /tmp/nt.org 2026-07-23 02:48:03.165968767 -0500 +@@ -6,19 +6,19 @@ + + This file contains project-specific information for this project. + +-**When to read this:** ++- When to read this: + - At the start of EVERY session (after reading protocols.org) + - When needing project context or history + - When checking reminders or pending decisions + +-**What's in this file:** ++- What's in this file: + - Project-specific context and goals + - Pending decisions + - Active reminders + +-**Session history is NOT in this file.** Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5. ++- Session history is NOT in this file. Each session's record lives in =.ai/sessions/YYYY-MM-DD-HH-MM-description.org= — one file per session. Catch-up reads the Summary sections of the most recent 5. + +-**For protocols and conventions, see:** [[file:protocols.org][protocols.org]] ++- For protocols and conventions, see [[file:protocols.org][protocols.org]]. + + * Project-Specific Context + +@@ -48,9 +48,9 @@ + - Include: What needs to be decided, options available, why it matters + - Remove decisions once resolved (the resolution is captured in the Session Log of the session where it was resolved) + +-**Example format:** ++- Example format: + #+begin_example +-** Feature Name or Topic ++,** Feature Name or Topic + + Craig needs to decide on [specific question]. + diff --git a/working/sentry-living-document/feedback-from-archangel.org b/working/sentry-living-document/feedback-from-archangel.org new file mode 100644 index 0000000..b59ce20 --- /dev/null +++ b/working/sentry-living-document/feedback-from-archangel.org @@ -0,0 +1,5 @@ +#+TITLE: Sentry dogfood feedback (first live run, archangel, night of +#+SOURCE: from archangel +#+DATE: 2026-07-21 07:22:17 -0500 + +Sentry dogfood feedback (first live run, archangel, night of 2026-07-20→21): ran clean end to end — 7 fires (1 working + 6 quiet heartbeats), morning teardown + 8-item approval-queue walk all worked as designed. Two findings for the Living Document: (1) todo-cleanup --archive-done can write a TRACKED file from an 'org-only' pass — on first archive it creates archive/task-archive.org and appends a .gitignore entry, so the pass produced a real commit and correctly triggered the fire-end conditional suite; worth a note in the pass list that todo-cleanup's first archive touches .gitignore. (2) In a project that gitignores .ai/, the whole spine is the mirror-only case — quiet fires produce zero commits, so 'git log main..sentry/*' understates the night's activity; the anchor's heartbeat list is the only record. Both are notes, not defects. diff --git a/working/sentry-living-document/feedback-from-takuzu.org b/working/sentry-living-document/feedback-from-takuzu.org new file mode 100644 index 0000000..fe3ddba --- /dev/null +++ b/working/sentry-living-document/feedback-from-takuzu.org @@ -0,0 +1,5 @@ +#+TITLE: Sentry dogfood feedback from its first live overnight run (t +#+SOURCE: from takuzu +#+DATE: 2026-07-21 06:43:55 -0500 + +Sentry dogfood feedback from its first live overnight run (takuzu, 2026-07-20/21, 8 hourly fires): the engine worked as designed end to end — entry gates, per-fire lock acquire/release, branch isolation, digest-vs-heartbeat split, approval queue, morning squash-merge teardown. Findings for the Living Document: (1) a Craig-granted rotating-angle bug-hunt pass found 3 real fixes in the first 4 fires then went honestly quiet — worth considering as an official opt-in pass; (2) randomized property sweeps (generate-and-verify in both engines) made good quiet-fire work beyond the frozen parity corpus; (3) lint-org's todo-format checkers (level2-done-without-closed, level-2-dated-header) fire on spec files' Decisions sections under docs/specs/ — consider scoping those checkers away from spec files or blessing CLOSED dates in spec-create; (4) the per-fire full task-audit pass is too heavy hourly — a mechanical-subset audit with judgment items queued once per night fit better. No engine defects observed; skip semantics and the quiet-fire heartbeat behaved per spec. diff --git a/working/sentry-living-document/proposed.diff b/working/sentry-living-document/proposed.diff new file mode 100644 index 0000000..90c0db4 --- /dev/null +++ b/working/sentry-living-document/proposed.diff @@ -0,0 +1,32 @@ +--- claude-templates/.ai/workflows/sentry.org 2026-07-23 02:39:42.349838885 -0500 ++++ /tmp/st.org 2026-07-23 02:48:24.104591775 -0500 +@@ -131,9 +131,9 @@ + + 3. *Triage intake — mail and messenger sources excluded.* Run =triage-intake.org=, loading only its non-mail, non-messenger source plugins (calendar, PR/ticketing). The mail and messenger plugins — cmail, any Gmail variant, Telegram, Signal, chat DMs — are never loaded by a sentry fire: Craig ruled 2026-07-21 that sentry doesn't check email or messengers. A manual "triage intake" still scans everything. Probe: the project has at least one *active* triage source that survives that exclusion — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources, or whose only declared sources are mail or messengers, probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended. + +-4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. ++4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=. Note that =--archive-done= is not purely an org-file pass on its first run in a project: it creates =archive/task-archive.org= and appends a =.gitignore= entry, so it produces a real tracked-file commit and correctly trips the fire-end conditional suite. (archangel, first live run 2026-07-21.) + +-5. *Task audit* — =task-audit.org=. Probe: a root =todo.org=. Priority regrades and consolidations queue for review; factual staleness fixes that are unambiguous execute. ++5. *Task audit* — the *mechanical subset* of =task-audit.org= hourly (staleness counts, structural checks, cookie recomputation); the judgment half (priority regrades, consolidations, merge candidates) runs *once per night* and queues its findings rather than repeating them every fire. Probe: a root =todo.org=. A full audit every hour is too heavy and re-surfaces the same judgment calls all night. (takuzu, first live run 2026-07-21.) Factual staleness fixes that are unambiguous still execute. + + 6. *Working-files hygiene* — flag =working//= directories whose backing task is closed (a filing candidate per =working-files.md=). Probe: a =working/= directory exists. The filing itself queues (it's a judgment move). + +@@ -145,7 +145,7 @@ + + 10. *Prep + symlink freshness* — stale daily-prep docs, broken symlinks. Probe: the prep dir / symlinks exist (work and home only, in practice). + +-11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.) ++11. *Bug finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. Randomized property sweeps (generate-and-verify against an engine's own invariants) are good quiet-fire work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few fires find the standing defects; a quiet bug hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four fires, then quiet.) This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.) + + (KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.) + +@@ -178,6 +178,8 @@ + + A bad night is discarded by deleting one branch — nothing reached main, nothing was pushed. + ++In a project that gitignores =.ai/=, the whole spine is untracked, so quiet fires produce no commits at all and =git log main..sentry/-= understates the night's activity. There the anchor's heartbeat list is the only record of what fired. Read the anchor, not just the log. (archangel, first live run 2026-07-21.) ++ + * Stop Sentry + + Trigger: "stop sentry" (and synonyms above). Sentry owns its own shutdown: diff --git a/working/sentry-living-document/sentry.org.proposed b/working/sentry-living-document/sentry.org.proposed new file mode 100644 index 0000000..62eac12 --- /dev/null +++ b/working/sentry-living-document/sentry.org.proposed @@ -0,0 +1,219 @@ +#+TITLE: Sentry — Overnight Hygiene Supervisor +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-19 + +* Overview + +Sentry is an interval loop that keeps a project's hygiene current while Craig is away. Each fire walks a fixed list of hygiene passes — roam pull, inbox zero, triage (no mail or messengers), todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness, bug finding — and commits each pass's writing to a throwaway daily branch. Nothing pushes. In the morning Craig reviews the branch, squash-merges what he wants, and deletes it. + +The design goal is a project that greets the morning already tidy, with every judgment call and every destructive action parked in an approval queue rather than executed unattended. Sentry does the mechanical sweeping; Craig does the deciding. + +This file is the engine. It owns the entry gates, the branch mechanics, the lock model, the per-fire pass runner, the digest and approval queue, the skip semantics, and the stop-sentry shutdown. The =agent-lock= helper (=.ai/scripts/agent-lock=) provides the locks. The passes reuse existing workflows (=inbox.org=, =triage-intake.org=, =clean-todo.org=, =task-audit.org=) under sentry's unattended contract. + +* When to Use This Workflow + +Craig arms sentry at the end of a session, with the machine left running, to have overnight hygiene done by morning. + +Triggers: + +- "start sentry", "run sentry", "arm sentry", "sentry mode" +- "let sentry watch this overnight", "keep this tidy overnight" +- "start sentry hourly", "start sentry every " (sets the loop interval) + +Stop trigger (see Stop Sentry below): + +- "stop sentry", "stand down sentry", "sentry off" + +Sentry is deliberately *not* auto-armed. Running it in a project is a per-project grant (the =:COMMIT_AUTONOMY:= marker) plus a deliberate launch with Craig at the terminal for the entry gates. + +* Prerequisite — the autonomy ticket + +Sentry commits unattended. =commits.md= gates commits on Craig's approval, so sentry needs standing, per-project authorization to run at all. Before anything else, read the project's =.ai/notes.org= Workflow State block for: + +: :COMMIT_AUTONOMY: yes + +If the marker is absent or not =yes=, decline to start and name the marker: + +: Sentry needs ":COMMIT_AUTONOMY: yes" in .ai/notes.org Workflow State to run — it commits unattended. Add it to grant, or run the hygiene passes by hand. + +No half-running mode: a project without the grant doesn't run sentry's read-only passes either. The grant is one line away, so this is a deliberate opt-in, not a barrier. + +* Entry — interactive, with Craig present + +Craig types the sentry trigger, so the first moves run with him at the terminal. Do them in order; each gate that fails stops entry until Craig answers. + +1. *Autonomy ticket* — the prerequisite above. Absent → decline and stop. + +2. *Dirty-tree gate.* =git diff --quiet HEAD= (tracked modifications only; untracked and gitignored files never block — an inbox drop or scratch file is not in-progress work). If the tracked tree is dirty, describe what's dirty and offer, inline-numbered per =interaction.md=: + + 1. Finish the job — commit the in-progress work first (recommended if it's a coherent unit) + 2. Stash it — =git stash= and start sentry on a clean tree + 3. Roll back named changes — discard specific files (names them) + + Wait for an answer. Sentry can't start unattended from a dirty state; that's the point. + +3. *Green-suite gate.* Run the project's full suite (=make test=, or the project's equivalent — detect it). Read the output. If anything is red, describe the failures and offer to investigate before arming. The loop starts only on a green baseline, because every unattended fire measures itself against "did I break this?" and a pre-existing red poisons that check. + +4. *Prior sentry branch.* =git branch --list 'sentry/*'=. An unmerged =sentry/*= branch from a previous night means the morning review didn't happen. Surface it and offer to squash-merge or delete it now (Craig is present); don't stack a second sentry branch on the first. + +5. *Reconcile the project branch.* Fetch and fast-forward-only against upstream — the same reconcile =startup= runs: + + : git fetch --all --prune + : git rev-list --left-right --count @{u}...HEAD + + Zero-behind → continue. Behind-only and clean → =git merge --ff-only @{u}=. Diverged → surface to Craig (he's present); don't auto-resolve. + +6. *Create the daily branch.* From HEAD: + + : git switch -c "sentry/$(date +%F)-$(uname -n)" + + The host suffix (=uname -n=) stops a same-date collision between the two daily drivers. The working tree now sits on this branch overnight — the launch hands the repo to sentry until the morning merge. Reclaiming it mid-night means stopping sentry first (see Stop Sentry). Note the Emacs buffer-revert caveat to Craig if he has the repo open: files change on disk under him overnight, so buffers want reverting after the morning merge (see =emacs.md=). + +7. *Arm the loop.* Start =/loop= at the interval (default hourly; Craig's "every " phrase overrides) with the per-fire body being one sentry fire (the Pass Runner below). Confirm the arming in one line: interval, branch name, project. + +* The lock model + +Two locks, both served by =.ai/scripts/agent-lock= (names only; the helper owns the paths, which live on tmpfs under =$XDG_RUNTIME_DIR/agent-locks/=, host-local and cleared on reboot). + +*Single-runner lock* (=sentry-=, 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 =/loop= fires again while a previous fire still holds it, the new fire's acquire fails and the fire skips with one digest line — no two fires run at once. The bounded wait is short (a few seconds); a live fire means defer, not queue. + +*Roam-write lock* (=roam-write=). A pass that edits a file under =~/org/roam= acquires it, runs =capture-guard --wait= (the human-capture layer stays underneath), edits the working tree, triggers =systemctl --user start roam-sync.service=, and releases. The lock spans only edit-plus-trigger. Sentry never runs =git= against =~/org/roam= — roam-sync stays the repo's only committer (the 2026-06-24 one-git-owner rule). Pass 1's =pull --ff-only= is the sole, read-only exception. + +Every reclaim of a stale lock surfaces in the digest — the helper prints the reclaim note, and the fire records it. A reclaim during a genuinely slow pass is possible, so it's never silent. + +* The Pass Runner — one contract per pass + +Each fire, after acquiring the single-runner lock and verifying branch state (below), walks the pass list in order. Every pass follows the same four-step contract: + +1. *Probe* — a cheap existence check for the pass's target (named per pass below). Absent → the pass is one skip line in the digest and nothing more. This is what makes the pass list portable: passes self-activate where their target exists and stay silent elsewhere, with zero per-project configuration. + +2. *Work* — run the pass under the unattended contract. Quick, solo, already-agreed mechanical actions execute. Anything destructive or requiring judgment does *not* execute — it appends to the morning-approval queue (what, why, the exact command or edit that fires on approval). A pass runs fully or not at all; there is no reduced-form pass. + +3. *Session-context entry* — a pass that does or queues work appends its digest line to the =session-context.org= Session Log (path resolved via =.ai/scripts/session-context-path=) before its commit, so a crash between them still leaves the trail. Per-pass lines for an all-quiet fire (every pass probe-skipped or no-op) are not written one by one — the fire collapses to a single heartbeat at fire-end (below), so an idle fire doesn't spray one skip line per pass. + +4. *Commit* — if the pass wrote to disk, commit it: =chore(sentry): =. 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 loop was armed on a prior day and crossed midnight, the branch keeps the arming date; that's fine, morning teardown handles it. If HEAD is somehow *not* a sentry branch (an interrupted stop, a manual checkout), skip the whole fire with a digest line rather than committing onto main. +- *Clean of foreign changes* — =git diff --quiet HEAD= excluding the spine set (=session-context.org= / =session-context.d/=, resolved via =session-context-path=). Sentry's own spine writes must not trip this; a genuinely unexpected dirty tree (something outside the spine changed and wasn't committed by a prior pass) poisons the fire — skip it with a digest line, the next fire retries. + +* Unattended safety — skip, never degrade + +With no one at the terminal, any unsafe state makes the affected scope skip with one digest line, and the next fire retries. Unsafe states and their scope: + +- *Unexpected dirty tree* (non-spine) → skip the whole fire. +- *Lost or un-acquirable single-runner lock* → skip the fire (another fire holds it, or the helper is missing). +- *A pass's own precondition unmet* (its probe fails, or a dependency is dirty) → skip that pass only. +- *Red suite at fire-end* (see below) → the commits stay on the branch, flagged in the digest for morning review; the fire doesn't roll back. + +Skips are never silent and never partial. Inside a *working* fire, a pass line means the pass fully ran and a skip line names why it didn't. An *all-quiet* fire is not a silent skip either: its single =sentry at HH:MM: nothing= heartbeat is the explicit record that every pass found nothing, standing in for a wall of identical skip lines. The anti-silence rule targets a pass that hides work it should have surfaced; a quiet fire has surfaced that there was none. + +** Multi-day stall notification + +An unmerged prior =sentry/*= branch at fire start (the morning review never happened) skips the fire. After the *second consecutive* fire skipped for this reason, send one persistent desktop notification naming the project and branch: + +: sentry stalled: 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 finding* — hunt for real bugs in the project's codebase: static analysis (=shellcheck= for shell, the project's own linters for its languages), config sanity checks, plus one targeted code-reading area per fire. Rotate the area across fires and name it in the digest, so coverage accumulates over a night instead of re-reading the same corner. Randomized property sweeps (generate-and-verify against an engine's own invariants) are good quiet-fire work here, reaching past a frozen test corpus. Expect the pass to go honestly quiet after the first few fires find the standing defects; a quiet bug hunt is a result, not a failure. (takuzu, first live run 2026-07-21: three real fixes in the first four fires, then quiet.) This pass does *not* run the test suite — the entry baseline already ran it, and re-running it hourly is anti-pattern 5; read the entry result instead. Probe: the project carries a codebase — source under version control beyond its org and tooling files. File each verified finding as a graded bug task in =todo.org= per the severity × frequency matrix (=todo-format.md=), deduped against existing tasks; an unverifiable suspicion is a digest line, not a task. Find, never fix unattended — a proposed fix, or any remediation beyond the task filing, queues for morning approval with its exact edit. (Added at Craig's order 2026-07-21, first dogfooded in dotfiles.) + +(KB lesson promotion — the pass the original proposal listed eleventh — is deferred to vNext. An unattended judgment pass writing to the shared knowledge base waits until sentry has quiet weeks behind it and a designed detection heuristic. See the filed lesson-detection-heuristic task.) + +* Fire-end — conditional suite, then the digest commit + +After the passes: + +1. *Conditional suite run.* If any pass this fire modified files *outside* the org/spine set (a code-touching pass, rare but possible via fixtures), run the full suite once. A green run confirms the fire's commits are safe; a red run flags the digest for morning review — the commits stay on the branch (nothing is pushed, so the morning gate catches it). No per-pass suite runs: the entry run is the green baseline, and hourly per-commit runs would turn a seconds-long fire into minutes all night. Fires that only touched org/spine files skip this. + +2. *Heartbeat or digest, then commit.* Decide quiet vs working. A *quiet* fire — every pass probe-skipped or no-op, nothing added to the approval queue — writes a single heartbeat line to the Session Log, =sentry at HH:MM: nothing= (HH:MM local, from =date=), and no per-pass digest block. A *working* fire — any pass ran, wrote, or queued — writes its full per-pass digest block. Then commit any accumulated spine writes in one sweep: =chore(sentry): digest —