aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-20 13:44:11 -0500
committerCraig Jennings <c@cjennings.net>2026-07-20 13:44:11 -0500
commit4d87f35063e8430408178871cbf20b7268d7836a (patch)
treef4bca35bceb6976d921c5ef5687369f06ab159e1
parent0e9958abfbe97540b348b2118a863f7995e73b9e (diff)
downloadrulesets-4d87f35063e8430408178871cbf20b7268d7836a.tar.gz
rulesets-4d87f35063e8430408178871cbf20b7268d7836a.zip
feat: gate triage sources on a per-project :TRIAGE_SOURCES: declaration
The general triage plugins (personal Gmail, cmail, calendar, Telegram, GitHub PRs) are template-synced into every project, so the old "plugins present" probe self-activated triage everywhere. The sentry live trial caught it reaching personal inboxes from a project that isn't a triage target. Now a general plugin activates only when the project names its basename in a :TRIAGE_SOURCES: line in notes.org Workflow State. A project-specific plugin stays active by presence. The gate lives in triage-intake Phase 0, so it fixes the interactive over-pull too, and sentry's pass-3 probe reads the same signal. Presence is capability, the declaration is activation. A project that declares nothing and owns no project plugin pulls nothing. Migration handoffs went to home and work. rulesets correctly declares nothing.
-rw-r--r--.ai/workflows/sentry.org2
-rw-r--r--.ai/workflows/triage-intake.org16
-rw-r--r--claude-templates/.ai/workflows/sentry.org2
-rw-r--r--claude-templates/.ai/workflows/triage-intake.org16
-rw-r--r--docs/specs/2026-07-20-triage-source-activation-spec.org13
-rw-r--r--todo.org16
6 files changed, 41 insertions, 24 deletions
diff --git a/.ai/workflows/sentry.org b/.ai/workflows/sentry.org
index bfeeffc..6e70402 100644
--- a/.ai/workflows/sentry.org
+++ b/.ai/workflows/sentry.org
@@ -129,7 +129,7 @@ In order. Each names its detection probe. A pass whose probe fails is one skip l
2. *Inbox zero* — run =inbox.org= roam mode under the no-approvals contract: quick+solo+agreed items execute, shared-asset and convention proposals park (prepared diff, =VERIFY= task, sender reply) in the approval queue. Edits to =~/org/roam/inbox.org= take the roam-write lock + =capture-guard=. Probe: the roam clone or a project =inbox/= exists. Tidying the shared roam inbox is allowed from *any* project session, work included — it's housekeeping on a shared resource, not a durable KB-node write, so the work-denylist doesn't gate it (=knowledge-base.md=). Never park it as a cross-project boundary crossing.
-3. *Triage intake* — run =triage-intake.org=. Probe: triage source plugins present for this project. Destructive actions (deleting, archiving, sending) queue; they never fire unattended.
+3. *Triage intake* — run =triage-intake.org=. Probe: the project has at least one *active* triage source — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended.
4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=.
diff --git a/.ai/workflows/triage-intake.org b/.ai/workflows/triage-intake.org
index 4f7c7b8..7451fed 100644
--- a/.ai/workflows/triage-intake.org
+++ b/.ai/workflows/triage-intake.org
@@ -11,6 +11,8 @@ Think of it as the ER intake queue: every new message, invite, and PR notificati
*This file is the engine.* It carries no sources of its own. Every source it scans comes from a *source plugin* — a =triage-intake.<source>.org= file the engine loads at Phase 0. The engine is source-agnostic and project-agnostic; the project- and account-specific knowledge lives entirely in the plugins. To add a source, drop a plugin file. To change one, edit its plugin. Never wire a source into this file.
+*Which sources a project pulls is a per-project choice.* A *project-specific* plugin (=.ai/project-workflows/triage-intake.*.org=, never synced) is active by presence — dropping it is the declaration. A *general* plugin (=.ai/workflows/triage-intake.*.org=, template-synced into every project — personal Gmail, cmail, calendar, Telegram, GitHub PRs) is active only when the project names its basename in a =:TRIAGE_SOURCES:= line in =.ai/notes.org= Workflow State (space-separated basenames, e.g. =:TRIAGE_SOURCES: personal-gmail cmail=). A project that declares nothing and owns no project plugin pulls nothing. This is the Phase 0 activation gate — presence is capability, the declaration is activation (see =docs/specs/2026-07-20-triage-source-activation-spec.org=).
+
Distinct from =daily-prep.org=:
- *daily-prep* — heavier, once daily, builds the day's plan + standup brief + meeting prep + time blocks.
- *triage-intake* — fast, repeatable, just answers "what's new since last check?"
@@ -58,16 +60,18 @@ ls .ai/workflows/triage-intake.*.org .ai/project-workflows/triage-intake.*.org 2
The glob exclude is automatic: =triage-intake.*.org= matches the plugins but not this engine file (=triage-intake.org= has no second dot-segment), so the engine never loads itself.
After globbing, for each plugin file:
-1. Read it.
-2. Evaluate its =ENABLED= precondition. If false, *announce the skip with its reason* ("skipping linear — mcp__linear not present") and move on.
-3. The surviving set is the source list for Phases A-D.
+1. *Activation gate.* A *general* plugin (from =.ai/workflows/=, template-synced into every project) is active only if its basename appears in the project's =:TRIAGE_SOURCES:= declaration (=.ai/notes.org= Workflow State — a space-separated list of source basenames). If it isn't declared, it is *inactive*: announce it ("inactive: personal-gmail — not in :TRIAGE_SOURCES:") and skip it. A *project-specific* plugin (from =.ai/project-workflows/=, never synced) is always active — dropping it there is itself the per-project declaration. This is what stops the synced general plugins from self-activating in projects that aren't triage targets: presence is capability, the declaration is activation (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). An absent or empty =:TRIAGE_SOURCES:= means no general sources are active; a project with no declaration and no project plugin has no active sources, so triage no-ops there.
+2. Read it.
+3. Evaluate its =ENABLED= precondition. If false, *announce the skip with its reason* ("skipping linear — mcp__linear not present") and move on.
+4. The surviving set — active and enabled — is the source list for Phases A-D.
-*Announce the loaded set before scanning* so the omission can't hide:
+*Announce the loaded set before scanning* so the omission can't hide — inactive (undeclared) plugins are named too, so a general plugin left out of =:TRIAGE_SOURCES:= is a visible choice, not a silent drop:
#+begin_example
-Loaded 5 source plugins:
- general: personal-gmail, personal-calendar, cmail, github-prs
+Loaded 2 source plugins (:TRIAGE_SOURCES: personal-gmail cmail):
+ general: personal-gmail, cmail
project: deepsat-gmail
+ inactive (undeclared): personal-calendar, github-prs, telegram
skipped: linear (mcp__linear not present)
#+end_example
diff --git a/claude-templates/.ai/workflows/sentry.org b/claude-templates/.ai/workflows/sentry.org
index bfeeffc..6e70402 100644
--- a/claude-templates/.ai/workflows/sentry.org
+++ b/claude-templates/.ai/workflows/sentry.org
@@ -129,7 +129,7 @@ In order. Each names its detection probe. A pass whose probe fails is one skip l
2. *Inbox zero* — run =inbox.org= roam mode under the no-approvals contract: quick+solo+agreed items execute, shared-asset and convention proposals park (prepared diff, =VERIFY= task, sender reply) in the approval queue. Edits to =~/org/roam/inbox.org= take the roam-write lock + =capture-guard=. Probe: the roam clone or a project =inbox/= exists. Tidying the shared roam inbox is allowed from *any* project session, work included — it's housekeeping on a shared resource, not a durable KB-node write, so the work-denylist doesn't gate it (=knowledge-base.md=). Never park it as a cross-project boundary crossing.
-3. *Triage intake* — run =triage-intake.org=. Probe: triage source plugins present for this project. Destructive actions (deleting, archiving, sending) queue; they never fire unattended.
+3. *Triage intake* — run =triage-intake.org=. Probe: the project has at least one *active* triage source — a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=), or a non-empty =:TRIAGE_SOURCES:= declaration naming general plugins that exist. Mere presence of the template-synced general plugins does *not* activate the pass; a project that declares no sources probe-skips (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). Destructive actions (deleting, archiving, sending) queue; they never fire unattended.
4. *Todo cleanup* — the =clean-todo.org= mechanics (hygiene pass + =--archive-done= + =--convert-subtasks=). Probe: a root =todo.org=.
diff --git a/claude-templates/.ai/workflows/triage-intake.org b/claude-templates/.ai/workflows/triage-intake.org
index 4f7c7b8..7451fed 100644
--- a/claude-templates/.ai/workflows/triage-intake.org
+++ b/claude-templates/.ai/workflows/triage-intake.org
@@ -11,6 +11,8 @@ Think of it as the ER intake queue: every new message, invite, and PR notificati
*This file is the engine.* It carries no sources of its own. Every source it scans comes from a *source plugin* — a =triage-intake.<source>.org= file the engine loads at Phase 0. The engine is source-agnostic and project-agnostic; the project- and account-specific knowledge lives entirely in the plugins. To add a source, drop a plugin file. To change one, edit its plugin. Never wire a source into this file.
+*Which sources a project pulls is a per-project choice.* A *project-specific* plugin (=.ai/project-workflows/triage-intake.*.org=, never synced) is active by presence — dropping it is the declaration. A *general* plugin (=.ai/workflows/triage-intake.*.org=, template-synced into every project — personal Gmail, cmail, calendar, Telegram, GitHub PRs) is active only when the project names its basename in a =:TRIAGE_SOURCES:= line in =.ai/notes.org= Workflow State (space-separated basenames, e.g. =:TRIAGE_SOURCES: personal-gmail cmail=). A project that declares nothing and owns no project plugin pulls nothing. This is the Phase 0 activation gate — presence is capability, the declaration is activation (see =docs/specs/2026-07-20-triage-source-activation-spec.org=).
+
Distinct from =daily-prep.org=:
- *daily-prep* — heavier, once daily, builds the day's plan + standup brief + meeting prep + time blocks.
- *triage-intake* — fast, repeatable, just answers "what's new since last check?"
@@ -58,16 +60,18 @@ ls .ai/workflows/triage-intake.*.org .ai/project-workflows/triage-intake.*.org 2
The glob exclude is automatic: =triage-intake.*.org= matches the plugins but not this engine file (=triage-intake.org= has no second dot-segment), so the engine never loads itself.
After globbing, for each plugin file:
-1. Read it.
-2. Evaluate its =ENABLED= precondition. If false, *announce the skip with its reason* ("skipping linear — mcp__linear not present") and move on.
-3. The surviving set is the source list for Phases A-D.
+1. *Activation gate.* A *general* plugin (from =.ai/workflows/=, template-synced into every project) is active only if its basename appears in the project's =:TRIAGE_SOURCES:= declaration (=.ai/notes.org= Workflow State — a space-separated list of source basenames). If it isn't declared, it is *inactive*: announce it ("inactive: personal-gmail — not in :TRIAGE_SOURCES:") and skip it. A *project-specific* plugin (from =.ai/project-workflows/=, never synced) is always active — dropping it there is itself the per-project declaration. This is what stops the synced general plugins from self-activating in projects that aren't triage targets: presence is capability, the declaration is activation (see =docs/specs/2026-07-20-triage-source-activation-spec.org=). An absent or empty =:TRIAGE_SOURCES:= means no general sources are active; a project with no declaration and no project plugin has no active sources, so triage no-ops there.
+2. Read it.
+3. Evaluate its =ENABLED= precondition. If false, *announce the skip with its reason* ("skipping linear — mcp__linear not present") and move on.
+4. The surviving set — active and enabled — is the source list for Phases A-D.
-*Announce the loaded set before scanning* so the omission can't hide:
+*Announce the loaded set before scanning* so the omission can't hide — inactive (undeclared) plugins are named too, so a general plugin left out of =:TRIAGE_SOURCES:= is a visible choice, not a silent drop:
#+begin_example
-Loaded 5 source plugins:
- general: personal-gmail, personal-calendar, cmail, github-prs
+Loaded 2 source plugins (:TRIAGE_SOURCES: personal-gmail cmail):
+ general: personal-gmail, cmail
project: deepsat-gmail
+ inactive (undeclared): personal-calendar, github-prs, telegram
skipped: linear (mcp__linear not present)
#+end_example
diff --git a/docs/specs/2026-07-20-triage-source-activation-spec.org b/docs/specs/2026-07-20-triage-source-activation-spec.org
index f5c339c..dce1ec1 100644
--- a/docs/specs/2026-07-20-triage-source-activation-spec.org
+++ b/docs/specs/2026-07-20-triage-source-activation-spec.org
@@ -4,10 +4,11 @@
#+TODO: TODO | DONE
#+TODO: DRAFT READY DOING | IMPLEMENTED SUPERSEDED CANCELLED
-* DRAFT triage source activation
+* 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
@@ -96,11 +97,13 @@ An undeclared general source is never pulled. Opt-in is explicit; there is no "p
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.
-** VERIFY Declaration format — =:TRIAGE_SOURCES:= space-separated basenames in notes.org Workflow State
-Proposed. Mirrors =:COMMIT_AUTONOMY:=. Open sub-questions: does an empty-but-present marker need to differ from an absent one (e.g. "declared none, on purpose" vs "never configured")? Should a project-specific-only project carry the marker at all, or is its absence correct there?
+** 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.
-** VERIFY Should interactive triage adopt the same gate, or only unattended?
-Recommended: same gate both paths (one activation layer). Craig to confirm — the alternative is gating only the sentry pass and leaving interactive as run-all-discovered, which is less clean but avoids changing a path he drives by hand.
+** 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
diff --git a/todo.org b/todo.org
index b2dd369..fec2a71 100644
--- a/todo.org
+++ b/todo.org
@@ -74,6 +74,13 @@ What we're verifying: an in-session monitor fire collapses to a one-line heartbe
- Start "auto inbox zero" with an empty roam inbox, let a cycle run. Expected: one line, =inbox zero at HH:MM: nothing=.
- Add a roam inbox item, let the next cycle run. Expected: the item summarized, filed, and queued.
Expected: every empty fire is a single labelled heartbeat; every fire with real work does its full turn unchanged.
+*** Triage source activation — declared sources pulled, undeclared dormant
+What we're verifying: triage-intake pulls only the sources a project declares (spec: docs/specs/2026-07-20-triage-source-activation-spec.org).
+- In rulesets (no =:TRIAGE_SOURCES:=, no project plugin), run "triage intake". Expected: Phase 0 announces every general plugin as "inactive (undeclared)", loads zero sources, and the sweep no-ops.
+- In a project with =:TRIAGE_SOURCES: personal-gmail cmail=, run "triage intake". Expected: Phase 0 loads personal-gmail and cmail as active general sources, names the rest inactive, and scans only the two.
+- In a project with a project-specific plugin (=.ai/project-workflows/triage-intake.*.org=) and no declaration, run "triage intake". Expected: the project plugin is active by presence; general plugins stay inactive.
+- Under sentry, confirm pass 3 (triage) probe-skips in a project with no active source, and runs where one is declared.
+Expected: undeclared general plugins never scan; declared ones and project-specific ones do; sentry's triage pass activates only where a source is active.
** DONE [#B] working/ tracked-from-creation + gitignored temp/ :feature:
CLOSED: [2026-07-20 Mon]
@@ -185,12 +192,11 @@ session needs its own contract (mutation rights, async surfacing, dedup
state across runs) — same open questions inbox.org logs for unattended
auto inbox zero. Take up only if the interactive shape proves too narrow.
-** TODO [#B] Triage source activation — per-project source declaration :feature:spec:
-Spec: [[file:docs/specs/2026-07-20-triage-source-activation-spec.org][docs/specs/2026-07-20-triage-source-activation-spec.org]] (DRAFT, ID af73ef0b-cd1d-46f1-9e1d-62695733a4de).
-
-From the sentry live trial (2026-07-20): triage-intake self-activates in every project because the general (personal-account) plugins are template-synced everywhere, so sentry's pass-3 probe misfires and would pull Craig's personal inboxes into whatever project the fire runs in. Fix is an activation layer — general plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins stay active by presence. Fixes the interactive over-pull too.
+** DONE [#B] Triage source activation — per-project source declaration :feature:spec:
+CLOSED: [2026-07-20 Mon]
+Spec: [[file:docs/specs/2026-07-20-triage-source-activation-spec.org][docs/specs/2026-07-20-triage-source-activation-spec.org]] (IMPLEMENTED, ID af73ef0b-cd1d-46f1-9e1d-62695733a4de). Craig approved after his read, both open decisions resolved via cj comments; built same session.
-Awaiting Craig's deep read of the spec (DRAFT → READY gate). Two decisions still open in the spec (declaration format details, whether interactive adopts the same gate). On READY, spec-response decomposes the 5 phases into build tasks. Supersedes Fire 1's triage-probe approval-queue item, which was the narrower probe-only framing.
+From the sentry live trial (2026-07-20): triage-intake self-activated in every project because the general (personal-account) plugins are template-synced everywhere, so sentry's pass-3 probe misfired and would pull Craig's personal inboxes into whatever project the fire ran in. Fixed with an activation gate in triage-intake Phase 0 — general plugins gate on a per-project =:TRIAGE_SOURCES:= declaration; project-specific plugins stay active by presence. Applies to interactive and unattended alike. sentry pass-3 probe reads the same signal. Migration handoffs sent to home + work. Supersedes Fire 1's narrower probe-only approval-queue item.
** DONE [#B] Silent-until-signal for in-session monitor loops :feature:spec:
CLOSED: [2026-07-20 Mon]