#+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/.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///pulls --hostname \ -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:" --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 \ ... #+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. *Always run =mbsync -a= at the end of a triage-intake* so the read flags propagate to the servers before the workflow closes — don't leave them queued for the next sync, and don't ask first (Craig's standing instruction, 2026-05-12). The "conflicting changes (N,M)" notices mbsync prints when both sides touched flags are normal — it resolves them. (This overrides the older "ask before syncing" posture inherited from =summarize-emails.org=.) 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, etc.). - *Synced* — confirm =mbsync -a= ran and note any non-trivial output. * 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.).