aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-11 18:39:02 -0500
committerCraig Jennings <c@cjennings.net>2026-05-11 18:39:02 -0500
commit7d9554cec5334c065dfe585d588f948187d03203 (patch)
treef6186b3313920ffbe872f0b095c9d5b9c87b93df
parent921b29847785d46766d00c26f72771daed6cbc9b (diff)
downloadrulesets-7d9554cec5334c065dfe585d588f948187d03203.tar.gz
rulesets-7d9554cec5334c065dfe585d588f948187d03203.zip
chore(ai): sync triage-intake workflow from claude-templates
New on-demand triage-intake workflow. It scans every inbox source (the three mail accounts, Slack, Linear, open PRs, both calendars, recent todo.org edits), surfaces what moved, runs the Linear Dev-Review sweep, and marks all unread INBOX mail plus every touched Slack conversation read. Also registered in INDEX.org, and the stale triage-intake reference dropped from wrap-it-up.org.
-rw-r--r--.ai/workflows/INDEX.org2
-rw-r--r--.ai/workflows/triage-intake.org122
-rw-r--r--.ai/workflows/wrap-it-up.org2
3 files changed, 124 insertions, 2 deletions
diff --git a/.ai/workflows/INDEX.org b/.ai/workflows/INDEX.org
index 7349d74..de1737b 100644
--- a/.ai/workflows/INDEX.org
+++ b/.ai/workflows/INDEX.org
@@ -29,6 +29,8 @@ This index must list every =.org= file in =.ai/workflows/= except this one. Star
- =daily-prep.org= — prep brief for the next workday. Two modes: full-prep (default) or standup-only.
- Full-prep triggers: "let's prep for tomorrow", "daily prep"
- Standup-only triggers: "what's my standup report", "let's do the daily standup report", "give me the standup brief"
+- =triage-intake.org= — on-demand triage: scan every inbox source (DeepSat Gmail, personal Gmail, cmail/Proton, Slack, Linear, GitHub PRs, both calendars, recent =todo.org= edits), surface what's moved, run the Linear Dev-Review sweep, mark *all* unread INBOX email across the three accounts and every touched Slack conversation as read. Lighter scope than =daily-prep.org='s triage section. Projects that want it called from =wrap-it-up.org= (or elsewhere) can opt in via a =.ai/project-workflows/<name>.org= extension.
+ - Triggers: "do a triage intake", "triage intake", "what's moved?", "what's new?", "check for movement"
- =journal-entry.org= — capture a daily journal entry.
- Triggers: "let's do a journal entry", "create a journal entry"
- =clean-todo.org= — tidy =todo.org=: hygiene pass + =--archive-done=, then summarize. Wrap-up does this automatically; this is the manual entry point.
diff --git a/.ai/workflows/triage-intake.org b/.ai/workflows/triage-intake.org
new file mode 100644
index 0000000..a0657b5
--- /dev/null
+++ b/.ai/workflows/triage-intake.org
@@ -0,0 +1,122 @@
+#+TITLE: Triage-Intake Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-05-11
+
+* Overview
+
+On-demand triage of all inbox sources — invoked any time the user wants a "what's moved?" snapshot. Scans every source for recent activity (the three mail accounts, Slack, Linear, open PRs, both calendars, recent =todo.org= edits), surfaces what's moved and what's actionable, runs the Linear Dev-Review sweep, and clears unread state at the end — *every* unread INBOX message across all three mail accounts, plus every Slack conversation it surfaced, marked read. Same source set as =daily-prep.org='s triage section, lighter scope: no =notes.org= reading, no inbox-file processing, no meeting prep, no time-blocking, no prep-doc generation. Projects that want this workflow called from =wrap-it-up.org= (or elsewhere) opt in via a =.ai/project-workflows/<name>.org= extension; the template workflows don't call triage-intake themselves.
+
+* When to Use
+
+When the user says:
+- "do a triage intake" / "triage intake" / "triage intake now"
+- "what's moved?" / "what's new?" / "anything new since X?"
+- "check for movement"
+
+* The Workflow
+
+** Phase 1: Fan-out (one parallel batch)
+
+Issue all of these in a single parallel batch (mix of MCP and Bash calls). Each source is optional — skip with a note if unavailable in this project (no MCP configured, no Slack workspace, no GitHub-family remote, etc.). This phase *surfaces recent activity for the report*; it doesn't bound what Phase 3 marks read — Phase 3 runs its own full unread query.
+
+1. *DeepSat Gmail* (=craig.jennings@deepsat.com=) — recent messages via =mcp__google-docs-work__listMessages= with =maxResults ~25=.
+2. *Personal Gmail* (=craigmartinjennings@gmail.com=) — recent messages via =mcp__google-docs-personal__listMessages= with =maxResults ~15=.
+3. *cmail / Proton* (=c@cjennings.net=) — local Maildir at =~/.mail/cmail/=, indexed by =mu=. Mirrors the mu4e unread-cmail query bound to =C-; e c u= in =mail-config.el=:
+ #+begin_src bash
+ mu find 'maildir:/cmail/INBOX AND flag:unread AND NOT flag:trashed' \
+ --sortfield=date --reverse \
+ --fields='d f s' \
+ | head -30
+ #+end_src
+ Proton has no Gmail API, so cmail is surfaced through =mu= (the two Google accounts use the richer Gmail MCPs above). Phase 3 re-queries the full unread set across all three accounts rather than relying on this capped list.
+4. *Slack (slack-deepsat)* — =mcp__slack-deepsat__conversations_unreads= (all channels + DMs).
+5. *Linear* — =mcp__linear__list_issues= with =assignee: "me"=, limit 30, ordered by recently updated. Surface tickets touched recently (new comments, status changes, new assignments).
+6. *GitHub PRs* — for each active repo in the project, list open PRs. For GitHub Enterprise, pass =--hostname=:
+ #+begin_src bash
+ gh api repos/<owner>/<repo>/pulls --hostname <ghe-host> \
+ -q '.[] | "#\(.number) \(.state) \(.title) — by \(.user.login), updated \(.updated_at)"'
+ #+end_src
+ Active repos and the host come from =.ai/project-workflows/triage-intake.org= (per-project list); fall back to the project's current-repo origin if no list is configured. Skip if the project has no GitHub-family remote.
+7. *Calendars* — DeepSat work + personal (no cmail-associated calendar). Two parallel reads, looking at the next ~7 days:
+ - =mcp__google-docs-work__listEvents= (or the unified =mcp__google-calendar__list-events= scoped to the DeepSat account).
+ - =mcp__google-docs-personal__listEvents= for personal.
+ Surface newly-added events the user may not have noticed yet — calendar invites that landed during the session, meetings someone else scheduled, time blocks added. Highlight conflicts with existing commitments where they matter.
+8. *(Optional) =todo.org= recent edits* — =git log -5 --oneline -- todo.org= or =stat= the file's mtime, to catch in-flight edits the user made between sessions.
+
+** Phase 2: Synthesize + Linear Dev-Review sweep
+
+Group findings by source, one short line per item. For each actionable item, propose the next step (reply / move ticket / close out / etc.). Don't propose actions for noise. Distinguish:
+
+- *Movement* — something changed (reply landed, ticket moved, PR pushed). Includes the actionable subset.
+- *Noise* — promos, notifications, automated emails. Cleared by the mark-as-read pass in Phase 3, not reported individually.
+
+*** Linear Dev-Review sweep
+
+For every ticket assigned to the user with status =Dev Review= (from the Phase 1 =list_issues= output), check whether its linked PR is merged. Use the =gitBranchName= field on each ticket to find the PR on the project's remote:
+
+#+begin_src bash
+gh pr list --search "head:<gitBranchName>" --state all \
+ --json number,state,headRefName,mergedAt,title
+#+end_src
+
+Skip the PR-merge check if the project has no GitHub-family remote (a self-hosted Gitea or plain-SSH remote has no =gh= support) — in that case surface the Dev-Review tickets and ask the user to confirm merge status manually.
+
+If a Dev-Review ticket's PR is *merged*, propose a status move:
+
+- *Done* — chores, refactors, test-coverage backfills, dead-code removal, e2e-flake fixes, anything with no PM-visible behavior change. PR titles prefixed =chore:=, =test:=, =refactor:=, =docs:= almost always belong here.
+- *PM Acceptance* — real behavior fixes or new features a PM (or end user) could verify by clicking through. PR titles prefixed =fix:=, =feat:= usually belong here unless the change is invisible to users.
+
+When in doubt, ask the user per ticket — don't auto-pick. After approval, move via =mcp__linear__save_issue= with the new state. Several can run in parallel.
+
+Skip the sweep entirely if the project doesn't use Linear (personal projects, the rulesets repo, etc.).
+
+** Phase 3: Mark-as-read (at the end — not as you go)
+
+Clear unread state. Default behavior unless the user explicitly says "leave this unread."
+
+*** Email — every unread INBOX message, all three accounts
+
+All three mail accounts are synced to local Maildirs and =mu=-indexed: =~/.mail/gmail/= (personal), =~/.mail/dmail/= (DeepSat), =~/.mail/cmail/= (Proton). Phase 1 surfaces the two Google accounts through the Gmail MCPs (richer thread/label data for the report) and cmail through =mu= (no Gmail API for Proton). Mark-as-read goes through the Maildirs *uniformly* — it's the one mechanism that cleanly expresses "all unread, not just what I surfaced."
+
+1. Query every unread INBOX message across the three accounts:
+ #+begin_src bash
+ mu find 'flag:unread AND NOT flag:trashed AND (maildir:/gmail/INBOX OR maildir:/dmail/INBOX OR maildir:/cmail/INBOX)' \
+ --fields='p'
+ #+end_src
+2. Pass *all* the returned paths to the flag manager in one call:
+ #+begin_src bash
+ ~/code/rulesets/.ai/scripts/maildir-flag-manager.py mark-read --reindex \
+ <path-1> <path-2> ...
+ #+end_src
+ *Always pass explicit paths.* Never call =mark-read= with no positional args — the bare default is "mark every unread message across every configured INBOX maildir on the machine," which happens to be the same set here but becomes a footgun the moment a project's extension narrows the scope. =--reindex= re-runs =mu index= so the local index reflects the new flags. =--dry-run= previews without modifying.
+3. Flag changes land on disk; =mbsync= propagates them to the servers on its next run. If the user wants the servers updated immediately, run =mbsync -a= after — but don't auto-sync; ask first (matches =summarize-emails.org='s "ask before syncing").
+4. Trash obvious junk separately and *only with user approval* — never trash without an explicit go-ahead.
+
+*** Slack — every conversation surfaced
+
+=mcp__slack-deepsat__conversations_mark= for each conversation touched during the intake, advancing the read pointer to the latest message surfaced. Don't mark conversations the intake didn't surface.
+
+*** Skip
+
+Linear, GitHub PRs, and calendars — none has a clean "mark this read" concept that maps to inbox hygiene.
+
+** Phase 4: Report
+
+One concise summary back to the user:
+
+- *What moved (by source)* — one line per change worth knowing.
+- *Actionable* — numbered, with the proposed next step per item.
+- *Cleared* — total count of emails (across the three accounts) and Slack conversations marked read.
+- *Pending decisions* — anything needing user input before action (draft replies awaiting approval, ambiguous routing, =mbsync= sync offer, etc.).
+
+* Principles
+
+- *Read-only by default.* Triage intake surfaces and asks; it doesn't reply, move tickets, or trash. Mark-as-read is the one default exception — it's hygiene, not a state change worth a gate — and it covers *all* unread INBOX mail, not just what got surfaced in the report.
+- *Surface first, mark second.* Mark-as-read happens at the end of the workflow, not inline. If the user pivots ("never mind, leave them"), nothing's already been touched.
+- *Same sources as daily-prep, lighter scope.* This workflow exists for mid-day or on-demand checks; daily-prep is the once-a-day full version (and runs its own triage section as part of its flow). Keep the two consistent when either changes; the planned end state is daily-prep delegating its triage section to this workflow.
+- *Each source is optional.* If a project doesn't have one (no Slack workspace, no GitHub-family remote, no cmail), skip it with a one-line note and continue.
+- *Per-project extension* via =.ai/project-workflows/triage-intake.org=: override the cmail / Maildir paths and the =maildir-flag-manager= command, add project-specific sources (extra Slack workspaces, additional mail accounts, JIRA, etc.), list the GitHub repos and host to check.
+
+* Living Document
+
+Update this workflow when source MCPs or local mail tooling change (new Slack workspace, switch from =mu= to =notmuch=, different cmail account, additional Maildir, etc.).
diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org
index 849950c..5c1af26 100644
--- a/.ai/workflows/wrap-it-up.org
+++ b/.ai/workflows/wrap-it-up.org
@@ -122,8 +122,6 @@ For each result, look up the linked PR (the =gitBranchName= field on the issue m
When in doubt, ask Craig per ticket. Don't auto-pick. After Craig confirms, move via =mcp__linear__save_issue= with =state="Done"= or =state="PM Acceptance"=. Several can run in parallel.
-This step is also part of =triage-intake.org=, so during a session that already triaged it may be a no-op. Run it anyway — it's idempotent, and the sweep catches anything merged between the last triage and the wrap-up.
-
Skip the step entirely if the project doesn't use Linear (e.g. personal projects, the rulesets repo).
** Step 4: Git commit + push