aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows
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 /.ai/workflows
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.
Diffstat (limited to '.ai/workflows')
-rw-r--r--.ai/workflows/sentry.org2
-rw-r--r--.ai/workflows/triage-intake.org16
2 files changed, 11 insertions, 7 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