aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows/triage-intake.org
blob: a0657b5c0040ea9056f05ec56f482c03d6ac9112 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
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.).