From ccc9c268a187daebfeab9c18418e9a9629f9acc8 Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Sun, 19 Jul 2026 04:56:20 -0500 Subject: feat(sentry): add the sentry overnight-hygiene engine workflow Phase 2 of the sentry supervisor. sentry.org is the engine: an interval loop (default hourly) that walks a fixed pass list (roam pull, inbox zero, triage, todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness) and commits each writing pass to a throwaway sentry/- branch. Nothing pushes. The morning teardown (review, squash-merge, delete) stays Craig's. The file owns the whole engine: the :COMMIT_AUTONOMY: entry ticket, the interactive entry gates (clean tree, green suite, prior-branch check, ff-only reconcile) that run with Craig present, the single-runner and roam-write locks via agent-lock, the probe-work-session-context-commit pass contract, the digest and morning-approval queue, the skip-not-degrade safety model, the conditional fire-end suite, the multi-day stall notification, and the stop-sentry shutdown. Every one of the spec's ten decisions and twelve review findings is reflected: host-suffixed branch so the two daily drivers don't collide, tmpfs lock homes so a lock never rides roam-sync to the other machine, roam-sync kept as the roam repo's only committer, spine-excluded dirty checks so sentry's own bookkeeping can't trip its safety skip, and the queue-don't-execute rule for every judgment or destructive action. Phase 3 wires the roam writers (inbox.org, knowledge-base.md) to acquire the roam-write lock and adds the wrap-it-up guard. Until then the engine is documented but the companions don't yet cooperate. INDEX.org lists it under Tasks and planning. --- .ai/workflows/INDEX.org | 3 + .ai/workflows/sentry.org | 215 ++++++++++++++++++++++++++++++ claude-templates/.ai/workflows/INDEX.org | 3 + claude-templates/.ai/workflows/sentry.org | 215 ++++++++++++++++++++++++++++++ 4 files changed, 436 insertions(+) create mode 100644 .ai/workflows/sentry.org create mode 100644 claude-templates/.ai/workflows/sentry.org diff --git a/.ai/workflows/INDEX.org b/.ai/workflows/INDEX.org index b031dbe..02c4264 100644 --- a/.ai/workflows/INDEX.org +++ b/.ai/workflows/INDEX.org @@ -58,6 +58,9 @@ This index must list every =.org= file in =.ai/workflows/= except this one and e - Speedrun triggers: "speedrun", "no approvals speedrun", "speedrun these: " — any phrase containing "speedrun" routes here (the preset), never to =no-approvals.org= - Manual triggers: "work the backlog", "work the backlog with " (file-only defaults) - Synthesis trigger: "synthesize backlog metrics" — read the per-project metrics logs, compute trends + the corrections signal, write one =:agent:metrics:= KB node (personal projects only) +- =sentry.org= — the overnight hygiene supervisor: an interval loop (default hourly) that walks a fixed pass list (roam pull, inbox zero, triage, todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness), commits each writing pass to a throwaway =sentry/-= branch (never pushed), and parks every judgment call and destructive action in a morning-approval queue. Gated on =:COMMIT_AUTONOMY: yes= plus interactive entry gates (clean tree, green suite) with Craig present. Locks via =agent-lock=; morning teardown (review, squash-merge, delete) is Craig's, never automated. + - Triggers: "start sentry", "run sentry", "arm sentry", "sentry mode", "start sentry every " + - Stop trigger: "stop sentry", "stand down sentry", "sentry off" ** Calendar diff --git a/.ai/workflows/sentry.org b/.ai/workflows/sentry.org new file mode 100644 index 0000000..8d03313 --- /dev/null +++ b/.ai/workflows/sentry.org @@ -0,0 +1,215 @@ +#+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, todo cleanup, task audit, working-files hygiene, spec board, link integrity, git health, prep freshness — 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-=). 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* — append the pass's digest line to the =session-context.org= Session Log (path resolved via =.ai/scripts/session-context-path=). This precedes the commit so a crash between them still leaves the trail. + +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. A pass line in the digest means the pass fully ran; a skip line names why it didn't. + +** 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. + +3. *Triage intake* — run =triage-intake.org=. Probe: triage source plugins present for this project. 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=. + +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. + +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). + +(KB lesson promotion — the proposal's eleventh pass — 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. *Digest commit.* Commit any accumulated spine writes in one sweep — =chore(sentry): digest —