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. --- .../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 +++++++++++++++++++++ 12 files changed, 1131 insertions(+) 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 (limited to 'working') 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 —