diff options
| -rw-r--r-- | .ai/workflows/inbox.org | 14 | ||||
| -rw-r--r-- | .ai/workflows/sentry.org | 2 | ||||
| -rw-r--r-- | .ai/workflows/triage-intake.org | 2 | ||||
| -rw-r--r-- | .ai/workflows/wrap-it-up.org | 14 | ||||
| -rw-r--r-- | claude-rules/knowledge-base.md | 21 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/inbox.org | 14 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/sentry.org | 2 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/triage-intake.org | 2 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/wrap-it-up.org | 14 | ||||
| -rwxr-xr-x | scripts/roam-sync.sh | 9 |
10 files changed, 89 insertions, 5 deletions
diff --git a/.ai/workflows/inbox.org b/.ai/workflows/inbox.org index 3bd9335..ca684e3 100644 --- a/.ai/workflows/inbox.org +++ b/.ai/workflows/inbox.org @@ -163,6 +163,20 @@ An org capture is usually only a few seconds of mid-finalize state, so =--wait= - *Auto inbox zero (=/loop=) cycle* → don't surface or wait further; defer the roam reconcile to the next cycle, which is itself the retry at loop cadence. The items were already filed in Phase C, so the next cycle's Phase C status-check drops the duplicates and its Phase D removes them. Note one line: "roam reconcile deferred — a capture is still open; next cycle catches it." - *Wrap-up sub-step* → don't block the wrap. Skip the roam reconcile for this run and surface one line: "Skipped roam-inbox reconcile — a live org-capture is open against it; claimed items stay and get caught next run." The items were already filed into =todo.org= in roam mode Phase C, so the next roam run's Phase C status-check drops the duplicates and its Phase D removes them — the skip self-heals. +*The roam-write lock (around the Phase D edit).* Capture-guard protects against a live *human* capture; the roam-write lock protects against a concurrent *agent* writer (a sentry inbox pass, a KB promotion) editing =~/org/roam= at the same time. Acquire it after the capture-guard clears and release it after the edit-and-trigger, so the two guards nest — capture-guard underneath, the agent lock around the write: + +#+begin_src bash +if [ -x .ai/scripts/agent-lock ]; then + .ai/scripts/agent-lock acquire roam-write --wait || { echo "roam-write busy; deferring roam reconcile" >&2; exit 1; } +fi +# capture-guard (above), then the Phase D read-modify-write of ~/org/roam/inbox.org, +# then trigger the sync — roam-sync stays the only committer: +systemctl --user start roam-sync.service +[ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock release roam-write +#+end_src + +Degrade gracefully when =agent-lock= isn't installed (an older checkout mid-sync): the write proceeds unlocked, today's behavior. A *present* helper reporting the lock busy after its bounded wait defers the roam reconcile (the auto-loop and wrap-up paths already defer-and-retry per the fallback list above); an *absent* helper never blocks it. + * Core §6 — Priority-scheme check This gates filing whenever there are accept-and-file items. Check whether =todo.org= has a top-of-file priority scheme (an explicit legend defining =[#A]= through =[#D]= semantics and mandatory/optional tag conventions — a =* <Project> Priority Scheme= section or similar). diff --git a/.ai/workflows/sentry.org b/.ai/workflows/sentry.org index 8d03313..eb276df 100644 --- a/.ai/workflows/sentry.org +++ b/.ai/workflows/sentry.org @@ -75,7 +75,7 @@ Craig types the sentry trigger, so the first moves run with him at the terminal. 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-<project>=). 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. +*Single-runner lock* (=sentry-<project>=, where =<project>= 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. diff --git a/.ai/workflows/triage-intake.org b/.ai/workflows/triage-intake.org index 9e08142..d476ac3 100644 --- a/.ai/workflows/triage-intake.org +++ b/.ai/workflows/triage-intake.org @@ -37,6 +37,8 @@ Typical timing: Do *not* use when running daily-prep — daily-prep already does this as Phase 3. +Also runs unattended as sentry's triage pass (=sentry.org=, pass 3): sentry invokes this engine under its no-approvals contract, where destructive actions (deleting, archiving, sending) queue for the morning-approval review instead of firing. The trigger phrases above are unchanged — a manual "triage intake" always routes here directly. + * Execution diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org index 5ce88a5..a032bc3 100644 --- a/.ai/workflows/wrap-it-up.org +++ b/.ai/workflows/wrap-it-up.org @@ -43,6 +43,20 @@ This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-qu * The Workflow +** Step 0: Refuse if sentry is live + +Before anything else, check whether sentry is running in this project. Sentry holds the working tree on its =sentry/<date>-<host>= branch and commits unattended; wrapping underneath it would archive the session anchor and tear down the buffer while the loop is still firing into it. If sentry's single-runner lock is held, stop and point at the shutdown path: + +#+begin_src bash +proj="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")" +if [ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock status "sentry-$proj" | grep -q '^held'; then + echo "sentry is active — say 'stop sentry' first" + exit 1 +fi +#+end_src + +The stop-sentry operation (defined in =sentry.org=) owns the shutdown: it cancels the loop, disposes of the branch, and walks the approval queue. Wrap-up carries only this one guard; a =stale= lock (a crashed fire) doesn't block — only a live =held= lock does. + ** Step 1: Finalize the Summary *** Early KB reflection (capture while fresh, before the Summary) diff --git a/claude-rules/knowledge-base.md b/claude-rules/knowledge-base.md index d61ef03..478d5b8 100644 --- a/claude-rules/knowledge-base.md +++ b/claude-rules/knowledge-base.md @@ -43,7 +43,26 @@ A write is one node per fact, under `agents/`, roam-valid so Craig's org-roam in <the fact, with [[id:...]] links to related nodes> ``` -Pull before writing, commit and push after (`git -C ~/org/roam add -A && git commit && git push`) — same session discipline as any repo. Never edit Craig's hand-authored nodes; link to them. This write autonomy is scoped to the KB alone — it is not permission to send email, comment on tickets, or post to any public or external channel. +Pull before writing (`git -C ~/org/roam pull --ff-only`, read-only). Then acquire the roam-write lock, write the node, and trigger roam-sync to commit and push — roam-sync stays the roam repo's only committer (the 2026-06-24 one-git-owner rule). The tree is chronically dirty from live captures, so an agent's own `git add -A && commit` could sweep an in-flight capture into a stray commit; edit-plus-trigger avoids that. Never edit Craig's hand-authored nodes; link to them. This write autonomy is scoped to the KB alone — it is not permission to send email, comment on tickets, or post to any public or external channel. + +The write block, with the lock and the trigger: + +```sh +# Acquire the roam-write lock so a concurrent sentry pass or inbox writer can't +# race this write. Callers pass a name; agent-lock owns the path (tmpfs). +if [ -x .ai/scripts/agent-lock ]; then + if ! .ai/scripts/agent-lock acquire roam-write --wait; then + # Busy after the bounded wait — surface and stop, don't write unlocked. + echo "roam-write lock held by another writer; try again shortly" >&2 + exit 1 + fi +fi +# ... write ~/org/roam/agents/<ts>-<slug>.org ... +systemctl --user start roam-sync.service # roam-sync commits + pushes +[ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock release roam-write +``` + +Degrade gracefully when `agent-lock` isn't installed (an older checkout mid-sync): the guard above is skipped and the write proceeds unlocked — today's behavior. Only a *present* helper reporting the lock busy after its bounded wait stops the write; an *absent* helper never blocks it. ## What goes in, what stays out diff --git a/claude-templates/.ai/workflows/inbox.org b/claude-templates/.ai/workflows/inbox.org index 3bd9335..ca684e3 100644 --- a/claude-templates/.ai/workflows/inbox.org +++ b/claude-templates/.ai/workflows/inbox.org @@ -163,6 +163,20 @@ An org capture is usually only a few seconds of mid-finalize state, so =--wait= - *Auto inbox zero (=/loop=) cycle* → don't surface or wait further; defer the roam reconcile to the next cycle, which is itself the retry at loop cadence. The items were already filed in Phase C, so the next cycle's Phase C status-check drops the duplicates and its Phase D removes them. Note one line: "roam reconcile deferred — a capture is still open; next cycle catches it." - *Wrap-up sub-step* → don't block the wrap. Skip the roam reconcile for this run and surface one line: "Skipped roam-inbox reconcile — a live org-capture is open against it; claimed items stay and get caught next run." The items were already filed into =todo.org= in roam mode Phase C, so the next roam run's Phase C status-check drops the duplicates and its Phase D removes them — the skip self-heals. +*The roam-write lock (around the Phase D edit).* Capture-guard protects against a live *human* capture; the roam-write lock protects against a concurrent *agent* writer (a sentry inbox pass, a KB promotion) editing =~/org/roam= at the same time. Acquire it after the capture-guard clears and release it after the edit-and-trigger, so the two guards nest — capture-guard underneath, the agent lock around the write: + +#+begin_src bash +if [ -x .ai/scripts/agent-lock ]; then + .ai/scripts/agent-lock acquire roam-write --wait || { echo "roam-write busy; deferring roam reconcile" >&2; exit 1; } +fi +# capture-guard (above), then the Phase D read-modify-write of ~/org/roam/inbox.org, +# then trigger the sync — roam-sync stays the only committer: +systemctl --user start roam-sync.service +[ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock release roam-write +#+end_src + +Degrade gracefully when =agent-lock= isn't installed (an older checkout mid-sync): the write proceeds unlocked, today's behavior. A *present* helper reporting the lock busy after its bounded wait defers the roam reconcile (the auto-loop and wrap-up paths already defer-and-retry per the fallback list above); an *absent* helper never blocks it. + * Core §6 — Priority-scheme check This gates filing whenever there are accept-and-file items. Check whether =todo.org= has a top-of-file priority scheme (an explicit legend defining =[#A]= through =[#D]= semantics and mandatory/optional tag conventions — a =* <Project> Priority Scheme= section or similar). diff --git a/claude-templates/.ai/workflows/sentry.org b/claude-templates/.ai/workflows/sentry.org index 8d03313..eb276df 100644 --- a/claude-templates/.ai/workflows/sentry.org +++ b/claude-templates/.ai/workflows/sentry.org @@ -75,7 +75,7 @@ Craig types the sentry trigger, so the first moves run with him at the terminal. 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-<project>=). 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. +*Single-runner lock* (=sentry-<project>=, where =<project>= 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. diff --git a/claude-templates/.ai/workflows/triage-intake.org b/claude-templates/.ai/workflows/triage-intake.org index 9e08142..d476ac3 100644 --- a/claude-templates/.ai/workflows/triage-intake.org +++ b/claude-templates/.ai/workflows/triage-intake.org @@ -37,6 +37,8 @@ Typical timing: Do *not* use when running daily-prep — daily-prep already does this as Phase 3. +Also runs unattended as sentry's triage pass (=sentry.org=, pass 3): sentry invokes this engine under its no-approvals contract, where destructive actions (deleting, archiving, sending) queue for the morning-approval review instead of firing. The trigger phrases above are unchanged — a manual "triage intake" always routes here directly. + * Execution diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org index 5ce88a5..a032bc3 100644 --- a/claude-templates/.ai/workflows/wrap-it-up.org +++ b/claude-templates/.ai/workflows/wrap-it-up.org @@ -43,6 +43,20 @@ This depends on three functions in =.emacs.d/modules/ai-term.el= (=cj/ai-term-qu * The Workflow +** Step 0: Refuse if sentry is live + +Before anything else, check whether sentry is running in this project. Sentry holds the working tree on its =sentry/<date>-<host>= branch and commits unattended; wrapping underneath it would archive the session anchor and tear down the buffer while the loop is still firing into it. If sentry's single-runner lock is held, stop and point at the shutdown path: + +#+begin_src bash +proj="$(basename "$(git rev-parse --show-toplevel 2>/dev/null || pwd)")" +if [ -x .ai/scripts/agent-lock ] && .ai/scripts/agent-lock status "sentry-$proj" | grep -q '^held'; then + echo "sentry is active — say 'stop sentry' first" + exit 1 +fi +#+end_src + +The stop-sentry operation (defined in =sentry.org=) owns the shutdown: it cancels the loop, disposes of the branch, and walks the approval queue. Wrap-up carries only this one guard; a =stale= lock (a crashed fire) doesn't block — only a live =held= lock does. + ** Step 1: Finalize the Summary *** Early KB reflection (capture while fresh, before the Summary) diff --git a/scripts/roam-sync.sh b/scripts/roam-sync.sh index 55422ec..ef43c8f 100755 --- a/scripts/roam-sync.sh +++ b/scripts/roam-sync.sh @@ -3,8 +3,13 @@ # # Commit any local changes, rebase onto the remote, push. Run by the # roam-sync systemd user timer (scripts/systemd/) every 15 minutes so -# Craig's hand edits travel without a manual git step. Agents don't need -# this — they pull/commit/push inline per claude-rules/knowledge-base.md. +# Craig's hand edits travel without a manual git step. This script is the +# roam repo's only committer (the 2026-06-24 one-git-owner rule): the tree +# is chronically dirty from live captures, so a second committer risks +# sweeping an in-flight capture into a stray commit. Agents edit the working +# tree under the roam-write lock, then trigger this unit (systemctl --user +# start roam-sync.service) instead of committing themselves — see +# claude-rules/knowledge-base.md and the inbox workflow's roam mode. # # On a rebase conflict: abort the rebase (never leave the repo mid-rebase # for a timer to mangle), keep the local commit, exit 1 so the failure is |
