diff options
Diffstat (limited to 'docs')
27 files changed, 1623 insertions, 20 deletions
diff --git a/docs/design/2026-05-28-pattern-catalog-prompt-labels-and-defaults.org b/docs/design/2026-05-28-pattern-catalog-prompt-labels-and-defaults.org index cc2cb77..9ee29d6 100644 --- a/docs/design/2026-05-28-pattern-catalog-prompt-labels-and-defaults.org +++ b/docs/design/2026-05-28-pattern-catalog-prompt-labels-and-defaults.org @@ -28,7 +28,7 @@ A generic =pearl--with-sentinel SENTINEL CANDIDATES= helper lets each call site ** Why for the catalog -This is the "no hidden affordances" pattern from the earlier note ([[file:2026-05-28-0003-from-pearl-rulesets-followup-no-empty-input.org]]) sharpened with a second rule: *if the affordance is visible, its label has to match what picking it does*. Visibility without accuracy is its own problem. A label that says "none" when the behavior is "any" is no better than an invisible empty-input idiom — both leave the user holding the wrong model. +This is the "no hidden affordances" pattern from the earlier note (a processed pearl-rulesets inbox item, since removed) sharpened with a second rule: *if the affordance is visible, its label has to match what picking it does*. Visibility without accuracy is its own problem. A label that says "none" when the behavior is "any" is no better than an invisible empty-input idiom — both leave the user holding the wrong model. Catalog shape suggestion: this is the same principle as Pattern 3, in a follow-up form. Either a single entry that captures both halves (visible + accurate) or two cross-linked entries. diff --git a/docs/design/2026-06-02-flush-promotion.org b/docs/design/2026-06-02-flush-promotion.org index 9d9d8a3..ff4e579 100644 --- a/docs/design/2026-06-02-flush-promotion.org +++ b/docs/design/2026-06-02-flush-promotion.org @@ -1,5 +1,5 @@ #+TITLE: Flush Promotion — Handoff Bundle (from work) -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-02 * Provenance diff --git a/docs/design/2026-06-02-pattern-catalog-spec.org b/docs/design/2026-06-02-pattern-catalog-spec.org index e74b8ae..6bb105a 100644 --- a/docs/design/2026-06-02-pattern-catalog-spec.org +++ b/docs/design/2026-06-02-pattern-catalog-spec.org @@ -1,5 +1,5 @@ #+TITLE: Cross-Project Pattern Catalog — Spec -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-02 One-page spec for capturing reusable design patterns so they travel from one project to the next instead of being re-derived. Drafted for Craig's spec-review; the five open questions from the [[file:../../todo.org][todo.org]] task carry a recommended call each, marked DECISION. diff --git a/docs/design/2026-06-16-inbox-zero-phase-e-proposal.diff b/docs/design/2026-06-16-inbox-zero-phase-e-proposal.diff new file mode 100644 index 0000000..e3d8ee8 --- /dev/null +++ b/docs/design/2026-06-16-inbox-zero-phase-e-proposal.diff @@ -0,0 +1,65 @@ +--- claude-templates/.ai/workflows/inbox-zero.org 2026-06-13 13:18:35.988799778 -0500 ++++ /proc/self/fd/12 2026-06-16 00:18:07.592944696 -0500 +@@ -1,6 +1,3 @@ +-#+TITLE: Inbox Zero Workflow +-#+AUTHOR: Craig Jennings & Claude +-#+DATE: 2026-06-13 + + * Overview + +@@ -19,7 +16,7 @@ + Reused from three callers so the steps live in one place: + - *Startup* (read-only nudge) — count the items, identify which appear related to this project, surface both numbers, offer processing as one of the startup options. Never auto-files. + - *Wrap-up* (Step 3 sub-step) — sweep items that belong here before the cleanup scripts, so imported tasks lint and ride the wrap commit. +-- *On demand* — "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox". ++- *On demand* — "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox". The on-demand caller (and the recurring loop that invokes it) also runs *Phase E* below: after routing the inbox, it works the project's actionable backlog, implementing eligible =:next:= / =:quick:+:solo:= tasks. Startup and wrap-up skip Phase E. + + Each project touches the roam inbox at least twice a session this way: once at startup, once at wrap-up. + +@@ -69,6 +66,46 @@ + + Report: moved (with their new priorities and tags), folded, dropped-as-done. Then the residue: foreign items (left for their owners, count only) and unowned items (count plus the headings that appear related to this project, for manual claim or prefix). Same "summarize what we kept" shape. + ++* Phase E — Execute actionable tagged tasks (autonomous; on-demand / loop caller only) ++ ++After routing the inbox, the on-demand and loop callers work the project's actionable backlog. *Startup (read-only) and wrap-up (winding down) skip this phase entirely* — it runs only when a human or the recurring loop invokes the workflow to make progress, never as a side effect of session bookkeeping. ++ ++** Eligibility gate ++ ++Scan =todo.org= (both items freshly filed in Phase B and the existing backlog). A task is a candidate when ALL hold: ++ ++1. Status is =TODO= — not =VERIFY=, =DOING=, =DONE=, or =CANCELLED=. =VERIFY= means "awaiting Craig's manual confirmation"; never auto-implement a VERIFY. Those are the manual-testing verifications this phase deliberately leaves alone. ++2. Tagged =:next:=, OR tagged BOTH =:quick:= AND =:solo:=. ++3. Implementable solo — no input or undecided judgment call from Craig. ++4. Estimated at roughly 30 minutes or less of focused work. ++ ++** Act-vs-file decision ++ ++For each candidate, after a quick scope read of the relevant code: ++ ++- *Clear, bounded, solo, ≤ ~30 min* → implement it now (below). ++- *Needs Craig's input, a decision, or design discussion* → do NOT implement. Leave it filed, add a one-line note on the task naming the input it needs, and surface it. ++- *An hour or more* → do NOT implement. Leave it filed and surface it as a larger task for a dedicated =/start-work= session. ++ ++When unsure which side a task falls on, file rather than implement. A wrong auto-implement costs more than a deferred task. ++ ++** Implementing a candidate ++ ++Per task, follow the project's commit discipline — the per-project waiver: no approval gate, but TDD + =/review-code= + =/voice personal= on every commit, no AI attribution: ++ ++1. Trace to root cause; write the failing test first (Red → Green → Refactor). ++2. Live-reload into the running daemon and verify per the emacs reload-and-verify loop. ++3. Close the task per =todo-format.md= (top-level → =DONE= + =CLOSED:=; sub-task → dated log rewrite). When the only residue is Craig's manual check, file a =VERIFY= child under "Manual testing and validation" and close the originating task (the codified manual-verification-handoff pattern). ++4. =/review-code --staged= → fix all Critical/Important → =/voice personal= on the message → commit individually. Push per the project's flow. ++ ++** Bounding the run ++ ++Default to one task per run: implement the highest-priority eligible candidate (=[#A]= before =[#B]= before =[#C]=), commit, then stop and let the next tick or the next on-demand invocation continue. A caller may work more than one in a run when the eligible tasks are small and clearly independent — but each gets its own test and its own commit, and the run stays reviewable. Never batch unrelated changes into one commit. ++ ++** Surface ++ ++Report what was implemented (task + commit), what was deferred and why (needs-input / too-large), and what stays filed. ++ + * Skip conditions + + - No =~/org/roam/inbox.org= → silent no-op. diff --git a/docs/design/2026-06-16-inbox-zero-phase-e-proposal.org b/docs/design/2026-06-16-inbox-zero-phase-e-proposal.org new file mode 100644 index 0000000..5fa7e12 --- /dev/null +++ b/docs/design/2026-06-16-inbox-zero-phase-e-proposal.org @@ -0,0 +1,137 @@ +#+TITLE: Inbox Zero Workflow +#+AUTHOR: Craig Jennings +#+DATE: 2026-06-13 + +* Overview + +The roam global inbox (=~/org/roam/inbox.org=) is Craig's cross-project GTD capture: one shared file every project can see. This workflow routes each inbox item to the project that owns it. The current session claims only the items belonging to THIS project, files them into the project's =todo.org=, and removes them from the shared inbox. Everything it doesn't own stays. The aspiration is inbox zero: over time, every item lands in its owning project. + +This is NOT =process-inbox.org=. That workflow handles the project's own =inbox/= directory (handoffs from other projects, scripts, Craig). This one handles the single global =roam/inbox.org= and the cross-project routing a shared file creates. + +This is also distinct from the wrap-up inbox/transcript routing feature (which moves session-filed keepers between projects). This routes the shared roam capture file by ownership prefix. + +** Scope: single-destination (v1) + +This version routes each item to its one owning project, identified by an explicit =<project>:= heading prefix. The multi-project domain-aware mode, which would guess the owner of every unprefixed item and empty the whole inbox in one run, is deferred (see "Deferred: domain-aware routing" at the end). v1 claims only what's prefixed for the current project, surfaces the rest, and never guesses. + +** Three callers + +Reused from three callers so the steps live in one place: +- *Startup* (read-only nudge) — count the items, identify which appear related to this project, surface both numbers, offer processing as one of the startup options. Never auto-files. +- *Wrap-up* (Step 3 sub-step) — sweep items that belong here before the cleanup scripts, so imported tasks lint and ride the wrap commit. +- *On demand* — "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox". The on-demand caller (and the recurring loop that invokes it) also runs *Phase E* below: after routing the inbox, it works the project's actionable backlog, implementing eligible =:next:= / =:quick:+:solo:= tasks. Startup and wrap-up skip Phase E. + +Each project touches the roam inbox at least twice a session this way: once at startup, once at wrap-up. + +* The ownership rule (the coordination primitive) + +The inbox is shared, so the workflow must never let two projects fight over an item or let one project grab another's. Ownership is by explicit prefix: + +- =<project>: ...= heading → owned by that project. The current project claims only items prefixed with its own identifier. +- Prefixed for *another* project → leave untouched (cross-project boundary, =protocols.org=). +- *No prefix* → unowned. Never auto-claim. Surface as candidates a human can claim or prefix. + +The prefix partition is what makes concurrent triage across projects safe: each project only ever removes its own items, so two sessions editing the inbox touch disjoint lines. + +** Resolving this project's identifier (v1) + +Use the project root basename plus its common aliases (=.emacs.d= ↔ =emacs=, and the obvious ones: =rulesets=, =work=, =home=). A project may override the inferred set with an =:INBOX_PREFIX:= line in =notes.org='s *Workflow State* section when the basename is fragile (a dot in the name, an alias the inference misses). The explicit override is optional in v1; the durable multi-project resolution is part of the deferred domain-aware mode. + +* Phase A — Identify, count, and match + +1. Resolve the current project's identifier and aliases (above). +2. Read =~/org/roam/inbox.org=. If absent, silent no-op (the file lives only on machines with the roam clone). +3. Bucket every item under the inbox heading: + - *claimed* — prefixed for this project + - *foreign* — prefixed for another project → leave + - *unowned* — no project prefix +4. *Summarize the scan* (Craig's requirement, every scan): report the total item count in the inbox, then the count that appears related to this project. "Appears related" is the union of claimed items (exact prefix) and any unowned item whose topic plainly concerns this project's domain (a content judgment, surfaced as a candidate, never auto-claimed). Foreign-prefixed items are not "related" — they belong to their owner. +5. If both claimed and related-unowned are empty, report the total and stop (the common case for most wraps). + +* Phase B — File each claimed item into todo.org + +Apply =process-inbox.org='s discipline against the project's =todo.org=; don't reinvent it: + +1. *Status check first.* Already done, or already a task in =todo.org=? → drop it, or fold into the existing task (dated sub-entry per =todo-format.md=). Don't duplicate. +2. *Rewrite* to terse-heading + body per =todo-format.md=. +3. *Priority + tags from THIS project's scheme* — the legend at the top of its =todo.org=, tags from that scheme's allowed set only. The project expresses someday-maybe with =[#D]=; there's no special someday-maybe routing. +4. *File* under the project's Open Work section. + +* Phase C — Reconcile the shared inbox + +The roam inbox lives in a git repo (=~/org/roam=, auto-synced by the =roam-sync= timer). Edit it carefully: + +1. *Pull first* (=git -C ~/org/roam pull --ff-only=). If it can't fast-forward (dirty tree, divergence), surface and stop. Don't auto-stash, auto-merge, or force. Resolve before removing items. +2. *Remove only the claimed items.* Never touch foreign or unowned items. +3. *Commit the roam repo as its own commit* (separate from any project wrap commit): =chore(inbox): route <project> tasks to <project>/todo.org=. Push, or leave for the =roam-sync= timer. Surface a blocked push; don't force. + +* Phase D — Surface + +Report: moved (with their new priorities and tags), folded, dropped-as-done. Then the residue: foreign items (left for their owners, count only) and unowned items (count plus the headings that appear related to this project, for manual claim or prefix). Same "summarize what we kept" shape. + +* Phase E — Execute actionable tagged tasks (autonomous; on-demand / loop caller only) + +After routing the inbox, the on-demand and loop callers work the project's actionable backlog. *Startup (read-only) and wrap-up (winding down) skip this phase entirely* — it runs only when a human or the recurring loop invokes the workflow to make progress, never as a side effect of session bookkeeping. + +** Eligibility gate + +Scan =todo.org= (both items freshly filed in Phase B and the existing backlog). A task is a candidate when ALL hold: + +1. Status is =TODO= — not =VERIFY=, =DOING=, =DONE=, or =CANCELLED=. =VERIFY= means "awaiting Craig's manual confirmation"; never auto-implement a VERIFY. Those are the manual-testing verifications this phase deliberately leaves alone. +2. Tagged =:next:=, OR tagged BOTH =:quick:= AND =:solo:=. +3. Implementable solo — no input or undecided judgment call from Craig. +4. Estimated at roughly 30 minutes or less of focused work. + +** Act-vs-file decision + +For each candidate, after a quick scope read of the relevant code: + +- *Clear, bounded, solo, ≤ ~30 min* → implement it now (below). +- *Needs Craig's input, a decision, or design discussion* → do NOT implement. Leave it filed, add a one-line note on the task naming the input it needs, and surface it. +- *An hour or more* → do NOT implement. Leave it filed and surface it as a larger task for a dedicated =/start-work= session. + +When unsure which side a task falls on, file rather than implement. A wrong auto-implement costs more than a deferred task. + +** Implementing a candidate + +Per task, follow the project's commit discipline — the per-project waiver: no approval gate, but TDD + =/review-code= + =/voice personal= on every commit, no AI attribution: + +1. Trace to root cause; write the failing test first (Red → Green → Refactor). +2. Live-reload into the running daemon and verify per the emacs reload-and-verify loop. +3. Close the task per =todo-format.md= (top-level → =DONE= + =CLOSED:=; sub-task → dated log rewrite). When the only residue is Craig's manual check, file a =VERIFY= child under "Manual testing and validation" and close the originating task (the codified manual-verification-handoff pattern). +4. =/review-code --staged= → fix all Critical/Important → =/voice personal= on the message → commit individually. Push per the project's flow. + +** Bounding the run + +Default to one task per run: implement the highest-priority eligible candidate (=[#A]= before =[#B]= before =[#C]=), commit, then stop and let the next tick or the next on-demand invocation continue. A caller may work more than one in a run when the eligible tasks are small and clearly independent — but each gets its own test and its own commit, and the run stays reviewable. Never batch unrelated changes into one commit. + +** Surface + +Report what was implemented (task + commit), what was deferred and why (needs-input / too-large), and what stays filed. + +* Skip conditions + +- No =~/org/roam/inbox.org= → silent no-op. +- No claimed and no related-unowned items → report the total, stop. +- Roam pull blocked → surface, stop before editing. + +* Caller integration + +** Startup (read-only nudge) + +Phase A of =startup.org= reads =~/org/roam/inbox.org= and produces the scan summary; Phase C surfaces one line: "Roam inbox: N items total, M appear related to this project — say 'inbox zero' to file them." Offered as one of the priority options. Startup never auto-files; it counts and offers. + +** Wrap-up (Step 3 sub-step) + +A sub-step at the start of wrap-up Step 3 (before the cleanup scripts, so imported tasks get linted and ride the wrap commit) delegates here for the claimed set. Skip-fast when nothing matches. + +* Deferred: domain-aware routing (future work, multi-project) + +v1 handles the single-destination case via the prefix rule. The multi-project parts are deferred until the need is real: + +1. *Domain-aware empty-it-all mode.* If rulesets held a description of each project's domain, one run could guess the owner of every item (prefixed or not) and empty the whole inbox at once, delivering each item to its owning project's =inbox/= via =inbox-send= (where that project's =process-inbox= gate still decides whether to file it). This turns "inbox zero" from a per-project aspiration into a single command. Open: where the domain map lives (central registry vs each project's =notes.org=), how confident a guess must be before auto-routing vs surfacing, and whether a low-confidence item stays put. +2. *Explicit per-project =:INBOX_PREFIX:= as the durable resolver*, replacing basename inference. +3. *Unowned-item lifecycle* once domain-aware routing exists (no item stays unrouted indefinitely). +4. *Concurrent push contention* on the shared roam repo: the pull-before-edit + ff-only + surface-on-conflict floor may want a retry-once-after-pull. + +Take these up when the single-destination version is in use and the multi-project pain is concrete. diff --git a/docs/design/2026-06-16-inbox-zero-phase-e-sender-note.org b/docs/design/2026-06-16-inbox-zero-phase-e-sender-note.org new file mode 100644 index 0000000..08e7650 --- /dev/null +++ b/docs/design/2026-06-16-inbox-zero-phase-e-sender-note.org @@ -0,0 +1,27 @@ +#+TITLE: inbox-zero Phase E — autonomous execution of :next: / :quick:+:solo: tasks + +* What changed + +Added a new "Phase E — Execute actionable tagged tasks" to inbox-zero.org (edited locally in .emacs.d as a stopgap; the edited file is attached alongside this note). After routing the roam inbox, the on-demand and loop callers now scan todo.org and autonomously implement eligible tasks. + +Eligibility gate (all must hold): status TODO (not VERIFY/DOING/DONE/CANCELLED); tagged :next: OR both :quick: and :solo:; solo-doable without Craig's input; ~30 min or less. VERIFY is explicitly excluded — in this project VERIFY means "awaiting Craig's manual confirmation," i.e. the manual-testing verifications that must NOT be auto-implemented. Anything needing input or an hour-plus is filed and surfaced, never implemented. Each implementation follows the project commit discipline (TDD + /review-code + /voice personal + individual commit, no AI attribution). Startup (read-only) and wrap-up (winding down) skip Phase E. Default is one task per run, highest priority first. + +* Why + +Craig wants the 30-minute inbox-zero loop to also burn down the small actionable backlog autonomously, not just route capture. The existing tag convention (:next:, :quick:+:solo:) already marks exactly the solo-doable work, so Phase E acts on it under a conservative act-vs-file gate. Companion change: .emacs.d is scheduling inbox-zero on a 30-minute loop with Phase E enabled. + +* Requesting considerations for a general (cross-project) improvement + +The local edit is a stopgap; the durable, canonical version is yours to shape. Open questions for the canonical: + +1. Commit autonomy. Phase E as written assumes .emacs.d's per-project waiver (no per-commit approval gate). Most projects do NOT have that waiver. The canonical Phase E should probably gate on whether autonomous commits are allowed, defaulting to file-only (no auto-commit, surface a diff) unless a project opts in. + +2. Tag resolution. The eligibility tags and the per-project priority/tag scheme vary. Phase E hardcodes :next: / :quick:+:solo:. Better: read the project's todo.org priority/tag scheme header to resolve which tags mean "autonomous-safe." + +3. Do-not-implement markers. "Manual testing verification" is encoded here as status VERIFY (faithful to todo-format.md). Other projects may use VERIFY differently or not at all. Worth a canonical definition of the "do-not-auto-implement" marker set. + +4. Bounding and kill switch. A loop firing every 30 min that implements + commits unattended is powerful. Consider a per-run cap (or a token budget) and an explicit kill switch beyond "default one task per run." + +5. Seam. Whether autonomous execution belongs inside inbox-zero at all, or in its own workflow (e.g. work-the-backlog.org) that the loop chains after inbox-zero. Coupling capture-routing with autonomous-implementation may be the wrong seam — separating them keeps inbox-zero's three existing callers (startup, wrap-up, on-demand) clean. + +Sent from .emacs.d (helper id n/a — primary session). diff --git a/docs/design/2026-06-17-ntfy-agent-comms-proposal.org b/docs/design/2026-06-17-ntfy-agent-comms-proposal.org index ce17138..0961b47 100644 --- a/docs/design/2026-06-17-ntfy-agent-comms-proposal.org +++ b/docs/design/2026-06-17-ntfy-agent-comms-proposal.org @@ -1,5 +1,5 @@ #+TITLE: Proposal — Promote the ntfy phone channel into a general agent-comms tool -#+AUTHOR: Craig Jennings & Claude (home project) +#+AUTHOR: Craig Jennings #+DATE: 2026-06-17 * Why this is in rulesets' inbox diff --git a/docs/design/2026-06-18-triage-intake-phone-push-workflow.org b/docs/design/2026-06-18-triage-intake-phone-push-workflow.org index cd830fb..a0bb416 100644 --- a/docs/design/2026-06-18-triage-intake-phone-push-workflow.org +++ b/docs/design/2026-06-18-triage-intake-phone-push-workflow.org @@ -1,5 +1,5 @@ #+TITLE: Triage Intake Workflow (Engine) -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-01 * Summary diff --git a/docs/design/2026-07-13-runtime-portability-inventories.org b/docs/design/2026-07-13-runtime-portability-inventories.org new file mode 100644 index 0000000..b9e8244 --- /dev/null +++ b/docs/design/2026-07-13-runtime-portability-inventories.org @@ -0,0 +1,84 @@ +#+TITLE: Runtime Portability — Hook, MCP, and Memory Inventories +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-13 + +Decision-prep for the generic-agent-runtime arc (spec: [[file:2026-05-28-generic-agent-runtime-spec.org]]). Three of the eight gap child tasks are inventories; this doc is their deliverable. Each section ends with a verdict and any decision left for Craig. + +* Hook parity inventory + +What's wired today (global =.claude/settings.json=, symlinked to =~/.claude/settings.json=): + +- PreToolUse AskUserQuestion hard-deny — blocks the popup-choice tool, forcing inline numbered options per =interaction.md=. Exists because prose alone failed. +- PreCompact =precompact-priorities.sh= — saves priorities before context compaction. +- SessionStart =session-title.sh= (cosmetic title) and =session-clear-resume.sh= (resume-after-/clear plumbing). +- Stop =ai-wrap-teardown.sh= — sentinel-gated session teardown. + +Shipped but unwired (reference copies in =hooks/= + =settings-snippet.json=): =git-commit-confirm.py=, =gh-pr-create-confirm.py=, =destructive-bash-confirm.py=. Not active under Claude either, so they impose no parity requirement today. + +Language bundles wire PostToolUse validators per edit (=validate-bash.sh=, =validate-el.sh=, =validate-go.sh=, plus python/typescript equivalents) — and every bundle also ships the same validation as a =githooks/pre-commit=. + +Per-hook portability verdicts: + +- AskUserQuestion deny: moot off Claude. The popup tool is Claude Code's; Codex-style and local harnesses have no equivalent tool to deny. No port needed. +- PostToolUse validators: covered off Claude. The bundles' git pre-commit hooks run the same validators, so enforcement survives at commit time instead of edit time. Acceptable downgrade, no work needed. +- SessionStart clear-resume: not portable as-is; /clear semantics are Claude's. This is the same surface as the "session plumbing per runtime" child task — resolve it there, not as a hook port. +- session-title: cosmetic, skip on other runtimes. +- PreCompact priority-save: genuine gap. Codex-style harnesses compact without a hook point. Downgrade is prose (the session-context write triggers in protocols.org already say "save before compaction"), which is weaker but is also the pre-2026 Claude behavior. Accept prose, or shorten sessions on those runtimes. +- Stop wrap-teardown: partial gap. Codex has a turn-completion notify command that could run the same sentinel-gated script; local harnesses vary. Port the script trigger where a hook point exists; where none does, teardown becomes a manual step in the wrap phrase. + +Verdict: only PreCompact and Stop carry real porting work, and both have acceptable degraded modes. Nothing here blocks a pilot. + +* MCP portability check + +Locally-configured servers (user scope, =~/.claude.json=): linear, notion, figma, slack-deepsat, google-calendar, google-docs-personal, google-docs-work, drawio, google-keep. No project-scope servers anywhere. All are ordinary MCP server definitions, portable to any MCP-speaking harness (Codex CLI reads them from its config; most local harnesses that matter speak MCP now). Port = translate the server blocks + carry the auth material. Mechanical. + +claude.ai-managed connectors (session-injected, not in local config): Gmail, and the claude.ai variants of Calendar/Drive. These do not travel. Overlap analysis: calendar and docs are already covered by the locally-configured servers; Gmail's workflow-preferred path is already =cmail-action= (local CLI), so the managed Gmail connector is convenience, not dependency. + +Named gap: signal-mcp. protocols.org "Paging Craig" names it as the only supported away-from-desk page path, and it is not in any local config — it lives claude.ai-side. Off Claude there is currently no page channel at all. This lands squarely on the open Signal-pager task ([#C], todo.org): the signal-cli runbook that task produces would be the runtime-neutral paging path. Recommend noting runtime-portability as an added motivation on that task. + +Verdict: portable except paging; paging's fix is already a filed task. + +* Memory story for non-Claude agents + +Claude auto-memory (=~/.claude/projects/<enc>/memory/=, MEMORY.md index) is harness-owned: written by Claude's memory tooling, loaded by its session start. A non-Claude agent should neither read nor write it. + +The designed cross-agent store already exists: the org-roam KB (=knowledge-base.md= — reading is plain rg over files, writing is one node per fact, both runtime-neutral). Project state lives in file artifacts every agent reads anyway: todo.org, notes.org, session anchors, docs/. + +One wording gap: knowledge-base.md's "Capture, then promote" section names harness memory as the capture layer. For a non-Claude agent the capture layer is the session log itself. Recommended one-sentence addition there: an agent without harness memory captures into its session log and promotes from it at wrap-up. Shared-asset edit, needs approval. + +Verdict: no build. One approved sentence in knowledge-base.md closes it. + +* Skill and command parity + +What rulesets ships: 11 skills (SKILL.md, model-invokable — add-tests, debug, five-whys, flush, frontend-design, pairwise-tests, playwright-js, playwright-py, review-code, root-cause-trace, voice) and 18 commands (plain .md prompts under =.claude/commands/=, user-invoked). Every body is markdown instructions; nothing executable lives in the registration layer. + +The load-bearing observation: a =/name= reference is just a pointer to a file on disk. Any harness that reads files can execute all 29 artifacts through one resolution rule in its bootstrap entry file: "a skill or command reference (=/voice=, =/review-code=, =/brainstorm=) resolves to =~/.claude/skills/<name>/SKILL.md= or =~/.claude/commands/<name>.md= — read the file and follow it." That single sentence, emitted by the instruction-bootstrap install target, makes the whole library portable with zero per-skill work. commits.md's existing "/voice unavailable — walk the patterns inline" fallback generalizes the same way. + +What that rule does not carry: + +- Auto-invocation. Claude Code triggers skills from their descriptions; other harnesses won't. The high-value auto-triggers (voice and review-code inside the publish flow) don't actually need it — commits.md invokes them by name at fixed flow points, and the resolution rule covers a by-name invocation. The convenience triggers (debug, frontend-design firing on topic match) degrade to on-request. Acceptable. +- Native slash registration. Codex-style harnesses have their own custom-prompt mechanism; the 18 commands could also be registered there for ergonomics (an optional install nicety, not a requirement — the resolution rule already works). +- flush. Its mechanics are Claude Code's (/clear, the self-inject resume hook). Explicitly not ported; the session-plumbing child owns that surface. +- Harness built-ins (code-review ultra, plan mode, artifacts). Not rulesets' to port; flows that name them need per-runtime alternatives or graceful absence, same as today when they're unavailable. + +Verdict: no per-skill porting matrix needed. One resolution sentence in the bootstrap entry file (instruction-bootstrap child), optional native registration as an install nicety, flush explicitly excluded. + +* Session plumbing per runtime + +Four pieces, examined: + +- =session-context-path= is already runtime-aware by design — the =AI_AGENT_ID= shape carries a runtime segment (=host.project.runtime.epoch=) and the resolver is pure bash. Nothing to do. +- The anchor cycle (suspend entry, startup's interrupted-session recovery, wrap-up's archive rename) is plain files driven by workflow prose. Any agent that reads protocols.org executes it identically. Nothing to do. +- =self-inject.sh= is harness-agnostic: it types arbitrary strings into a tmux pane. The Claude-bound part is only the *payload* — "/clear" plus a resume line that leans on the SessionStart hook. A Codex auto-flush is a payload variant: the clear command differs (confirm the exact one when wiring) and the second injected line must itself carry the resume instruction ("read .ai/session-context.org and resume") since no hook fires. Arguably simpler than the Claude path. +- =session-clear-resume.sh= (the SessionStart hook) is Claude-only, and the payload variant above makes it unnecessary off Claude. + +Verdict: no build. The flush *skill* stays Claude-official; a codex auto-flush is a documented payload variant to wire the day a codex session actually wants it, not before. + +* Decisions for Craig + +All four approved by Craig, 2026-07-13: + +1. PreCompact downgrade off Claude: prose-only "save before compaction" accepted. +2. Stop-teardown off Claude: port via Codex notify where available, manual teardown elsewhere. +3. Runtime-portability motivation note added to the Signal-pager task. +4. knowledge-base.md capture-layer sentence added (non-Claude runtimes capture into the session log and promote from there). diff --git a/docs/design/2026-07-14-sentry-workflow-proposal.org b/docs/design/2026-07-14-sentry-workflow-proposal.org new file mode 100644 index 0000000..d29ceb8 --- /dev/null +++ b/docs/design/2026-07-14-sentry-workflow-proposal.org @@ -0,0 +1,150 @@ +#+TITLE: Sentry Workflow — Proposal for rulesets +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-13 + +* Status +Proposal from the work project to the rulesets session, for implementation as a +general (shared-layer) workflow: =.ai/workflows/sentry.org= plus a small +supporting script or two under =.ai/scripts/=. + +* Problem + +An agent session left running overnight can keep a project clean and healthy on +a cadence, but the current pieces don't coordinate. Two gaps: + +1. *No agent-vs-agent collision guard on shared public files.* =capture-guard= + only detects Emacs org-capture buffers (the human case) on one target file, + and it's advisory, not a lock. There is no flock anywhere in =.ai/scripts/=. + Multiple sessions (several projects, sometimes both daily drivers) can + read-modify-write =~/org/roam/inbox.org= and the =agents/= KB nodes at once. + The only coordination is the git pull/commit/push discipline in + knowledge-base.md, which is optimistic — a pull/push race forks a + =*sync-conflict*= file, and the KB search already globs those out, so we know + the forks happen and get dropped. + +2. *No single serialized runner.* Firing inbox-zero and triage-intake as + separate hourly crons means they can overlap each other and other agents. + +Sentry is one supervisor loop that runs a fixed sequence of hygiene passes +serially, holds locks so it never collides with itself or other agents, and +checkpoints aggressively for crash recovery. + +* What sentry is + +A single workflow, driven by one interactive =/loop= (one cron), that on each +fire walks an ordered list of passes inside a single turn. It replaces the +per-task crons. Design invariants: + +- *Serial.* Passes run one at a time, in order. They never overlap each other. +- *Single-runner lock.* Sentry takes a project-local flock + (=.ai/.sentry.lock=) at entry. If a prior fire is still running, this fire + skips instead of doubling up. +- *Shared-file lock.* Any pass that writes a public roam file first acquires a + roam-write lock (flock on =~/org/roam/.roam-write.lock=) held across the whole + read-modify-write-commit-push cycle, so it serializes against other agents on + the same host. =capture-guard= still runs underneath for the human case. (The + roam-write lock is new; it's the general guard the dropped KB-guard note would + have proposed, folded in here.) +- *Non-destructive by default.* Read-only and idempotent-filing passes run + fully. Anything destructive or judgment-heavy (triage trash/mark-read, task + regrades, spec flips) queues its proposal into the digest for morning + approval, never fires unattended. +- *Idempotent + isolated.* Every pass is safe to repeat. A pass that errors + doesn't abort the rest; its failure is a loud banner in the digest. + +* Crash-recovery spine (applies to every pass) + +Crash recovery has been a live consideration. Two enforced steps wrap each pass: + +1. *Session-context between passes.* Before the next pass starts, the current + pass appends a dated entry to =.ai/session-context.org= recording what it + did. A crash mid-sequence leaves a readable "got this far" trail, so the next + session resumes without re-deriving state. +2. *Local commit, no push, on any disk change.* Every pass that writes to disk + gets its own =chore(sentry): <pass> — <what changed>= commit on the current + branch, unpushed. A crash loses at most one pass. Craig reviews the stack in + the morning and pushes deliberately. Conventional messages, no AI + attribution. (Open decision: commit on the current branch vs a dedicated + =sentry/<date>= branch — see Open decisions.) + +The unit per pass is therefore: do the work → log to session-context → commit. +That triplet is the recovery boundary. + +* Passes (v1 — all ten, plus the first-run KB promotion) + +Ordered so read-only/pull passes precede writes, and the heaviest external +sweep sits mid-run: + +1. *Roam pull* — =git -C ~/org/roam pull --ff-only= so later reads are fresh. + Read-only. +2. *Inbox zero* — inbox.org roam mode. Route roam-inbox items this project + owns. Acquires the roam-write lock for the inbox edit. +3. *Triage intake* — triage-intake.org. External-accounts sweep, classify, + file Action tasks. Destructive actions (trash/mark-read) queue for morning; + =mbsync -a= and the sentinel advance run. +4. *Todo cleanup* — clean-todo.org + todo-cleanup.el + lint-org.el. Convert + level-3 done subtasks to dated logs, archive done subtrees, flag lint. +5. *Task audit* — task-audit.org. Re-check =:solo:=/=:quick:= tags, + priority-scheme conformance, bug severity×frequency grades, stale =DOING= + specs with closed parents, blocked/blocker reciprocity. Regrades queue for + morning. +6. *Working-files hygiene* — flag =working/<slug>/= dirs whose task is DONE but + not filed to =assets/=. Report only; filing is a morning decision. +7. *Spec status board* — the docs-lifecycle grep. Flag =DOING= specs with + closed parents, or specs stuck in =DRAFT=. +8. *Link integrity* — lint-org broken =file:= links across todo/notes/prep. +9. *Git health* — uncommitted drift, unpushed commits, stale local branches, + whether main is behind origin. +10. *Prep + symlink freshness* — verify tomorrow's prep and both symlinks + resolve. +11. *KB lesson promotion (first run of the session, and thereafter as new + lessons accrue)* — promote recent durable lessons out of the fast capture + layer. CLASSIFICATION-GATED: + - Personal project → write to the roam KB as =agents/= nodes (pull, lock, + write, commit, push), per knowledge-base.md. + - Work / denylisted project → NEVER touch roam. Promote to the project's own + durable store (e.g. =deepsat/knowledge.org=) instead, and emit the + one-line refusal note from the knowledge-base.md refusal contract so + nothing is lost silently. + +* Digest output + +One consolidated, timestamped summary per fire: run time first, then per-pass +one-liners (what it found / did), the morning-approval queue (proposed +trash/mark-read, regrades, files-to-move), and any failures as loud banners at +the top. Delta-only where a pass supports it (triage): a pass with nothing to +report is one line. + +* Cadence + +One hourly sentry to start (passes short-circuit fast when there's nothing to +do). A slower evening interval (every 2–3 hours) is a config knob if hourly +proves noisy. + +* Open decisions for rulesets + +1. *Commit target* — per-pass commits on the current branch (simplest, matches + "commit but don't push") vs a dedicated =sentry/<date>= branch (keeps the + working branch clean, costs a branch dance). Proposal leans current branch. +2. *Roam-write lock scope* — host-local flock only (solves same-host + multi-agent, the common case) vs also hardening roam-sync to surface + cross-host conflicts loudly. Proposal: ship the flock now, note the + cross-host limit, treat roam-sync conflict-surfacing as a separate follow-up. +3. *Interval default* — hourly vs evening-only. Proposal: hourly, config knob. +4. *Which passes gate on a green tree* — e.g. skip todo-cleanup commits if the + project suite is red, matching inbox monitor-mode's clean-tree gate. + +* Cover note from sender (work, 2026-07-14 00:02) + +Intro for the sentry-proposal.org file just sent. This is a NEW general workflow proposal (.ai/workflows/sentry.org + supporting scripts under .ai/scripts/), not an edit to an existing synced file. + +What it is: one serialized supervisor loop that runs a project's evening hygiene passes on a cadence, with locks so it never collides with itself or with other agents. It replaces firing inbox-zero + triage-intake as separate crons. + +Why now: an agent left running overnight can keep a project clean, but the current pieces don't coordinate. capture-guard only handles the Emacs-capture case on one file and holds no lock; there is no flock anywhere in .ai/scripts/; and separate crons let inbox-zero and triage overlap each other and other agents on shared roam files, which already fork *sync-conflict* files that the KB search globs out. + +Three design points worth your judgment: +1. Crash-recovery spine on every pass (crash recovery has been a live concern): enforce a session-context.org update BETWEEN passes, and a local commit (no push) after any disk change, so a crash loses at most one pass. +2. A new roam-write flock (~/org/roam/.roam-write.lock) as the shared-file guard. This absorbs a separate 'guard all public KB files' note we considered and dropped. The lock is the better home for that concern than a bespoke per-file guard. +3. The KB lesson-promotion pass is classification-gated: personal projects promote to roam agents/ nodes; a denylisted work project promotes to its own local store (deepsat/knowledge.org) and never touches roam, per knowledge-base.md's refusal contract. + +Companion files to reconcile: knowledge-base.md (the gated promotion + refusal contract), inbox.org (roam mode + the new lock), triage-intake.org (runs as a sentry pass; keep its own triggers), capture-guard (becomes one layer under the roam-write lock, not the whole guard), and clean-todo.org / task-audit.org / lint-org.el (invoked as passes). Open decisions for you are listed at the bottom of the proposal file: commit target, lock scope, interval default, and which passes gate on a green tree. diff --git a/docs/design/2026-07-15-subproject-pattern-proposal.org b/docs/design/2026-07-15-subproject-pattern-proposal.org new file mode 100644 index 0000000..61ade18 --- /dev/null +++ b/docs/design/2026-07-15-subproject-pattern-proposal.org @@ -0,0 +1,22 @@ +#+TITLE: Proposal: promote the "subproject" pattern into the claude-r +#+SOURCE: from home +#+DATE: 2026-07-15 23:24:07 -0500 + +Proposal: promote the "subproject" pattern into the claude-rules layer. + +WHAT WE DID (in home, 2026-07-15) +We named and built a pattern for what happens when a standalone project is consolidated into another project. home absorbed nine former projects on 2026-06-11 (danneel, finances, jr-estate, kit, health, documents, clipper, elibrary, philosophy). We now call each folded-in unit a "subproject": a former standalone project that lives as a subdirectory, shares the parent's .ai/ scope (one session, one toolchain), but keeps its own files, assets, and history self-contained under its subdir. + +The pattern has four parts: +1. Vocabulary — parent project, subproject, consolidation (the process), subproject archiving. Terminology so we can talk precisely about where a subproject's documents/assets live and archive them cleanly. +2. Subproject brief — a read-first orientation doc at <subproject>/<subproject>-brief.org (situation, current state, reading list, and for deep ones cast/timeline/key-facts-with-verify-flags). The parent's notes.org holds a thin index + a read-first rule + pointers to urgent items only, never a copy of subproject substance ("one fact, one home"). +3. Lifecycle criteria — when to CREATE a subproject (consolidate: domain fit, low independent cadence, shared-scope safety re: privacy/remote posture, no independent collaborators), and when/how to ARCHIVE one cleanly (terminal state -> move the self-contained subdir to <parent>/archive/, reconcile pointers/links/tags, manifest note). +4. Instrumentation — a use/miss log, metrics weighted toward Craig-caught errors over self-report, review folded into task-audit, and a falsifiable kill criterion. + +WHY IT'S GENERAL (not just a home thing) +Consolidating a project into another drops what the former project's own startup used to auto-load (its full depth), so sessions start cold and re-read files. That failure recurs whenever ANY project folds into another. The pattern also gives consolidation and archiving a named, repeatable shape, and the self-contained-subdir rule is what makes archiving a move-plus-pointer-reconcile instead of an excavation. + +THE PROPOSAL +Promote this to a rulesets claude-rule (e.g. claude-rules/subprojects.md): the vocabulary, the brief convention, the parent-vs-subproject content criterion, and the create/archive criteria. Any parent/hub project would then inherit it. home's .ai/subprojects.org (attached next) becomes the local instance/index under the general rule. It's a companion to working-files.md (in-progress artifact layout) and docs-lifecycle.md (formal-doc lifecycle) — same family, different scope: this one governs whole-subproject orientation and lifecycle. + +Attached: the home instance .ai/subprojects.org as the reference draft to adapt. Apply your value gate; this is a proposal, not a synced-file edit. diff --git a/docs/design/2026-07-15-subprojects-convention-home-instance.org b/docs/design/2026-07-15-subprojects-convention-home-instance.org new file mode 100644 index 0000000..b031a41 --- /dev/null +++ b/docs/design/2026-07-15-subprojects-convention-home-instance.org @@ -0,0 +1,282 @@ +#+TITLE: Subprojects — Convention +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-15 + +# The reference spec for the subproject pattern: what a subproject is, when to +# create one (consolidation), how to keep it legible (the brief + content +# split), and how to archive one cleanly. Not read every session — the +# always-on parts (the subproject index + the read-first rule) live in +# notes.org and point here. Read this when consolidating a project, creating +# or maintaining a subproject brief, or archiving a subproject. + +* Vocabulary (say it the same way every time) + +The terminology is the point — it's what lets us talk precisely about where a +subproject's documents and assets live, and archive them cleanly. + +- *Parent project* (a.k.a. hub) — a top-level project that owns an =.ai/= + scope: its own protocols, session cadence, memory, inbox, todo. home, work, + rulesets, .emacs.d are parent projects. Reached by launching an agent + session in its directory. +- *Subproject* — a formerly-standalone project that has been *consolidated* + into a parent and now lives as a subdirectory of it (=danneel/=, + =finances/=, =kit/= …). A subproject shares the parent's =.ai/= tooling and + session — it is not a separate agent scope — but keeps its own working + files, assets, notes, and history self-contained under its one subdirectory. +- *Consolidation* — the process of turning a standalone project into a + subproject of a parent. (home's nine subprojects were consolidated on + 2026-06-11.) The inverse — promoting a subproject back to a standalone + project — is rare but allowed; it's just consolidation run backward. +- *Subproject archiving* — retiring a completed or abandoned subproject + cleanly. Because a subproject is self-contained under one subdirectory, + archiving is a move plus a pointer reconcile, not an excavation. +- *Subproject brief* — the read-first orientation doc for one subproject, at + =<subproject>/<subproject>-brief.org=. + +The cross-project boundary rule (=claude-rules/cross-project.md=) governs +*parent* projects, which are separate =.ai/= scopes. It does *not* apply +between a parent and its subprojects — they share one scope. + +* Why the pattern exists + +As a standalone project, each of these units auto-loaded its own notes.org — +its full depth — at every startup, so recall felt instant. Consolidated into a +parent, a subproject is one compressed section competing with the others, and +the parent's startup surfaces hub-wide state, not any one subproject's depth. +So a session starts cold on a given subproject and re-reads several files on +demand. This convention restores fast, accurate orientation without +re-bloating what loads every session — and gives consolidation and archiving a +named, repeatable shape. + +* Document & asset location (what makes clean archiving possible) + +Everything for a subproject lives under =<subproject>/= — its =notes.org=, +=assets/=, working files, event log, briefs. Links *within* a subproject are +relative to the subproject dir, so the whole tree can move without breaking +them. The parent's own top-level =assets/= is for parent-level material only, +never a subproject's. This self-containment is exactly what lets archiving be +a single =mv= plus a pointer reconcile. Keep it strict: a subproject's asset +in the parent's shared =assets/= is a future broken link. + +* The two layers (parent vs subproject) + +- *Always-on layer* — the parent's notes.org. Read at every startup, so every + line is a tax paid whether or not it's relevant today. Holds only what must + be seen session-independently. +- *On-demand layer* — the subproject brief + the subproject's notes.org. + Loaded only when working that subproject. Holds the depth. + +The whole design is: sort content by *when it's needed*, keep the always-on +layer thin, put the depth one open away. + +* Creating a subproject (consolidation criteria) + +Consolidate a standalone project into a parent as a subproject when *all* hold: + +1. *Domain fit* — the project is a facet of a domain the parent already owns + (danneel is home life-admin; finances is home). +2. *Low independent cadence* — it no longer needs its own session rhythm. + You'd rather reach it from the parent than launch it standalone, and its + standalone startup/sync/session overhead now outweighs the isolation. +3. *Shared-scope safety* — it does not need a git/remote/visibility posture + distinct from the parent. A public code project, a team repo, or anything + with its own CI or gitignore-privacy posture *stays standalone* — folding + it would break the parent's privacy model (see + =claude-rules/git-hosting-privacy-model= and the gitignore-vs-track + decision). Personal/documentation units fold; code-with-a-public-remote + does not. +4. *No independent collaborators* depend on it as a standalone deliverable. + +Keep it standalone when any of those fail: active independent development, its +own team or remote, a distinct visibility/security posture, or activity high +enough that competing in the parent's hub would bury it. + +*The consolidation act itself* (mechanics, mirroring home's 2026-06-11 fold): +move the project tree under the parent as =<parent>/<subproject>/=; import its +tasks into the parent's todo with an area =:tag:= and a =:MIGRATED_FROM:= +property; fold its live reminders/decisions into the parent's notes as +pointers (see the content split); build the subproject brief; record the fold +in a consolidation manifest (=docs/consolidation-manifest-<subproject>.org=). +Retire the source to =~/projects/.retired/<name>= once parity is verified. + +* The subproject brief + +** Path (fixed — discovery is mechanical) + +=<subproject>/<subproject>-brief.org= (=danneel/danneel-brief.org=, +=finances/finances-brief.org=). The subproject name is the directory name, so +the brief name follows for free. One predictable name means the read-first +rule never has to hunt; put descriptive richness *inside*, not in the filename. + +** Spine (scales from light to deep) + +Required in every brief, even a fifteen-line one: +- *One-paragraph situation* — what this subproject is, in plain language. +- *Current state / open threads* — the live stuff. The only high-churn + section; everything else is stable. +- *Reading list* — the subproject's deeper files and what each holds. The + brief *points into* notes.org and the rest; it does not copy them. + +Add only where the subproject warrants it (danneel, jr-estate, finances earn +all of these; elibrary or documents may earn none): +- Cast / contacts. +- Timeline (for matters with history). +- Key facts / numbers, with explicit *verify* flags anywhere the underlying + record disagrees with itself. A brief that asserts a wrong number is worse + than no brief — flag, don't guess. + +** Read-first rule (the behavior) + +When a session's work targets a subproject — a file under =<subproject>/=, its +=:tag:=, or Craig naming the topic — open =<subproject>/<subproject>-brief.org= +*first*, before touching anything else in it. If the subproject has no brief +yet, creating it is the first step of the work (lazy backfill), and the same +pass reconciles that subproject's parent-vs-subproject content. + +* Parent-vs-subproject content criterion + +The test for any piece of content: *if next session I'm working a different +subproject, do I still need to see this?* Three bins: + +1. *Yes, and it's a fact or policy* → hub-level → the parent's notes.org. + Machine names, mail policy, calendar access, the hub map, the subproject + index. +2. *Yes, but only because it's urgent* — a deadline or blocking status that + would be missed if it only lived where I might not look this week → the + parent holds a *one-line pointer* (item + date + "see the subproject + brief"); the substance stays in the subproject. +3. *No* → the subproject. Working depth: contacts, contract facts, case + theory, evidence, document index, and any reminder that isn't time-critical. + +One hard rule over both axes: *one fact, one home.* The parent never copies +subproject substance — it points. The parent holds the trigger; the subproject +holds the truth. Every parent↔subproject duplicate is a second surface that +goes stale. + +* Archiving a subproject (archiving criteria + clean process) + +Archive a subproject when it reaches a *terminal* state: +- *Complete* — the work is done and closed (a settlement signed, a probate + closed, a trip finished): no live threads, no open tasks, nothing + time-sensitive in the parent still pointing at it; or +- *Abandoned / superseded* — dropped, or replaced by other work. + +Clean archive process (self-containment is what makes each step a one-liner): +1. Close or settle its tasks in the parent's todo (DONE / CANCELLED), per the + todo completion rules. +2. Move the whole subproject tree to the parent's archive: + =<parent>/archive/<subproject>/= (keep it intact — archiving is reversible, + not deletion). +3. Update inbound =file:= links that pointed into it (grep first, per the + keep-links-current rule). +4. Move or retire its brief with it, and drop its row from the active + subproject index (or mark it =archived= there). +5. Strip its pointers/reminders from the parent's always-on layer, and retire + its =:tag:= from active use. +6. Record the archive in a manifest note + (=docs/archive-manifest-<subproject>.org=): why, when, where it moved, + what links were repointed. Mirrors the consolidation manifest. + +The =:LAST_UPDATED:= on a brief plus "no open tasks under the tag" is the +signal a subproject is a candidate for archiving — surfaced at the review +cadence, never auto-archived. + +* Freshness + +- *Wrap-up hook*: at wrap-up, for each subproject touched this session, refresh + its brief's Current-state section and bump its =:LAST_UPDATED:= date. Stable + sections rarely change, so this is cheap. +- *Staleness nudge*: a brief whose =:LAST_UPDATED:= is old while its subproject + was recently active is worth surfacing at startup. + +* Self-improvement loop + +Every real use emits a signal, and the signal is *captured*, not silently +acted on: +- Brief answers the question → *hit*. +- Brief is missing something, stale, or wrong → *miss*, logged in + subprojects-log.org (a home-side artifact; no rulesets file). +- Each miss drives *two* updates: *local* (fix that brief) and, if the same + kind of miss recurs across subprojects, *structural* (fix this spine/spec, + not just the one file). The structural half is the actual self-improvement — + grow-and-refine from real use rather than rewriting from scratch (the ACE + idea behind the =codify= skill). + +* Metrics + +Split by who can trust them. + +*Process signals (I observe, cheaply):* +- *Orientation cost* — files/tool-calls before my first correct action in a + subproject. Target: ~1 (the brief), occasionally a second deep dive. +- *Brief hit rate* — fraction of subproject questions answered from the brief + alone. Should climb. +- *Freshness gap* — days between =:LAST_UPDATED:= and last subproject activity. + Target ~0. + +*Outcome signals (Craig is ground truth — weight these over the above):* +- *Craig-caught errors* — the real quality bar. Target: trend to zero. If + briefs work, Craig stops catching me. +- *Felt speed* — did dropping into the subproject feel fast and right. + +*Honesty caveat.* Most process signals are self-observed, and self-report is +biased toward "hit." So the external signals (Craig-caught errors, felt speed) +outrank mine, and the log is auditable so Craig can spot-check my self-grades. +The instrumentation must stay lighter than the briefs, or it rots like an +unmaintained brief. Small N, no clean A/B — this is directional evidence, not +proof. + +* Review cadence + +Fold a "subprojects health" check into the existing task-audit cadence: read +the log, look at the hit-rate / correction / freshness trends, flag any +subproject that's gone terminal (archive candidate), and check the +parent-vs-subproject criterion — anywhere the parent holds a copy where it +should hold a pointer. Decide continue / adjust / kill for the pattern itself. + +* Kill criteria (falsifiable) + +After ~6-8 subproject-sessions across ≥3 subprojects, drop or rework the +pattern if: +- the correction rate isn't falling, or Craig is still catching errors; or +- the freshness gap keeps reopening despite the wrap-up hook; or +- maintenance costs more time than the orientation it saves. + +Success is the mirror: orientation down to ~1 read, Craig catching ~nothing, +briefs current, and I'm right fast in any subproject cold. + +* Rollout + +- Define the convention (this file) + the index scaffold in notes.org. Done + 2026-07-15. +- *Lazy backfill*: the next session that touches a subproject with no brief + creates the brief as its first step — cost paid exactly when it's used. +- *Reconcile in the same pass*: building a subproject's brief includes + reconciling its parent-vs-subproject content — push substance down, leave + pointers up, drop dead reminders. +- *Promotion*: this pattern is home-instanced but general. Consolidation into + subprojects will recur whenever any project folds into another, so this is a + candidate to promote into the rulesets rules layer (proposed to rulesets + 2026-07-15). + +* Adoption status + +| Subproject | Brief exists | Reconciled | Notes | +|------------+--------------+------------+---------------------------------------| +| danneel | yes | yes | First worked instance, 2026-07-15. | +|------------+--------------+------------+---------------------------------------| +| clipper | no | no | Lazy backfill on next touch. | +|------------+--------------+------------+---------------------------------------| +| documents | no | no | Lazy backfill on next touch. | +|------------+--------------+------------+---------------------------------------| +| elibrary | no | no | No notes.org yet; lightest subproject.| +|------------+--------------+------------+---------------------------------------| +| finances | no | no | Deep; earns the full spine. | +|------------+--------------+------------+---------------------------------------| +| health | no | no | Has its own notes.org. | +|------------+--------------+------------+---------------------------------------| +| jr-estate | no | no | Deep; earns the full spine. | +|------------+--------------+------------+---------------------------------------| +| kit | no | no | Lazy backfill on next touch. | +|------------+--------------+------------+---------------------------------------| +| philosophy | no | no | Lazy backfill on next touch. | diff --git a/docs/design/2026-07-16-polyglot-bundle-collision.txt b/docs/design/2026-07-16-polyglot-bundle-collision.txt new file mode 100644 index 0000000..b7e1257 --- /dev/null +++ b/docs/design/2026-07-16-polyglot-bundle-collision.txt @@ -0,0 +1,30 @@ +Proposal: language bundles collide on coverage-makefile.txt in a polyglot project, and the second install loses silently. + +Context: scaffolded a new project (clock-panel) on 2026-07-16 with both the python and typescript bundles installed into the same project. + +What happened. The second install printed: + + [skip] coverage-makefile.txt already exists (use FORCE=1 to overwrite) + +Both bundles ship a file at that same path. Python installed first and won, so the project ended up with Python's coverage fragment only. The TypeScript one (c8, Istanbul json-summary) never landed. The line reads like routine idempotence, not a dropped deliverable, so it's easy to skim past. I only caught it by checking which targets were actually in the file. + +The workaround is bad. FORCE=1 gets the TypeScript fragment but overwrites the Python one, so you can't have both without renaming by hand between installs. FORCE=1 also re-seeds CLAUDE.md, which is fine on a fresh project and destructive on a customized one. What I did in clock-panel: install python, mv coverage-makefile.txt coverage-makefile-python.txt, install typescript FORCE=1, mv coverage-makefile.txt coverage-makefile-typescript.txt. Both fragments now coexist under language-suffixed names. + +The deeper problem, which the filename collision only hints at: both fragments define targets with the SAME names. + + coverage: + coverage-summary: + +So even with both files present, a polyglot project can't copy both into one Makefile. It gets duplicate targets. The fragments assume they're the only language in the project. Renaming the files doesn't fix that, it just makes both sets of instructions visible while leaving the conflict for whoever pastes them. + +Options, in the order I'd weigh them: + +Suffix the shipped filename per bundle (coverage-makefile-python.txt, coverage-makefile-typescript.txt) so installs never collide. Cheap, and it makes the multi-bundle case work by default. It leaves the target-name conflict. + +Namespace the targets too (coverage-python / coverage-typescript, with a coverage target that runs both). Fixes the real problem for polyglot projects. Bigger change, and it makes single-language projects type a longer target name unless there's an alias. + +At minimum, make the skip line loud when the skipped file came from a DIFFERENT bundle than the one that wrote it. A skip that means "already yours" and a skip that means "another language's file is here and yours is being dropped" currently look identical. + +Worth deciding whether polyglot projects are a supported case at all. If they are, the bundles need a collision story. If they aren't, install-lang could say so when it detects a second bundle going into a project that already has one, rather than half-installing. + +Also flagged separately in a note from home today: the same install-lang run is where I'd look for other per-bundle files that could collide on a shared filename. I only checked coverage-makefile.txt. diff --git a/docs/design/2026-07-17-dated-log-planning-line-strip-proposal.md b/docs/design/2026-07-17-dated-log-planning-line-strip-proposal.md new file mode 100644 index 0000000..2448600 --- /dev/null +++ b/docs/design/2026-07-17-dated-log-planning-line-strip-proposal.md @@ -0,0 +1,15 @@ +Proposal: two linked gaps that let a closed sub-task keep polluting the org agenda. + +WHAT HAPPENED (home, 2026-07-17) +Six completed sub-tasks under a DONE parent (a finished trip) had each been closed correctly to the dated event-log form per todo-format.org's depth-based completion rule — keyword, priority, and tags dropped, heading rewritten to "YYYY-MM-DD Day @ HH:MM:SS -ZZZZ <past-tense>". But every one kept its old active SCHEDULED: <date> planning line. A dated-log heading has no TODO keyword, and org-agenda renders ANY headline carrying an active SCHEDULED, so all six showed on the daily agenda as weeks-overdue ("Sched.31x") long after the work and the parent were done. They're invisible to a keyword scan (no TODO) and survive archive-done (parent isn't level-2 CANCELLED), so nothing catches them. + +ROOT CAUSE +The completion rewrite strips keyword/priority/tags but nothing strips the planning line, and an interactive org close (org-log-done) only stamps CLOSED — it never removes a pre-existing SCHEDULED/DEADLINE. So the stale timestamp lingers and pins the entry to the agenda permanently. + +TWO FIXES, both rulesets-owned: + +1. todo-format.org — the sub-task completion rule (the "*** and deeper — rewrite to a dated event-log entry" section). Add an explicit step: remove any SCHEDULED:/DEADLINE: planning line when rewriting to the dated form. The completion time already lives in the heading; an active planning date on a historical log entry is always wrong. The VERIFY dated-completion path and todo-cleanup.el --convert-subtasks want the same treatment (the batch converter pulls the timestamp from CLOSED and keeps heading text — it should also drop the planning line). + +2. lint-org.el — add a checker (proposed name: dated-log-heading-active-timestamp). Flag any dated-log heading (a heading matching the YYYY-MM-DD ... @ ... form, no TODO keyword) that still carries an active <...> SCHEDULED or DEADLINE. This is the mechanical backstop for #1, the same way subtask-done-not-dated backstops the depth rule. It would have caught all six here. + +Local fix already applied in home (stripped the six lines by hand); this is about the durable convention + lint so the next instance is prevented and caught. No home-specific detail needs to travel — the gap is general to any project using the dated-log completion form. diff --git a/docs/design/2026-07-17-todo-cleanup-dated-seal-proposal.md b/docs/design/2026-07-17-todo-cleanup-dated-seal-proposal.md new file mode 100644 index 0000000..61d457c --- /dev/null +++ b/docs/design/2026-07-17-todo-cleanup-dated-seal-proposal.md @@ -0,0 +1,38 @@ +Proposal: change todo-cleanup.el's --archive-done aging to a dated-seal model + +Origin: work project, 2026-07-17. Reconciling a stale rotation convention with the tool's actual behavior. Craig ratified the design in-session. + +## The problem + +`--archive-done` today does two things: move level-2 DONE/CANCELLED out of Open Work into Resolved, then age Resolved by moving subtrees older than `tc-archive-retain-days` (default 7) into one rolling file `tc-archive-file` (default archive/task-archive.org). + +Two mismatches with the intended convention: + +1. Retention is 7 days; the convention wants the last month kept inline in Resolved. +2. One rolling file forever; the convention wants periodic dated files so todo.org's archive is browsable by seal date. + +An earlier attempt to name files by calendar quarter (resolved-YYYY-QN.org) hit a seam: with a one-month retention window, a task closed in the last month of a quarter isn't archived until after the quarter boundary, so a quarter-named file can't hold it without mislabeling. + +## The design (ratified) + +Redefine the sealed file by a predicate that's always true of its contents, and date it by the seal run rather than by calendar quarter: + +- A level-2 DONE/CANCELLED subtree is archived when its CLOSED date is older than one month, OR its CLOSED date can't be parsed (archive regardless — a keyword-complete task with no readable close date is cruft, not live work). +- The working (open) file keeps the existing name task-archive.org. A seal renames it to resolved-YYYY-MM-DD.org (the seal date) and starts a fresh working file. +- The dated file means "everything sealed as of that date," not a quarter — so a late-quarter close archived after a boundary is never mislabeled. Cadence (quarterly) becomes independent of correctness; slip is harmless. + +## Concrete changes to todo-cleanup.el + +1. `tc-archive-retain-days` default 7 → 31 (or expose it; the value should reflect "one month"). Reconcile the test at test-todo-cleanup.el:362 that asserts 7-day aging. +2. Aging predicate: archive when `closed < now - retain-days` OR `closed` is unparseable. Today an unparseable-CLOSED subtree is already moved out in aging per the docstring ("those with no parseable CLOSED date are moved out") — keep that, and make it explicit in the contract. +3. Add a seal step (new flag, e.g. `--seal`, or fold into a boundary check): when invoked, rename the current `tc-archive-file` (task-archive.org) → `resolved-<today>.org` beside it and let the next aging recreate a fresh working file. Whether the seal fires automatically at a quarter boundary or stays a manual flag is your call — the manual quarterly rotation task covers it until then. +4. Tests: update the file-aging tests (test-todo-cleanup.el:362, :378, :491, :513, :518) for the new retain default and the seal/rename behavior. The gitignore-inheritance logic (tc--ensure-archive-gitignored) applies unchanged to both the working and sealed names. + +## Companion state already applied in the work project (for reference, not to sync) + +- todo.org grew a `* Work Resolved` section; 30 level-2 DONE/CANCELLED moved there. +- archive/task-archive.org renamed to archive/resolved-2026-07-17.org (grandfathered H1 seal), inbound link updated. +- archive/README.org rewritten to the dated-seal contract above. +- The rotation task rewritten to the new procedure. + +The rulesets change is only the todo-cleanup.el behavior + its tests. The README lives per-project (this one is already updated); if rulesets ships a canonical archive README template, align it to the dated-seal contract too. diff --git a/docs/design/2026-07-18-colloquialisms-and-the-list-proposal.md b/docs/design/2026-07-18-colloquialisms-and-the-list-proposal.md new file mode 100644 index 0000000..7254447 --- /dev/null +++ b/docs/design/2026-07-18-colloquialisms-and-the-list-proposal.md @@ -0,0 +1,22 @@ +#+TITLE: PROPOSAL: a "Colloquialisms and Expansions" convention + the +#+SOURCE: from home +#+DATE: 2026-07-18 18:04:42 -0500 + +PROPOSAL: a "Colloquialisms and Expansions" convention + the "the list" before-close-queue norm. Craig recommends other projects adopt both. Home has implemented it locally as the reference; sending it up so rulesets can decide whether to make it a shared norm. + +THE NORM: "the list" = a before-close FIFO queue +When Craig says "put X on the list" / "add X to the list", X is appended to a Before-Close Queue: a FIFO queue of tasks and actions to finish before the session closes (wrap-up). Work oldest-first. Process the queue at wrap-up before teardown, surfacing anything unfinished rather than dropping it. Session-scoped: resets when the session anchor is archived at wrap. Anything that must outlive the session is a todo.org task instead. + +Home implementation (reference): +- The norm is documented in home .ai/notes.org under a new "* Colloquialisms and Expansions" section (notes.org is read every startup, so the agent honors it without a workflow change). +- The queue itself lives in the session anchor (session-context.org) under a "* Before-Close Queue" heading, seeded empty. + +THE BROADER IDEA: a "Colloquialisms and Expansions" shorthand dictionary +A per-project (or shared) reference mapping Craig shorthand phrases to their expansion, so the agent applies them without asking. Seed entries Craig named: +1. "the list" -> append to the Before-Close Queue (above). +2. "tell <project> <message>" -> drop the message in that project inbox via inbox-send (python3 .ai/scripts/inbox-send.py <project> --text "..."), the sanctioned cross-project handoff, never a direct write. + +WHY RULESETS SHOULD CARE +Both are cross-project by nature. "tell <project>" already rides inbox-send, which every project has. The before-close queue is a session-lifecycle concept that wrap-it-up owns, and wrap-it-up is a rulesets-owned synced workflow. If this becomes a shared norm, the durable wiring is: (a) a colloquialisms reference shipped in the template (or a protocols.org section), and (b) a wrap-it-up step that processes the Before-Close Queue before teardown. Home cannot wire (b) durably from downstream because wrap-it-up.org is synced, so it is stubbed via the notes.org norm for now and needs the canonical change to stick. + +RECOMMENDATION FROM CRAIG: roll this shorthand out to the other projects. diff --git a/docs/design/2026-07-20-signal-pager-runbook.org b/docs/design/2026-07-20-signal-pager-runbook.org new file mode 100644 index 0000000..f31ed18 --- /dev/null +++ b/docs/design/2026-07-20-signal-pager-runbook.org @@ -0,0 +1,198 @@ +#+TITLE: Signal Pager Runbook +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-20 + +The operational reference for the agent pager — how a page reaches Craig's +phone, how his replies come back, how the account stays healthy, and the +signal-cli setup behind it. This is the Signal successor to the retired ntfy +runbook. Canonical home is rulesets because the pager is cross-machine tooling. + +* What the pager is + +One Signal identity, =+15045173983=, registered in *velox's* signal-cli +(account file 465310, velox is the primary device). It is a dedicated pager +number, not Craig's personal Signal. Pages go *from* that identity *to* Craig's +own Signal account, which fires a normal mobile push on his phone. + +As of 2026-07-20 the identity spans two devices: velox (primary) and ratio +(linked device "ratio-pager"). Any machine holding the account sends directly; +a machine that doesn't relays to velox over the tailnet. + +Two constants the tooling depends on: + +- Pager account: =+15045173983= (primary on velox, linked on ratio). +- Recipient: Craig's Signal account UUID =b1b5601e-6126-47f8-afaa-0a59f5188fde=. + His phone *number* reads as unregistered in Signal's directory — always + target the UUID, never the number. + +velox is the laptop that travels with Craig, so the pager account rides with +him; ratio holds it too, so a page still lands when velox is down. + +* Choosing a channel + +Two trigger words, two channels, and both work from any agent runtime (nothing +here is Claude-specific). protocols.org "Reaching Craig" is the short version +pointed at every project; this runbook is the full one for the Signal side. + +- *"page me"* — desktop notification, stays up until dismissed: + + #+begin_src bash + notify info "Title" "Message" --persist + #+end_src + +- *"text me"* — the phone, over Signal: + + #+begin_src bash + agent-text "Message for Craig's phone" + #+end_src + +- *"text and page me"* — both. The default when a run can't tell whether he's + away: the desktop one is free and the phone one reaches him if he is. + +* Sending a text + +=agent-text= (shipped at =claude-templates/bin/agent-text=, installed to +=~/.local/bin= by =make -C ~/code/rulesets install=) is the interface. It hides +the machine topology by checking whether the account is registered in the +local signal-cli: + +- If the account is local (velox's primary or a linked device like ratio), it + sends directly — no velox dependency. +- Otherwise it ssh-relays the send to velox over the tailnet. +- On failure (velox down or unreachable from a non-linked machine) it prints the + desktop fallback line and exits non-zero, so a caller can tell the page did not + land. + +The raw command it runs, for reference or a manual send from velox: + +#+begin_src bash +signal-cli -a +15045173983 send -m "your message" b1b5601e-6126-47f8-afaa-0a59f5188fde +#+end_src + +From another machine, the same send relayed over the tailnet: + +#+begin_src bash +ssh velox.tailf3bb8c.ts.net \ + "signal-cli -a +15045173983 send -m 'your message' b1b5601e-6126-47f8-afaa-0a59f5188fde" +#+end_src + +Prefer =agent-text= over the raw command — it hardens the message for the remote +shell and handles the fallback. Reach for the raw form only when debugging. + +* Reading replies + +Craig replies to a page straight from Signal on his phone. The reply is a normal +data message *to* the pager account, so it is waiting in the pager's inbound +queue until something receives it. + +Drain the queue and read what is there: + +#+begin_src bash +# On velox: +signal-cli -a +15045173983 receive --timeout 10 +# From another machine: +ssh velox.tailf3bb8c.ts.net "signal-cli -a +15045173983 receive --timeout 10" +#+end_src + +=receive= prints every queued envelope and exits 0 once the queue drains or the +timeout elapses. A text reply from Craig arrives as an envelope from his UUID +carrying a =Body:= line — that line is the reply text. Most envelopes are +delivery/read receipts and typing indicators (no =Body:=); the reply you want is +the data message with body text. For a script that waits on a reply, add +=--send-read-receipts= so his phone shows the page was read, and parse stdout for +the =Body:= line on an envelope from =b1b5601e-…=. + +Note: =receive= is destructive — it consumes the queue. Whatever drains the +queue (an on-demand read, or the warm-keeping timer below) is what sees the +reply, and it is seen once. An agent that pages and then waits for an answer +should do its own =receive= rather than race the timer. + +* Keeping the account warm (receive timer) + +Signal expects a registered account to receive regularly. Left alone, the pager +account drifts stale — signal-cli warns "Messages have been last received N days +ago" (observed at 47 days on 2026-07-20 before a manual drain reset it). A stale +account is a reliability risk on the one channel that reaches Craig when he is +away. + +The fix mirrors roam-sync: a systemd user timer that drains the queue on a +cadence, keeping the account warm and, as a bonus, picking up async replies. With +the account linked on both machines, each device wants its own regular receive, +so the timer runs on *both* velox and ratio (the shared =common= dotfiles +package, same home as roam-sync). + +- Script: =scripts/signal-receive.sh= (rulesets, so both machines get it on + =git pull=). It no-ops cleanly on a machine that lacks the account. +- Units: =scripts/signal-receive.service= + =.timer= (reference copies under + =scripts/systemd/=; the stowed copies live in =common/.config/systemd/user/= + of the dotfiles repo, so both machines get them). +- Cadence: every 15 minutes (=OnUnitActiveSec=15min=), matching roam-sync. + +Enable on each machine (one-time, per daily-drivers.md's one-time-setup class): + +#+begin_src bash +# After the dotfiles + rulesets pull, on each daily driver: +systemctl --user daemon-reload +systemctl --user enable --now signal-receive.timer +systemctl --user status signal-receive.service # confirm a clean receive +#+end_src + +* signal-cli setup notes + +- *Version:* signal-cli 0.14.5 on velox (2026-07-20). +- *Accounts:* velox's signal-cli holds the pager identity =+15045173983= as the + registered primary (account file 465310). ratio's signal-cli holds two + accounts: Craig's personal number =+15103169357= (its own primary, + note-to-self only — no phone push) and the pager identity as a *linked device* + (Device 2, "ratio-pager", linked 2026-07-20). Both accounts coexist; target + the pager with =-a +15045173983=. A future daily driver joins the same way. +- *signal-mcp:* on velox, Claude sessions may also expose a =signal-mcp= tool + (=send_message_to_user=, same pager identity) configured in velox's global + =~/.claude.json=. It works there but is invisible from any other machine and + from non-Claude runtimes, so =agent-text= is the portable habit. The old + =page-signal= shell script was removed 2026-06-12 — do not resurrect it. +- *Linking a device:* to add a second signal-cli as a linked device of the pager + account (see the open decision below), provision it from the new machine and + approve the link from the account holder: + + #+begin_src bash + # On the new machine — prints a tsdevice:/ URI (render as QR to approve): + signal-cli link -n "ratio-pager" + # Approve from velox (the primary device): + signal-cli -a +15045173983 addDevice --uri "tsdevice:/?uuid=…" + #+end_src + +* Decision — linked device (2026-07-20) + +The topology question — ssh-relay only vs. registering daily drivers as linked +devices — was decided in favor of linked devices. ssh-relay only was simpler +(one identity, one receive point) but had a single point of failure: a page +failed when velox was down or off the tailnet. + +Registering ratio as a linked device removes that: ratio sends directly, so a +page lands even when velox is down. The costs, both paid: linked-device +provisioning per machine (the =link= / =addDevice= handshake above), and each +device wanting its own regular =receive= — so the warm-keeping timer moved from a +velox-only home to the shared =common= package, running on both. + +Adding another daily driver later is the same handshake plus a dotfiles stow; +the timer and =agent-text= already generalize to "any machine holding the +account." + +* History + +- 2026-07-04 — home retired ntfy (self-hosted on ratio) and tore it down, + switching agent paging to Signal. Handoff to rulesets to document and own. +- 2026-07-13 — reconciled to one pager identity on velox; =agent-page= shipped + (direct on velox, ssh-relay elsewhere, desktop fallback); protocols.org "Paging + Craig" rewritten around the two channels. +- 2026-07-20 — this runbook; receive-timer script + units added; a manual drain + cleared the 47-day staleness live; ratio linked as a device of the pager + account and its direct send verified; the tool (still named =agent-page= that + morning) generalized to send directly from any machine holding the account; + receive timer moved to the shared =common= package and enabled on both machines. +- 2026-07-20 (later) — notification vocabulary split: "page me" is the desktop + channel, "text me" is Signal, "text and page me" is both. The tool was renamed + =agent-page= → =agent-text= to match, with a deprecated =agent-page= shim + delegating to it. protocols.org section renamed "Paging Craig" → "Reaching + Craig". diff --git a/docs/design/task-review.org b/docs/design/task-review.org index 6c6dac7..c9ae023 100644 --- a/docs/design/task-review.org +++ b/docs/design/task-review.org @@ -1,5 +1,5 @@ #+TITLE: Design: Daily Task-Review Habit -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-05-16 #+OPTIONS: toc:nil num:nil diff --git a/docs/specs/2026-06-16-autonomous-batch-execution-spec.org b/docs/specs/2026-06-16-autonomous-batch-execution-spec.org index 23fc574..a42adc3 100644 --- a/docs/specs/2026-06-16-autonomous-batch-execution-spec.org +++ b/docs/specs/2026-06-16-autonomous-batch-execution-spec.org @@ -1,5 +1,5 @@ #+TITLE: Autonomous-Batch Task Execution — Spec -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-16 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED @@ -21,7 +21,7 @@ |----------+--------------------------------------------------------------------| | Date | 2026-06-16 | |----------+--------------------------------------------------------------------| -| Related | [[file:../../working/inbox-zero-phase-e/proposed-inbox-zero.org][Phase E proposal]]; [[file:../design/2026-06-15-fix-speedrun-workflow-proposal.org][speedrun proposal]] | +| Related | [[file:../design/2026-06-16-inbox-zero-phase-e-proposal.org][Phase E proposal]]; [[file:../design/2026-06-15-fix-speedrun-workflow-proposal.org][speedrun proposal]] | |----------+--------------------------------------------------------------------| * Summary @@ -375,16 +375,16 @@ Verification is by invocation against a project's real =todo.org=: run the loop * References / Appendix -- [[file:../../working/inbox-zero-phase-e/proposed-inbox-zero.org][Phase E proposal (inbox-zero stopgap)]] and [[file:../../working/inbox-zero-phase-e/sender-note.org][its sender note with the 5 open questions]]. +- [[file:../design/2026-06-16-inbox-zero-phase-e-proposal.org][Phase E proposal (inbox-zero stopgap)]] and [[file:../design/2026-06-16-inbox-zero-phase-e-sender-note.org][its sender note with the 5 open questions]]. - [[file:../design/2026-06-15-fix-speedrun-workflow-proposal.org][speedrun proposal]] (file retains its original on-disk name pending a rename pass). -- [[file:../../.ai/workflows/inbox-zero.org][inbox-zero.org (canonical, A-D)]] — the routing workflow this feature decouples from. +- [[file:../../.ai/workflows/inbox.org][inbox.org (canonical A-D; was inbox-zero.org)]] — the routing workflow this feature decouples from. - =~/code/rulesets/claude-rules/knowledge-base.md= — the org-roam write contract the synthesis step follows. * Review and iteration history ** 2026-06-16 Tue — author - What: initial draft reconciling the Phase E and fix-speedrun proposals into one work-the-backlog.org feature, plus the effectiveness-measurement instrumentation. - Why: two overlapping proposals arrived within a day; building them separately would duplicate the execution loop and let it drift. Craig also asked explicitly for measurement + org-roam synthesis. -- Artifacts: this spec; the two source proposals under docs/design/ and working/inbox-zero-phase-e/. +- Artifacts: this spec; the two source proposals under docs/design/ (the Phase E proposal, diff, and sender note now filed there). ** 2026-06-28 Sun — revision (Craig) - What: removed the task-size gate (size no longer defers; large tasks decompose into per-commit chunks); recast the act-vs-file rule as a crisp four-item defer checklist keyed on test-writability; added crisp =:solo:= / =:quick:= definitions destined for =todo-format.md= and made their assessment mandatory in task-review + task-audit; added the speedrun's pre-flight decision-gathering step (batch the quick questions up front, "skip this" drops a task, then run hands-off); renamed "fix speedrun" → "no-approvals speedrun" in prose. Status stays draft pending ratification of the revised decisions. - Why: the original criteria were adjectives, not checkable; the size gate forced Craig to stay at his desk for anything non-trivial, defeating the away-from-desk use case; and decision-needing tasks were over-deferred when many need only a quick upfront answer. diff --git a/docs/specs/2026-06-16-encourage-kb-contribution-spec.org b/docs/specs/2026-06-16-encourage-kb-contribution-spec.org index cfbfe79..3d08ed5 100644 --- a/docs/specs/2026-06-16-encourage-kb-contribution-spec.org +++ b/docs/specs/2026-06-16-encourage-kb-contribution-spec.org @@ -1,5 +1,5 @@ #+TITLE: Encourage Org-Roam KB Contribution Across Workflows — Spec -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-16 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED diff --git a/docs/specs/2026-07-01-docs-lifecycle-spec.org b/docs/specs/2026-07-01-docs-lifecycle-spec.org index dcf88c8..91c1603 100644 --- a/docs/specs/2026-07-01-docs-lifecycle-spec.org +++ b/docs/specs/2026-07-01-docs-lifecycle-spec.org @@ -1,13 +1,14 @@ #+TITLE: Docs Lifecycle — Spec -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-07-01 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED -* DOING Docs lifecycle +* IMPLEMENTED Docs lifecycle :PROPERTIES: :ID: 80b0787b-4a60-4c82-8a16-b383d3e3c8f2 :END: +- 2026-07-04 Sat @ 11:46:31 -0500 — DOING → IMPLEMENTED: all four build phases shipped (docs-lifecycle rule + spec-workflow updates, spec-sort helper + 30-test bats suite, rulesets pilot sorting the pile and standing up the status board, startup nudge gated on :LAST_SPEC_SORT:) plus the follow-up file:→id: link conversion. Remaining manual validation (startup nudge fires/clears, moved-spec links click through in Emacs) tracked as its own task; a failed check promotes to a bug. - 2026-07-01 Wed @ 23:34:15 -0400 — READY → DOING: spec-response decomposition ran — build parent in todo.org carries the :SPEC_ID: binding, one task per phase plus the flip-to-IMPLEMENTED task and the manual-testing child. First live exercise of the transition-ownership table. - 2026-07-01 Wed @ 23:22:50 -0400 — DRAFT → READY: Codex re-review found all fourteen review findings closed and no remaining blocking implementation-readiness gaps. - 2026-07-01 Wed @ 22:54:41 -0400 — verify pass on the second responder round: all five fixes held, findings 1-9 unregressed, verdict ready; three minor nits folded in (scoped id-link criterion, untracked-copy cleanup in the recovery recipe, two stale prose spots). Stays DRAFT pending the reviewers' flip. @@ -17,7 +18,7 @@ - 2026-07-01 Wed @ 22:13:00 -0400 — drafted from the five decisions settled 2026-06-28 (todo.org "Spec storage location + lifecycle-status convention"). * Metadata -| Status | doing | +| Status | implemented | |----------+------------------------------------------------------------------| | Owner | Craig Jennings | |----------+------------------------------------------------------------------| diff --git a/docs/specs/2026-07-14-sentry-workflow-spec.org b/docs/specs/2026-07-14-sentry-workflow-spec.org new file mode 100644 index 0000000..307d0be --- /dev/null +++ b/docs/specs/2026-07-14-sentry-workflow-spec.org @@ -0,0 +1,278 @@ +#+TITLE: Sentry Workflow — Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-14 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED sentry workflow +:PROPERTIES: +:ID: f6c51f27-d7a2-4b63-9ff9-5ba005a66dfb +:END: +- 2026-07-19 Sun @ 05:04:00 -0500 — IMPLEMENTED: all four phases built, committed, and pushed (agent-lock a8b6cf4, engine ccc9c26, companions c6383e9). Full suite green throughout. The overnight live trial is handed to Craig as a structured manual-testing task; its findings file as follow-up tasks, not a build gate. +- 2026-07-19 Sun @ 04:35:57 -0500 — DOING: build started. Decomposed into the four implementation phases as todo.org build tasks under the sentry parent (SPEC_ID-bound); running in no-approvals + auto-flush mode. +- 2026-07-19 Sun @ 04:35:57 -0500 — READY: Craig completed his deep read and approved the spec for build. Gate passed. +- 2026-07-14 Tue @ 02:03:28 -0500 — all 12 review findings dispositioned live with Craig and folded into the design; decisions now 10/10; still DRAFT pending Craig's deep read. +- 2026-07-14 Tue @ 00:52:03 -0500 — drafted, all nine design decisions resolved live with Craig during the authoring session. + +* Metadata +| Status | implemented | +|----------+------------------------------------------| +| Owner | Craig Jennings | +|----------+------------------------------------------| +| Reviewer | (spec-review, next session) | +|----------+------------------------------------------| +| Related | [[file:../../todo.org][todo.org — sentry build task]] | +|----------+------------------------------------------| + +* Summary + +Sentry is one supervisor workflow that runs a project's hygiene passes serially on a cadence, holds locks so it never collides with itself or other agents, commits its work to a host-suffixed side branch for morning review, and checkpoints between passes so a crash loses at most one pass. It replaces the idea of firing inbox-zero and triage-intake as separate uncoordinated crons. + +* Problem / Context + +An agent session left running overnight can keep a project clean, but the pieces don't coordinate. There is no lock anywhere in .ai/scripts/ (verified 2026-07-14): capture-guard covers only the Emacs org-capture case on one file, and it's advisory. Multiple sessions (several projects, sometimes both daily drivers) can read-modify-write ~/org/roam/inbox.org and the agents/ KB nodes at once. The only coordination is git optimism, and we know it loses: the KB search already globs out *sync-conflict* files because pull/push races fork them. + +Separate crons compound it: inbox-zero and triage-intake fired independently can overlap each other and any interactive session on the same shared files. + +Proposal origin: the work project, 2026-07-13 (preserved at [[file:../design/2026-07-14-sentry-workflow-proposal.org][docs/design/2026-07-14-sentry-workflow-proposal.org]]). This spec supersedes the proposal where they differ: the skeptical review found four conflicts with existing discipline, each resolved as a Decision below. + +* Goals and Non-Goals + +** Goals +- One serialized runner per project: passes never overlap each other, and a fire that finds a prior fire still running skips. +- A shared-file lock that actually serializes same-host agents writing public roam files. +- Crash-lossless to one-pass granularity: session-context entry plus a commit after every disk-writing pass. +- Main stays clean: overnight commits land on a host-suffixed sentry branch so cross-machine pulls and the template sync keep working. +- Nothing destructive fires unattended: trash/mark-read, regrades, and file moves queue for morning approval. +- Portable across projects with zero configuration: passes detect their targets and skip where not applicable. + +** Non-Goals +- No report-only or degraded pass mode. A pass runs fully or it doesn't run (Craig's explicit direction). The morning-approval queue is a permanent division of labor, not a degraded mode. +- No cross-host locking. The lock is host-local; cross-host races stay with roam-sync's abort-loudly rebase. +- No unattended /schedule contract. Sentry runs inside a live session Craig starts, driven by /loop, the same boundary inbox.org draws for auto inbox zero. +- Sentry never runs git against the roam repo. roam-sync remains that repo's only committer. + +** Scope tiers +- v1: the sentry.org engine (ten mechanical passes, entry gates, branch mechanics, digest, the stop-sentry operation), the agent-lock helper script with bats tests, the roam-write lock adopted by every roam write path, companion-file reconciliations (knowledge-base.md write recipe, inbox.org core §5, roam-sync.sh header, triage-intake.org note, wrap-it-up.org active-sentry guard), INDEX.org entry. +- Out of scope: changes to roam-sync.sh behavior; changes to the wrap-up teardown feature. +- vNext (logged to todo.org): cross-host roam conflict surfacing; the KB lesson-promotion pass (blocked on the lesson-detection heuristic task); a fully-unattended /schedule variant if the interactive shape proves too narrow. + +* Design + +** For Craig (the user altitude) + +You type the trigger in a project session ("run sentry", or the /loop line it expands to). Sentry first checks its entry ticket: the project's notes.org must carry :COMMIT_AUTONOMY: yes, or sentry declines to start and names the marker. Then the entry gates run while you're still at the terminal: a dirty tracked tree stops and describes what's dirty, file by file, and offers numbered options (finish the job, stash it, or name changes to roll back); a red test suite stops the same way with an offer to investigate. The loop starts only once both are answered. + +Launching is a handoff — the launch contract: sentry owns the repo until the morning merge. Working in that repo mid-night means saying "stop sentry" first (it cancels the loop and walks the branch and queue disposition), and open Emacs buffers on project files want a revert after launch and again after the merge. "Wrap it up" while sentry is active refuses and points at "stop sentry". + +Each fire (hourly by default) walks the pass list serially and appends one timestamped digest to the session log: failures as banners on top, then one line per pass, then the morning-approval queue. A pass with nothing to do is one line. Anything destructive or judgment-shaped waits in that queue for you. + +In the morning you review the sentry/<date>-<host> branch (a stack of small unpushed chore(sentry) commits), squash-merge it into your working branch, delete it, and answer the approval queue. If a night's branch is still unmerged when sentry next starts, that fire skips and says so. + +** For the implementer + +*The engine.* A synced template workflow, .ai/workflows/sentry.org, driven by /loop <interval> (default hourly). Each fire acquires the single-runner lock, verifies branch state, walks the pass list, writes the digest, commits the spine residue, and releases the lock. The runner re-touches the lock between passes (the heartbeat), so a live run's lock is never older than one pass. Every pass follows one contract: probe, work, session-context entry, then a commit when the pass wrote to disk. The probe asks whether the pass's target exists in this project; when it doesn't, the pass is one skip line and nothing more. The session-context entry precedes the commit so a crash between them still leaves the trail. + +*Branch mechanics.* At loop start (after the entry gates and a fetch-and-ff-only reconcile of the project branch — a diverged branch joins the interactive gate), sentry creates sentry/YYYY-MM-DD-<host> from HEAD and checks it out; the host suffix prevents a same-date collision between the daily drivers. Commits are conventional, one per writing pass: chore(sentry): <pass> — <what changed>. Nothing pushes. At entry, an existing unmerged sentry/* branch means skip-and-note. No second branch stacks on the first. Morning teardown (review, squash-merge, delete) is Craig's, documented in the workflow, never automated. + +*Locks.* One helper script, .ai/scripts/agent-lock, serves both locks. flock is unusable here: every Bash call an agent makes is its own short-lived shell, so a flock dies with the call that acquired it. Instead: atomic mkdir as acquire (create-and-win or fail-and-lose), a metadata file inside recording PID, hostname, and ISO timestamp, and age-based staleness reclaim so a crashed run's lock expires instead of wedging every later fire. Subcommands: acquire <name> [--ttl], release <name>, status <name>. Lock homes: /run/user/<uid>/agent-locks/<name>/ for both locks — the helper owns the path scheme, callers pass names — with ~/.cache/agent-locks/ as the fallback where no runtime dir exists. tmpfs makes a lock host-local by construction, keeps it out of every repo (a lock inside ~/org/roam would ride roam-sync's git add -A to the other machine as a phantom hold), and clears it on reboot. Contention is a bounded wait (~30s), then defer-and-note. With the heartbeat keeping a live lock young, TTLs size to the longest single pass; every reclaim surfaces in the digest. + +*Roam writes.* A pass touching a public roam file acquires the roam-write lock, runs capture-guard --wait (the human-capture layer stays underneath), edits the working tree, triggers roam-sync with systemctl --user start roam-sync.service, and releases the lock. Sentry never runs a git write against ~/org/roam, and pass 1's ff-only pull is its only git read of that repo. The 06-24 one-git-owner rule holds. The lock spans only edit-plus-trigger; roam-sync itself is serialized by systemd (a oneshot unit never runs concurrently with itself). + +*Unattended safety.* With no one at the terminal, any unsafe state (unexpected dirty tree, red suite, lost lock, unmerged prior branch) makes the affected scope (the pass, or the whole fire, whichever the state poisons) skip with one digest line. The next fire retries; if the state persists to morning, the interactive entry gate handles it with Craig present. Skips are never silent and never partial: no pass runs in a reduced form. Two refinements keep the machinery honest: the dirty check excludes the spine set (session-context files, path resolved via session-context-path) so sentry's own bookkeeping can't trip it, and after the second consecutive fire skipped on an unmerged prior branch, sentry sends one persistent desktop notification naming the project and branch, then repeats at most daily — a multi-day stall never stays silent. + +*Pass list (v1), in order, each with its detection probe:* + +1. Roam pull — git -C ~/org/roam pull --ff-only, skipped when the tree is dirty (roam-sync owns that case) or the clone is absent. Read-only; the one narrow exception to "don't touch roam git," pull-only and ff-only, so later reads are fresh. +2. Inbox zero — inbox.org roam mode, run under the engine's no-approvals contract (quick+solo+agreed items execute; shared-asset proposals park; everything lands in the morning queue). Probe: roam clone or project inbox/ exists. +3. Triage intake — triage-intake.org. Probe: triage plugins present. Destructive actions queue. +4. Todo cleanup — clean-todo.org mechanics. Probe: root todo.org. +5. Task audit — task-audit.org. Probe: root todo.org. Regrades queue. +6. Working-files hygiene — flag working/<slug>/ dirs whose task is closed. Probe: working/ exists. Filing queues. +7. Spec status board — the docs-lifecycle grep. Probe: docs/specs/ exists. +8. Link integrity — broken file: links in the project's org files. Probe: lint-org.el present. +9. Git health — drift, unpushed commits, stale branches, main-behind-origin. Probe: .git. +10. Prep + symlink freshness. Probe: the prep dir / symlinks exist (work and home only, in practice). +(KB lesson promotion, the proposal's eleventh pass, is deferred to vNext — see the KB finding and the filed lesson-detection-heuristic task. v1 ships the ten mechanical passes above.) + +*Digest.* Appended per fire to the session-context.org Session Log (which the spine already writes), so it survives a crash, rides the session archive, and is on screen in the running session. The morning-approval queue accumulates under one heading in the same file — each item carries what, why, and the exact command or edit that fires on approval. + +* Alternatives Considered + +** Separate crons per task (status quo direction) +- Good, because each piece stays independently simple. +- Bad, because nothing serializes them: the observed sync-conflict forks are this cost, already paid. +- Bad, because N crons means N session lifecycles to manage instead of one. + +** flock for both locks (as proposed) +- Good, because it's the standard tool and kernel-enforced. +- Bad, because it binds to a living process; agent tool calls are short-lived shells and /loop turns share none, so the lock evaporates on return. Disqualifying. + +** Per-pass commits on the current branch (as proposed) +- Good, because commits sit where the edits apply; no morning merge step. +- Bad, because unpushed commits on main on two daily drivers diverge main by morning, the exact state startup's fast-forward refuses and the template-sync guard blocks on. Disqualifying with two machines. + +** Degraded report-only mode for unsafe states +- Good, because some findings still land when committing is unsafe. +- Bad, because it blurs the contract: a pass that half-ran reads as having run. Craig rejected it outright; skip-and-note is the replacement. + +** Per-project pass manifest instead of detection +- Good, because explicit control over what runs where. +- Bad, because it's one more thing to maintain and forget; detection activates a pass the day its target appears. Rejected for v1. + +* Decisions [10/10] + +** DONE Commit target: host-suffixed sentry branch +- Context: overnight commits must survive two daily drivers that both pull main; unpushed commits on main diverge it across machines, breaking startup fast-forward and the template-sync guard. +- Decision: We will commit each writing pass to sentry/YYYY-MM-DD-<host>, created from HEAD at loop start, never pushed. Morning flow: review, squash-merge, delete. An unmerged prior sentry branch at entry skips the fire. +- Consequences: easier — main's ref stays clean for cross-machine sync; a bad night is discarded by deleting one branch. Harder — a daily merge step; the working tree sits on a non-main branch overnight. + +** DONE Roam-write lock scope: host-local only +- Context: observed conflicts are same-host multi-agent; cross-host races go through roam-sync's rebase, which aborts loudly. +- Decision: We will ship the host-local lock and file cross-host conflict-surfacing as a separate task. +- Consequences: easier — small, testable v1. Harder — a true cross-host race still forks; we accept the rarity. + +** DONE Interval default: hourly, with a knob +- Context: passes short-circuit in seconds when idle; the real cost is digest noise, not compute. +- Decision: We will default to hourly and expose the interval as the /loop argument. +- Consequences: easier — projects stay clean through the night. Harder — a capture-heavy evening produces several small commits; the knob is the escape hatch. + +** DONE Entry gates are interactive, not silent +- Context: Craig types the sentry command in, so the first fire runs with him at the terminal. +- Decision: We will stop at entry on a dirty tracked tree (describe what's dirty; offer finish-the-job / stash / rollback-named-changes) and on a red suite (describe failures; offer to investigate). The loop starts only after both are answered. Untracked files never block. +- Consequences: easier — no guessing about his in-progress work. Harder — sentry can't start unattended from a dirty state; that's the point. + +** DONE No report-only mode; unsafe unattended states skip +- Context: a degraded pass mode blurs whether a pass ran; Craig rejected it explicitly. +- Decision: We will make every pass run fully or not at all. Unattended unsafe states (dirty tree, red suite, lost lock, unmerged prior branch) skip the poisoned scope with one digest line; the next fire retries. The morning-approval queue for destructive/judgment actions stays — it's a permanent contract, not degradation. +- Consequences: easier — a pass line in the digest means it fully ran. Harder — a persistent unsafe state means zero hygiene until morning; accepted. + +** DONE Roam git discipline: roam-sync stays the only committer +- Context: the proposal had sentry commit-and-push roam under the lock; the 06-24 fix made roam-sync the repo's single git owner because the tree is chronically dirty from live captures. +- Decision: We will wrap only edit-plus-trigger in the roam-write lock; sentry never runs git against ~/org/roam (pass 1's ff-only pull is the sole, read-only exception). +- Consequences: easier — one git owner, no mid-rebase states from agents. Harder — an edit lands remotely only when roam-sync fires; the manual trigger closes most of that gap. + +** DONE Lock mechanics: mkdir-atomic helper with staleness reclaim +- Context: flock can't span tool calls (short-lived shells, no shared process across /loop turns). +- Decision: We will build .ai/scripts/agent-lock — atomic mkdir acquire, PID/host/timestamp metadata, age-based reclaim, acquire/release/status subcommands, bats-tested — and use it for both locks. +- Consequences: easier — locks survive between calls and self-clear after crashes. Harder — TTL tuning; a reclaim during a genuinely slow pass is possible, so the helper surfaces every reclaim rather than reclaiming silently. + +** DONE Autonomy gate: :COMMIT_AUTONOMY: yes is the entry ticket +- Context: commits.md gates commits on approval; sentry commits unattended, so it needs standing, per-project authorization. +- Decision: We will have sentry decline to start in any project whose notes.org lacks :COMMIT_AUTONOMY: yes, naming the marker. +- Consequences: easier — running sentry somewhere is a deliberate grant; no half-running mode to reason about. Harder — read-only passes don't run in ungranted projects either; that's acceptable, since launch is one marker away. + +** DONE Pass portability: detection over configuration +- Context: several proposed passes are work/home-specific; run verbatim elsewhere they're vacuous or error. +- Decision: We will open every pass with a cheap existence probe and skip with one digest line when the target is absent. +- Consequences: easier — zero config, passes self-activate when targets appear. Harder — an intentionally-unwanted pass needs a vNext exclusion marker if that ever becomes real. + +** DONE Suite policy: entry run plus conditional fire-end run, no per-pass runs +- Context: the verification discipline requires a full suite run before every commit, but sentry commits per pass, hourly, mostly touching org files the suite doesn't exercise; a per-commit run would turn a seconds-long fire into minutes, all night. +- Decision: We will run the suite once at entry (the green baseline the gates require) and again at fire-end only when a pass modified files outside the org/spine set. No per-pass runs. The deviation is justified by the unpushed branch and the morning review gating everything before push. +- Consequences: easier — idle fires stay cheap and the rare code-touching pass is still caught before its commits age. Harder — a suite break introduced by an org-only edit (possible via fixtures) surfaces at morning review rather than at the offending commit. + +* Review findings [12/12] + +** DONE Overnight working-tree ownership is undefined +Resolved 2026-07-14 (Craig): launch contract with in-place checkout. Launching sentry hands the repo to sentry until the morning merge; reclaiming it mid-night means stopping the loop first. The entry gate (clean tree, Craig present) fronts the handoff, the unattended dirty-skip backstops anything that slips, and the workflow documents the Emacs buffer-revert caveat at launch and after the morning merge. Worktree isolation was rejected because untracked inbox drops exist only in the main tree, which blinds the inbox pass; plumbing commits were rejected as fragile. + +** DONE Roam-write lock guards nothing unless every roam writer acquires it +Resolved 2026-07-14 (Craig): Phase 3 strengthened from notes to mandatory write-path changes. inbox.org core §5 gains acquire/release around the Phase D edit; knowledge-base.md's write recipe gains the same around its write block. Both degrade gracefully when the helper isn't installed (proceed unlocked — today's behavior); an interactive caller finding the lock busy does a bounded wait (~30s) then surfaces to the user rather than proceeding unlocked. + +** DONE Roam lock location gets committed by roam-sync +Resolved 2026-07-14 (Craig): both locks live under /run/user/<uid>/agent-locks/<name>/, with ~/.cache/agent-locks/ as the fallback where no runtime dir exists. The agent-lock helper owns the path scheme; callers pass names, never paths. tmpfs gives host-locality by construction and reboot self-clearing for free. + +** DONE Sentry's own spine writes trip its dirty-tree skip +Resolved 2026-07-14 (Craig), all three parts: the unsafe-state dirty check excludes the spine set (session-context.org / session-context.d/), each fire ends with one digest commit that sweeps accumulated spine writes so read-only fires still leave a clean tree, and the spine path resolves through .ai/scripts/session-context-path so a concurrent agent's anchor is never clobbered. + +** DONE Pass 8 depends on a known data-corrupting bug +Resolved 2026-07-14, mid-review: the lint-org fix landed in 951b6fc — block-type-aware scanning in both helpers, CLI report-only by default with writes behind --fix, and the true corruption path (wrap-org-table's load-time dispatch firing on lint-org's require) guarded to entry-script-only. Pass 8 runs against the fixed linter; the prerequisite is satisfied and no Implementation-phases dependency is needed. + +** DONE Unattended inbox-pass semantics are undefined +Resolved 2026-07-14 (Craig): sentry runs inbox processing under inbox.org's no-approvals contract verbatim — quick+solo+agreed items execute, shared-asset and convention proposals park (prepared diff, VERIFY task, sender reply). Everything the pass did lands in the morning queue, and anything the engine would ask interactively defers to the queue instead of blocking the fire. + +** DONE Per-pass commits skip the pre-commit suite run — undecided convention conflict +Resolved 2026-07-14 (Craig): recorded as a Decision (see Decisions — suite policy). Entry run establishes the green baseline; no per-pass runs; a fire-end suite run fires only when a pass modified files outside the org/spine set, so the rare code-touching pass is caught before its commits age overnight. Justification recorded with both halves of the consequences. + +** DONE KB promotion pass has no defined lesson source +Resolved 2026-07-14 (Craig): pass 11 is cut from v1 and deferred to vNext. KB promotion stays a wrap-up concern — the editorial moment where Craig answers the promotion prompt. An unattended judgment pass writing to the shared KB waits until sentry has quiet weeks behind it AND a designed detection heuristic; the heuristic design is filed in todo.org ([#D] "KB lesson-detection heuristic", which blocks re-adding the pass). + +** DONE Lock contention and reclaim mechanics are half-specified +Resolved 2026-07-14 (Craig): both mechanics adopted. Contention: bounded wait (~30s, capture-guard's --wait shape), then defer — the pass skips with a digest line and the next fire retries. Reclaim: heartbeat refresh — the runner re-touches the lock timestamp between passes, so a live run's lock is never older than one pass and the TTL sizes to the longest single pass (minutes). Every reclaim surfaces in the digest. + +** DONE Wrap-up while the loop is live is undefined +Resolved 2026-07-14 (Craig): wrap-up refuses while sentry is active. It detects the live single-runner lock and stops with "sentry is active — say 'stop sentry' first." The stop-sentry operation is defined in sentry.org and owns the shutdown: cancel the loop, walk the branch disposition (squash-merge now or leave named), walk-or-carry the approval queue. Wrap-up itself gains only the one guard; the shutdown logic lives with sentry. + +** DONE Entry should reconcile the project branch before branching +Resolved 2026-07-14 (Craig): adopted. After the entry gates pass, sentry runs the same fetch-and-ff-only reconcile startup uses, then creates the branch. A diverged branch joins the interactive entry gate (Craig is present at launch) rather than being auto-resolved. + +** DONE Multi-day unmerged branch stalls hygiene silently +Resolved 2026-07-14 (Craig): adopted. After the second consecutive fire skipped for the unmerged-branch reason, sentry sends one persistent desktop notification naming the project and branch ("sentry stalled: <branch> unmerged — merge or delete to resume"), then repeats at most daily. Persistent notify matches the paging convention: it stays on screen until dismissed. + +* Implementation phases + +** Phase 1 — agent-lock helper +.ai/scripts/agent-lock (canonical: claude-templates/.ai/scripts/) with bats tests: acquire/release/status, contention, staleness reclaim, metadata. Tree stays working; nothing calls it yet. + +** Phase 2 — sentry.org engine +The workflow file: entry ticket, interactive gates, branch mechanics, pass runner with the probe→work→log→commit contract, digest + approval queue, skip semantics. INDEX.org entry. Mirror synced. + +** Phase 3 — companion reconciliations +knowledge-base.md write recipe (acquire/release the roam-write lock around the write block; edit-plus-trigger replaces inline pull/commit/push); inbox.org core §5 (acquire/release around the Phase D edit, capture-guard staying as the human layer underneath; graceful degradation when the helper is absent); roam-sync.sh header comment; triage-intake.org note (runs as a sentry pass; own triggers kept); wrap-it-up.org active-sentry guard (refuse and point at "stop sentry"). + +** Phase 4 — verification +make test green; a live trial night on rulesets (ratio): entry gates exercised, one fire observed end to end, morning branch review performed; follow-up tasks filed from what the trial surfaces. + +* Acceptance criteria +- [ ] agent-lock: two concurrent acquires produce exactly one winner; release frees; a stale lock (aged past TTL) is reclaimed with a surfaced note; a busy lock is a bounded wait then defer; heartbeat refresh keeps a live lock young; locks live under the runtime dir (cache fallback), never inside a repo; bats suite green. +- [ ] Sentry declines to start without :COMMIT_AUTONOMY: yes, naming the marker. +- [ ] Entry on a dirty tracked tree stops with the file-by-file description and the three options; untracked files don't trigger it. +- [ ] A fire on a clean project produces one digest with every pass either run or skipped-with-reason; no other output shape exists. +- [ ] Writing passes commit individually to sentry/<date>-<host>; main's ref is untouched; nothing is pushed. +- [ ] A roam-writing pass acquires the roam-write lock, passes capture-guard, edits, triggers roam-sync, releases, and never runs git write commands against ~/org/roam. +- [ ] An unmerged prior sentry branch at entry causes a skip-and-note, not a second branch. +- [ ] Destructive/judgment actions appear only in the approval queue, never executed unattended. +- [ ] On a project missing a pass target (no todo.org, no triage plugins), the pass skips with one line and the rest run. +- [ ] Entry reconciles the project branch ff-only before creating the sentry branch; a diverged branch stops at the interactive gate. +- [ ] Sentry's own spine writes never trip the dirty-tree skip (a full fire of read-only passes ends with a clean tree via the digest commit). +- [ ] "Wrap it up" during an active loop refuses and names "stop sentry"; "stop sentry" cancels the loop and walks the branch and queue disposition. +- [ ] The second consecutive unmerged-branch skip produces one persistent desktop notification; repeats are at most daily. +- [ ] The inbox pass parks a shared-asset proposal (VERIFY + prepared diff + sender reply) rather than applying it, and the morning queue lists everything the pass did. + +* Readiness dimensions +- Data model & ownership: locks own their directories under /run/user/<uid>/agent-locks/ (tmpfs, outside every repo); digest and queue live in session-context.org (session-owned, path via session-context-path); commits on the sentry branch are Craig-authored per commits.md. +- Errors, empty states & failure: every skip names its reason in the digest; a failed pass banners and the rest continue; the digest surfaces lock-reclaim events. No silent outcomes. +- Security & privacy: no credentials touched; KB promotion honors the knowledge-base.md work-project refusal contract; commit messages follow the no-tooling-enumeration rule where applicable. +- Observability: the digest is the observability surface (per-fire, timestamped, failures on top); session-context.org persists it across crashes. +- Performance & scale: idle fire is seconds (probes + short-circuits); heaviest pass is triage (network); hourly cadence assumed fine; the knob covers it. +- Reuse & lost opportunities: reuses inbox.org, triage-intake.org, clean-todo, task-audit, lint-org.el, capture-guard, roam-sync wholesale; the only new code is agent-lock and the engine file. +- Architecture fit & weak points: engine+passes mirrors the engine+plugin convention; weak points are lock TTL tuning (reclaim vs slow pass) and /loop session lifetime (a killed session ends sentry; the lock's TTL clears the residue). +- Config surface: /loop interval; :COMMIT_AUTONOMY: marker; lock TTLs as script flags with defaults. Nothing else. +- Documentation plan: the workflow file is the doc (When to Use + the morning-review section); INDEX.org entry; no README change needed. +- Dev tooling: bats for agent-lock via the existing glob-discovered suites; make test covers it with no Makefile edit. +- Rollout, compatibility & rollback: additive, new files plus comment-level reconciliations; no consuming project changes behavior until Craig launches sentry there. Rollback is deleting the branch and not launching. +- External APIs & deps: none beyond tools already in use (git, systemctl, bats). N/A for schema verification. + +* Risks, Rabbit Holes, and Drawbacks +- The heartbeat bounds the TTL question to a single pass, but a pass that legitimately outruns its TTL (a slow triage sweep on a bad network) can still be reclaimed under itself. Start generous, surface every reclaim, tune from the digest. +- /loop lifetime: sentry dies with its session (harness restart, machine sleep). Acceptable for v1: the morning state is a readable branch plus session-context, and nothing corrupts. +- Digest fatigue: hourly fires on an active evening could stack noise. The interval knob and the one-line-when-idle rule are the mitigations; revisit after a week of use. +- The sentry branch holds todo.org/notes.org edits overnight; Craig editing those files on main before the morning merge makes the squash-merge conflict. That conflict is small, self-inflicted, and resolvable by hand; the morning-review section documents it. + +* Review and iteration history + +** 2026-07-14 Tue @ 02:03:28 -0500 — Claude Code (rulesets) — responder +- What: all 12 findings walked with Craig one by one and dispositioned; each completed in place and folded into the body. Load-bearing calls: launch contract with in-place checkout (worktrees rejected — untracked inbox drops are invisible there); every roam writer adopts the lock; locks relocate to the XDG runtime dir; spine-set exclusion + fire-end digest commit; inbox pass runs the no-approvals contract; suite policy recorded as Decision 10; KB promotion pass cut to vNext (heuristic task filed); heartbeat + bounded-wait lock mechanics; wrap-up refuses during a live loop ("stop sentry" owns shutdown); entry ff-only reconcile; stall notify after two skips. Mid-walk, Craig redirected the pass-8 finding into fixing the lint-org bug immediately — landed as 951b6fc, which also found and closed the true 2026-07-09 corruption path (wrap-org-table's load-time dispatch firing on lint-org's require). +- Why: Craig chose to resolve every open question together rather than batch them into a written response round. +- Artifacts: findings section above (12/12 done); todo.org tasks (KB heuristic [#D], bug task closed); commit 951b6fc. + +** 2026-07-14 Tue @ 01:15:00 -0500 — Claude Code (rulesets) — reviewer +- What: adversarial weakness review at Craig's direction — batches of candidate weaknesses checked against the spec, looped until a round produced fewer than five substantive new ones. Round 1: 20 candidates, 11 answered or mitigated by the spec, 9 became findings. Round 2: 7 candidates, 3 answered, 4 folded into findings. Round 3 produced fewer than five substantive candidates; loop terminated. Result: 10 blocking + 2 non-blocking findings recorded above. Rubric: Not ready until the blocking findings are dispositioned. Status stays DRAFT for Craig's deeper review. +- Why: Craig asked for the weakness loop before his own deep review, in place of a standard first-pass spec-review. +- Artifacts: findings grounded in code reads — roam-sync.sh:28 (git add -A commits anything inside the roam repo), .ai/scripts/session-context-path (AI_AGENT_ID scoping), todo.org [#B] "Org-table helpers corrupt example blocks" (lint-org mutate-on-lint), inbox.org core §2 no-approvals park path, verification.md pre-commit suite rule. + +** 2026-07-14 Tue @ 00:52:03 -0500 — Craig Jennings — author +- What: initial draft. +- Why: work project's sentry proposal accepted after skeptical review; nine design decisions resolved live in-session, recorded under Decisions. +- Artifacts: [[file:../design/2026-07-14-sentry-workflow-proposal.org][origin proposal]]; todo.org sentry build task. diff --git a/docs/specs/2026-07-20-silent-until-signal-monitors-spec.org b/docs/specs/2026-07-20-silent-until-signal-monitors-spec.org new file mode 100644 index 0000000..c15d10f --- /dev/null +++ b/docs/specs/2026-07-20-silent-until-signal-monitors-spec.org @@ -0,0 +1,126 @@ +#+TITLE: Silent-Until-Signal Monitor Loops — Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-20 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED silent-until-signal monitor loops +:PROPERTIES: +:ID: af592bd6-d3e6-47e2-8804-2a287b4d9303 +:END: +- 2026-07-20 Mon @ 13:34:53 -0500 — READY (Craig approved after his read) → building Phases 2-5 immediately → IMPLEMENTED. Phase 2 (auto triage-intake heartbeat), Phase 3 (auto inbox-zero heartbeat), Phase 4 (shared-policy home: the spec is the single definition, each workflow states its own heartbeat and references it), Phase 5 (manual-testing checklist). All phases shipped. +- 2026-07-20 Mon @ 12:30:00 -0500 — Phase 1 (sentry quiet-fire heartbeat) implemented ahead of the full READY gate, at Craig's direction — a quiet sentry fire now collapses to =sentry at HH:MM: nothing=, and the "no silent skip" discipline is reconciled (the heartbeat is the explicit "nothing" record). Phases 2-5 (triage, inbox, shared-policy home, verification) still pending Craig's deep read. Spec stays DRAFT. +- 2026-07-20 Mon @ 12:14:20 -0500 — drafted during the morning session. Design decisions resolved live with Craig off the .emacs.d proposal and the sentry live-trial evidence. DRAFT pending his deep read. + +* Metadata +| Status | draft | +|----------+--------------------------------------------------------------| +| Owner | Craig Jennings | +|----------+--------------------------------------------------------------| +| Reviewer | (spec-review, next session) | +|----------+--------------------------------------------------------------| +| Related | [[file:../../todo.org::*Silent-until-signal for in-session monitor loops][todo.org — silent-until-signal task]] | +|----------+--------------------------------------------------------------| + +* Summary + +An in-session monitor loop (sentry, auto triage-intake, auto inbox-zero) fires the model once per interval. Today each fire narrates a full turn even when nothing changed, so a long session fills with walls of "quiet fire" / "no new items" output. The fix is a policy, not a mechanism: every monitor fire does cheap detection first, and on an empty check collapses to a single labelled heartbeat line ("sentry at 03:20: nothing") and stops. Only a fire that finds a genuinely new item spends a full surface-and-judge turn. Detection stays in-session, so the policy applies uniformly to file-based and MCP-auth loops alike. + +* Problem / Context + +In-session cron/loop monitors surface a visible model turn on every fire. When nothing changed, that turn is pure noise — a per-pass sentry digest full of SKIP lines, or a triage/inbox "nothing new" report. Over a night or a long session the signal (the one fire that found something) drowns in the empty ticks. The rulesets sentry live trial (2026-07-20) demonstrated it directly: fires 1-2 did real work, fires 3-8 were near-identical walls of no-op lines. + +Origin: a Craig-approved proposal from .emacs.d (2026-07-20), captured off an auto inbox-zero session filling with empty-check noise. + +** Why not an external watcher (the rejected shape) + +The proposal's first instinct was a shell-level watcher (systemd timer or backgrounded loop) doing detection outside the model, producing zero output on an empty check and dropping a handoff into inbox/ only on a real item — so the model runs zero turns when nothing changed. That is truly-zero-idle, but it has a disqualifying cost: it moves detection out of the session, and auto triage-intake's sources (Gmail, Slack, Linear) are reachable only through the session's inherited MCP auth. triage-intake.org is explicit that it runs in the live session precisely because "the headless-auth wall that blocks a detached cron run does not apply." A detached watcher cannot scan those sources at all. The external-watcher shape would therefore split the three targets into two incompatible cases (file-detectable vs MCP-auth) and still leave triage unsolved. + +The reframing (Craig, 2026-07-20): the noise is a *policy* problem — when to spend a full model turn — not a missing piece of infrastructure. Keeping detection in-session and making the empty fire cheap solves the actual complaint without the watcher, without per-machine daemon setup, and without breaking MCP auth. It applies to all three loops uniformly. + +* Goals and Non-Goals + +** Goals +- An empty monitor fire produces exactly one labelled heartbeat line, not a full narrated turn: =<workflow> at HH:MM: nothing=. +- A fire that detects a genuinely new item does the full surface-and-judge turn unchanged. +- One uniform policy across sentry, auto triage-intake, and auto inbox-zero — no per-loop special-casing. +- Detection stays in-session so MCP-auth loops (triage) get the same treatment as file-based loops. +- Each loop reuses the seen-state it already keeps; no new watcher-owned seen-list. + +** Non-Goals +- No external watcher, systemd timer, or Monitor-tool daemon. Detection is the loop body's own cheap check. +- No truly-zero-idle (no model turn at all on empty). That needs an external watcher and is the shape rejected above; if it is ever wanted for the file-based loops only, it is a separate vNext, logged not built. +- No change to what a loop does when it *does* find something — the surface/judge/act behaviour is untouched. +- No change to the one-shot (non-loop) invocations of these workflows. + +* Design + +** The policy + +Every in-session monitor fire runs in two steps: + +1. *Detect (cheap, silent).* Run the loop's existing detection against its seen-state — sentry's pass probes and branch/digest state, triage's sentinel scan, the inbox monitor's disposition check. This is Bash/tool work, not narration. +2. *Branch on the result.* + - *Nothing new* → emit one line, =<workflow> at HH:MM: nothing=, and end the fire. No digest, no per-pass lines, no report. + - *Something new* → the full existing turn: surface, judge, act/queue, and its normal richer output. + +The heartbeat is the whole output of an empty fire. Its format is fixed: the workflow's short name, =at=, =HH:MM= (local, from =date=), then =: = and the result word (=nothing= for an empty check). Examples: =sentry at 03:20: nothing=, =triage intake at 03:30: nothing=, =inbox zero at 03:30: nothing=. + +** Per-workflow application + +- *Sentry.* A fire whose passes all probe-skip or no-op collapses to =sentry at HH:MM: nothing=. No per-pass digest block is written for a quiet fire. A fire that runs or queues anything writes its full digest as today. (This supersedes the trial's behaviour, where fires 3-8 each wrote a full no-op digest.) +- *Auto triage-intake.* An auto-mode sweep that finds nothing across its enabled sources collapses to =triage intake at HH:MM: nothing=. Detection stays in-session, so the MCP sources are scanned normally; only the output on empty changes. +- *Auto inbox-zero.* A roam-mode cycle that finds no new inbox items collapses to =inbox zero at HH:MM: nothing=. + +** Seen-state (no new artifact) + +Because detection stays in-session, each loop keeps using the state it already maintains to know what is "new": triage's =.ai/last-triage-intake= sentinel, sentry's branch + digest, the inbox monitor's per-cycle disposition. The proposal's "watcher-owned seen-list replacing the in-anchor Dispositioned list" is dropped — it was an artifact of the external-watcher shape, which is not being built. + +** The accepted consequence + +An in-session loop still fires the model once per interval; the harness invokes it each time. So "silent" means the empty fire is *cheap* (one detection pass plus one heartbeat line), not *absent*. This is the deliberate trade for uniformity and for keeping the MCP auth that makes triage possible. The heartbeat also doubles as a liveness pulse: a visible "still running, nothing to do" beats silence that is indistinguishable from a stalled loop. + +* Decisions + +** DONE Policy, not mechanism — detection stays in-session +CLOSED: [2026-07-20 Mon] +Craig reframed the proposal: silent-until-signal is a policy about when to spend a full model turn, not a new watcher. Keeping detection in-session dissolves the MCP-auth split (triage's sources need session auth) and needs no per-machine daemon. + +** DONE Empty fire → one labelled heartbeat line (not fully silent) +CLOSED: [2026-07-20 Mon] +Format =<workflow> at HH:MM: nothing=. A visible pulse is worth one line so a running loop is distinguishable from a stalled one; full silence was the rejected alternative. + +** DONE Applies to sentry, auto triage-intake, and auto inbox-zero uniformly +CLOSED: [2026-07-20 Mon] +The two MCP-auth loops are first-class targets, not just sentry, precisely because detection stays in-session. + +** DONE No external watcher; no truly-zero-idle in v1 +CLOSED: [2026-07-20 Mon] +The systemd-timer / Monitor-tool / backgrounded-shell shapes are out. Truly-zero-idle (no turn on empty) is the only thing they'd buy, it only works for file-based loops, and it breaks triage. Logged as a possible file-only vNext, not built. + +** DONE Reuse each loop's existing seen-state +CLOSED: [2026-07-20 Mon] +No new watcher-owned seen-list; the sentinel / branch-digest / disposition each loop already keeps is the detection state. + +* Implementation Phases + +** Phase 1 — sentry quiet-fire heartbeat — DONE 2026-07-20 +Edit =.ai/workflows/sentry.org= (canonical + mirror): a fire whose passes all probe-skip or no-op writes =sentry at HH:MM: nothing= instead of a full per-pass digest; a fire that runs or queues anything writes the full digest unchanged. Update the digest section and the Common Mistakes "silent skip" note to draw the quiet-fire-vs-working-fire line. Run sync-check. (Separable and the highest-value piece — do first.) + +Shipped 2026-07-20: five edits to sentry.org — Pass Runner step 3 (per-pass lines collapse on a quiet fire), Fire-end step 2 (the heartbeat-vs-digest decision + heartbeat commit variant), the digest section (working-block vs quiet-heartbeat), Unattended-safety (quiet fire is not a silent skip), Common Mistakes #7 (the carve-out). Canonical + mirror synced, lint clean. + +** Phase 2 — auto triage-intake heartbeat +Edit =.ai/workflows/triage-intake.org= Auto mode: an empty sweep collapses to =triage intake at HH:MM: nothing=. Preserve in-session detection and the accumulate-don't-mutate contract. Run sync-check. + +** Phase 3 — auto inbox-zero heartbeat +Edit =.ai/workflows/inbox.org= Auto inbox zero mode: an empty cycle collapses to =inbox zero at HH:MM: nothing=. Run sync-check. + +** Phase 4 — state the shared policy once +Factor the policy statement into one place both loops and sentry point at (a short section in =inbox.org= monitor-mode core, or a =claude-rules/= note if it reads as cross-cutting), so the three workflows reference one definition rather than restating it. Decide the home during build. + +** Phase 5 — verification +Workflow prose, no bats surface. Verify by exercising: a sentry quiet fire prints one heartbeat line and writes no digest; a working fire still writes its full digest and commits. For the two MCP loops, confirm an empty sweep prints the heartbeat and a sweep with a planted item still does the full turn. Add a manual-testing checklist entry per =verification.md= where a live check is the only proof. + +* Prototype / UI + +Not applicable — no UI surface; the deliverable is the one-line heartbeat format. diff --git a/docs/specs/2026-07-20-triage-source-activation-spec.org b/docs/specs/2026-07-20-triage-source-activation-spec.org new file mode 100644 index 0000000..dce1ec1 --- /dev/null +++ b/docs/specs/2026-07-20-triage-source-activation-spec.org @@ -0,0 +1,127 @@ +#+TITLE: Triage Source Activation — Spec +#+AUTHOR: Craig Jennings +#+DATE: 2026-07-20 +#+TODO: TODO | DONE +#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED + +* IMPLEMENTED triage source activation +:PROPERTIES: +:ID: af73ef0b-cd1d-46f1-9e1d-62695733a4de +:END: +- 2026-07-20 Mon @ 13:42:32 -0500 — READY (Craig approved after his read, both open decisions resolved via cj comments: declaration format "good. approved", interactive gate "all of them") → built and IMPLEMENTED same session. Phase 0 activation gate in triage-intake.org, sentry pass-3 probe, engine-intro documentation (the template notes.org has no Workflow State block, so the declaration is documented in triage-intake.org rather than there — deviation from the drafted Phase 3), migration handoffs to home + work, a manual-testing entry. Canonical + mirror synced. +- 2026-07-20 Mon @ 08:43:23 -0500 — drafted during the morning sentry review. The activation model converged live with Craig off the sentry live-trial finding. DRAFT pending his deep read. + +* Metadata +| Status | draft | +|----------+--------------------------------------------------------------| +| Owner | Craig Jennings | +|----------+--------------------------------------------------------------| +| Reviewer | (spec-review, next session) | +|----------+--------------------------------------------------------------| +| Related | [[file:../../todo.org::*Triage source activation][todo.org — Triage source activation task]] | +|----------+--------------------------------------------------------------| + +* Summary + +triage-intake should pull only the sources a project has chosen to pull. Today it runs every plugin it can discover, and the general (personal-account) plugins are template-synced into every project, so every project looks like it wants to triage Craig's personal Gmail, cmail, calendar, and Telegram. The fix is one activation layer: a general plugin runs only when the project names it in a =:TRIAGE_SOURCES:= declaration; a project-specific plugin stays active by its presence, which is already a deliberate per-project act. Sentry's pass-3 probe then reads the same signal. + +* Problem / Context + +The 8-fire sentry live trial (rulesets, 2026-07-20) surfaced this. Sentry's pass 3 probe is "triage source plugins present for this project," and triage-intake discovers sources by globbing =.ai/workflows/triage-intake.*.org= (general, template-synced) plus =.ai/project-workflows/triage-intake.*.org= (project-specific, never synced). Because the general plugins sync into every project, "plugins present" is true everywhere, so the triage pass self-activates in every project — including rulesets, which is not a triage target. + +The harm is two-layered, and sentry's existing safety rule only catches one layer. Sentry's pass-3 line says "destructive actions queue; they never fire unattended," which would hold back the trash/mark-read/star hygiene. But triage-intake also reads the accounts and files each Action item to the local =todo.org= as a =:quick:reactive:= task. Reading and filing are not destructive, so nothing queues them. Under sentry, triage would still authenticate to Craig's personal inboxes overnight and file his personal action items into whatever project the fire runs in. Wrong scope, and it touched real accounts to get there. + +The same over-pull exists interactively: running triage-intake by hand in rulesets today would pull personal Gmail too. The trial only made the unattended case visible. + +Root cause: the model conflates *presence* with *activation*. A plugin has two existing gates — it must be globbed (presence) and pass its =ENABLED= precondition (capability: "is the gmail MCP reachable?"). Neither answers the question the trial exposed: *should this project pull this source?* For the general plugins, presence comes from sync and capability is true on Craig's machine everywhere, so both gates pass in every project. + +Asymmetry that shapes the fix: project-specific plugins do not leak. They live in =.ai/project-workflows/=, are never synced, and exist only where someone deliberately dropped one. A project that polls an RSS feed puts an =triage-intake.rss-*.org= plugin there, and it runs in that project and nowhere else. Only the general synced plugins leak. So the missing activation layer only needs to gate the general plugins. + +* Goals and Non-Goals + +** Goals +- Per-project source selection: a project pulls exactly the sources it declares, nothing more. +- Off-limits by default: a general source that a project has not named is never pulled there. +- Project-specific sources keep working by presence — dropping the plugin is the declaration (Craig's RSS-feed case works unchanged). +- One fix covers both paths: the activation layer lives in triage-intake, so interactive and unattended (sentry) runs both respect it. +- Sentry's pass-3 probe reads the same activation signal rather than mere plugin presence. + +** Non-Goals +- No change to the per-plugin =ENABLED= capability check — it stays the "can I reach this source" gate. +- No change to the four-bucket classification, the digest shape, or the close behavior. +- No auto-migration across projects. Projects that pull general sources today declare them via handoff; the change never edits another project's config unattended. +- No new plugin discovery mechanism — the two-directory glob stays. + +* Design + +** Two plugin classes, one new activation layer + +- *Project-specific plugins* (=.ai/project-workflows/triage-intake.*.org=): active by presence. The plugin exists only because the project author put it there, which is itself the per-project declaration. No further gate. This is where a project's own sources live — an RSS feed, a work Linear, a work Slack. +- *General plugins* (=.ai/workflows/triage-intake.*.org=, template-synced — personal Gmail, cmail, calendar, Telegram, GitHub PRs): active only when the project names the source in its =:TRIAGE_SOURCES:= declaration. Present-but-undeclared means available-not-active: the plugin is on disk, but this project does not pull it. + +** The declaration + +A line in the project's =.ai/notes.org= Workflow State block, alongside =:COMMIT_AUTONOMY:= and =:LAST_AUDIT:=: + +: :TRIAGE_SOURCES: personal-gmail cmail + +Space-separated source names matching general-plugin basenames (=personal-gmail=, =cmail=, =personal-calendar=, =telegram=, =github-prs=). Absent or empty means no general sources are active for this project. Project-specific plugins are unaffected by this line — they run regardless, because presence is their declaration. + +** triage-intake Phase 0 change + +Phase 0 keeps globbing both directories. The loaded-set computation changes: for each *general* plugin, additionally require its basename to appear in =:TRIAGE_SOURCES:=; skip it with an announced reason otherwise ("skipping personal-gmail — not in :TRIAGE_SOURCES:"). Project-specific plugins skip this check. The =ENABLED= capability check still runs on the survivors. The announce-loaded-set block already exists and gains an "inactive (undeclared)" line so the omission stays visible rather than silent — the same anti-silence discipline Phase 0 already enforces. + +** Sentry pass-3 probe change + +The probe changes from "triage source plugins present" to "the project has at least one active triage source" — any project-specific plugin present, or a non-empty =:TRIAGE_SOURCES:= intersecting the general plugins on disk. rulesets, declaring nothing and owning no project-specific plugin, probe-skips cleanly. + +** Migration + +Projects that pull general sources today (home, and possibly work) each add a =:TRIAGE_SOURCES:= line, or their triage goes quiet. This is a per-project handoff, not an automated sweep — the change can't safely guess each project's intended source set. Projects that only ever ran project-specific plugins need no migration. + +* Decisions + +** DONE Activation gates general plugins only; project-specific stay active-by-presence +CLOSED: [2026-07-20 Mon] +Converged live with Craig. Project-specific plugins are already per-project (never synced), so they need no gate; only the general synced plugins leak, so only they need a declaration. + +** DONE The activation layer lives in triage-intake Phase 0, not only the sentry probe +CLOSED: [2026-07-20 Mon] +Placing it in the engine fixes the interactive over-pull too. Sentry inherits the signal rather than reimplementing it. + +** DONE Off-limits by default +CLOSED: [2026-07-20 Mon] +An undeclared general source is never pulled. Opt-in is explicit; there is no "pull everything discovered" default. + +** DONE Migration is per-project handoffs, never an unattended edit of another project's config +CLOSED: [2026-07-20 Mon] +The change can't guess a project's intended source set, and cross-project auto-edits violate the boundary rule. + +** DONE Declaration format — =:TRIAGE_SOURCES:= space-separated basenames in notes.org Workflow State +CLOSED: [2026-07-20 Mon] +Approved by Craig (2026-07-20). =:TRIAGE_SOURCES: personal-gmail cmail= in notes.org Workflow State, mirroring =:COMMIT_AUTONOMY:=. An empty-but-present marker and an absent one are treated identically — both mean "no general sources active" — so there's no need to distinguish them. A project-specific-only project carries no marker; its absence is correct, since those plugins activate by presence. + +** DONE Interactive triage adopts the same gate as unattended +CLOSED: [2026-07-20 Mon] +Craig: "all of them" (2026-07-20). The activation gate lives in triage-intake Phase 0 and applies to every path — interactive and unattended alike — so running triage by hand in a project also respects its =:TRIAGE_SOURCES:= declaration. One activation layer, no per-path special-casing. + +* Implementation Phases + +** Phase 1 — triage-intake Phase 0 activation rule +Edit =.ai/workflows/triage-intake.org= (canonical =claude-templates/.ai/workflows/= + mirror): add the general-vs-project-specific activation rule to Phase 0, the =:TRIAGE_SOURCES:= read, and the "inactive (undeclared)" announce line. Run sync-check. + +** Phase 2 — sentry pass-3 probe +Edit =.ai/workflows/sentry.org= (canonical + mirror): change the pass-3 probe to "any active triage source present" and note the activation source. Run sync-check. + +** Phase 3 — document the declaration +Document =:TRIAGE_SOURCES:= in the notes.org Workflow State reference (the template =claude-templates/.ai/notes.org= Workflow State block) so new projects see it. Cross-reference from triage-intake. + +** Phase 4 — migration handoffs +=inbox-send= home (and work, if it pulls general sources) the =:TRIAGE_SOURCES:= line each should add, with the reason. No auto-edit. + +** Phase 5 — verification +triage-intake is a workflow (prose), not a script, so there's no bats surface for the activation rule directly. Verify by a scripted check that a project with no declaration loads zero general plugins (a small fixture around the Phase-0 glob-and-filter logic if it's extracted, or a manual-testing checklist entry otherwise). Confirm rulesets probe-skips triage under sentry, and home still pulls its declared sources. + +* Prototype / UI + +Not applicable — no UI surface. diff --git a/docs/specs/agent-knowledge-base-spec.org b/docs/specs/agent-knowledge-base-spec.org index e36d897..d5c0ce7 100644 --- a/docs/specs/agent-knowledge-base-spec.org +++ b/docs/specs/agent-knowledge-base-spec.org @@ -1,5 +1,5 @@ #+TITLE: Agent Knowledge Base on Org-roam — Spec -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-10 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED diff --git a/docs/specs/inbox-workflow-consolidation-spec.org b/docs/specs/inbox-workflow-consolidation-spec.org index 4543e77..24ce30a 100644 --- a/docs/specs/inbox-workflow-consolidation-spec.org +++ b/docs/specs/inbox-workflow-consolidation-spec.org @@ -1,5 +1,5 @@ #+TITLE: Inbox Workflow Consolidation — Spec -#+AUTHOR: Craig Jennings & Claude +#+AUTHOR: Craig Jennings #+DATE: 2026-06-23 #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED diff --git a/docs/specs/wrapup-routing-spec.org b/docs/specs/wrapup-routing-spec.org index 1a150fc..07d9ec6 100644 --- a/docs/specs/wrapup-routing-spec.org +++ b/docs/specs/wrapup-routing-spec.org @@ -4,14 +4,15 @@ #+TODO: TODO | DONE #+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED -* DOING Wrap-Up Inbox/Transcript Routing — Spec +* IMPLEMENTED Wrap-Up Inbox/Transcript Routing — Spec :PROPERTIES: :ID: 00b47414-2213-4a99-be35-48ceb266fc08 :END: +- 2026-07-04 Sat @ 11:49:59 -0500 — DOING → IMPLEMENTED: the task-routing build shipped and is green — route_recommend.py (destination discovery + recommendation, 13 unit tests), the :ROUTE_CANDIDATE: marker wired into inbox process mode, the wrap-it-up router sub-step, and the route-batch delivery helper (9 bats tests). Manual cross-project end-to-end validation tracked as its own task; the transcript-filing half stays a deferred vNext. - 2026-07-02 Thu @ 00:17:01 -0400 — retrofitted by spec-sort; status set to DOING (evidence-based, human-confirmed) * Metadata -| Status | doing | +| Status | implemented | |----------+-----------------------------------------------------| | Owner | Craig Jennings | |----------+-----------------------------------------------------| @@ -217,7 +218,7 @@ Everything else accepted as written: H1 (inbox-route supersedes direct-move; D2/ ** 2026-06-21 Sun @ 01:58:41 -0400 — Claude Code (rulesets) — reviewer - What: spec-review pass. Rubric *Not ready*, two blocking findings. H1: the inbox-route alternative (inbox-send each routable keeper to the destination's inbox/, let its own process-inbox file it) supersedes the direct-move design — reshape D2, drop Phase 2 and D3's provenance burden. H2: pin the candidate-set marking to Option A (tag =:ROUTE_CANDIDATE:= at process-inbox file time). Four medium findings (M1 confidence tiers, M2 empty-set silence, M3 paired process-inbox edit phase, M4 cross-project.md note). Full review + drop-in implementation tasks in the review file. - Why: Craig challenged D2 directly (why edit a foreign todo.org rather than use the sanctioned inbox-send path). The review confirmed it: inbox-send already emits the exact provenance D3 reinvents, process-inbox already files per-item with the destination's own gate, cross-project.md sanctions the inbox path, and a verified precondition reverses the spec's assumption — chime and yt-sync have inbox/ but no todo.org, so direct-move silently drops keepers headed there while inbox-route degrades gracefully. -- Artifacts: [[file:wrapup-routing-spec-review.org][review file]]. Next: spec-response to disposition H1/H2 (recommend accept both), which moves the rubric to Ready. +- Artifacts: the review file (since folded into this spec). Next: spec-response to disposition H1/H2 (recommend accept both), which moves the rubric to Ready. ** 2026-06-21 Sun @ 02:06:37 -0400 — Craig Jennings + Claude Code (rulesets) — responder - What: folded the spec-review in. Accepted H1 (inbox-route) and H2 (tag at file time); superseded D2 and D3; added D7 (deliver via =inbox-send=), D8 (=:ROUTE_CANDIDATE:= marker at file time), D9 (local source removal + reject-flow recovery). Rewrote Summary, Goals, Design mechanics, Implementation phases (dropped the atomic-move helper — Phase 2 is now the =process-inbox= marker edit), and Acceptance criteria for the inbox-route. One modify (D9) refines H1's vague source-handling. Cookie [9/9]; Status → Ready. |
