aboutsummaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-19 16:14:30 -0600
committerCraig Jennings <c@cjennings.net>2026-02-19 16:14:30 -0600
commitb104dde43fcc717681a8733a977eb528c60eb13f (patch)
tree08909de273851264489aecfb0eed5e57503d387e /docs
parentf429b540fe14c88a07ad5685919d8e18d6ec0352 (diff)
downloadarchangel-b104dde43fcc717681a8733a977eb528c60eb13f.tar.gz
archangel-b104dde43fcc717681a8733a977eb528c60eb13f.zip
docs: sync templates, process announcements, update todo headers
Synced workflows, scripts, and protocols from templates. Processed 4 announcements (calendar cross-visibility, gcalcli, open-tasks, summarize-emails). Renamed todo.org headers to project-named convention.
Diffstat (limited to 'docs')
-rw-r--r--docs/announcements/inbox-gitkeep.txt10
-rw-r--r--docs/notes.org26
-rwxr-xr-xdocs/scripts/maildir-flag-manager.py345
-rw-r--r--docs/workflows/add-calendar-event.org208
-rw-r--r--docs/workflows/delete-calendar-event.org217
-rw-r--r--docs/workflows/edit-calendar-event.org255
-rw-r--r--docs/workflows/open-tasks.org151
-rw-r--r--docs/workflows/read-calendar-events.org214
-rw-r--r--docs/workflows/summarize-emails.org237
9 files changed, 1663 insertions, 0 deletions
diff --git a/docs/announcements/inbox-gitkeep.txt b/docs/announcements/inbox-gitkeep.txt
new file mode 100644
index 0000000..f8946c2
--- /dev/null
+++ b/docs/announcements/inbox-gitkeep.txt
@@ -0,0 +1,10 @@
+The inbox/ directory was disappearing between sessions because git doesn't track
+empty directories. A .gitkeep file has been added to fix this.
+
+Action: If your project has an inbox/ directory, ensure it contains a .gitkeep file:
+
+ touch inbox/.gitkeep
+
+If your project doesn't have an inbox/ directory yet, create one with .gitkeep:
+
+ mkdir -p inbox && touch inbox/.gitkeep
diff --git a/docs/notes.org b/docs/notes.org
index ec6e861..1f562cd 100644
--- a/docs/notes.org
+++ b/docs/notes.org
@@ -327,6 +327,32 @@ Each entry should use this format:
** Session Entries
+*** 2026-02-19 Thu @ 16:11-16:14 -0600
+
+*Status:* COMPLETE
+
+*What We Completed:*
+- Template sync from claude-templates (protocols, workflows, scripts, announcements)
+- Processed 4 announcements:
+ 1. Calendar workflows updated with cross-calendar visibility
+ 2. gcalcli now available for Google Calendar CLI access
+ 3. New open-tasks workflow — updated todo.org headers to project-named convention (Archangel Open Work / Archangel Resolved)
+ 4. New summarize-emails workflow added
+- New workflows synced: add-calendar-event, delete-calendar-event, edit-calendar-event, read-calendar-events, open-tasks, summarize-emails
+- New script synced: maildir-flag-manager.py
+
+*Files Modified:*
+- [[file:../todo.org][todo.org]] — renamed headers to project-named convention
+
+*Files Added (from template):*
+- docs/workflows/{add,delete,edit,read}-calendar-event.org
+- docs/workflows/open-tasks.org, summarize-emails.org
+- docs/scripts/maildir-flag-manager.py
+- docs/announcements/inbox-gitkeep.txt
+
+*Outstanding Reminder:*
+- [2026-02-12] Verify TrueNAS ISO hash — still pending
+
*** 2026-02-12 Thu @ 08:23-16:08 -0600
*Status:* COMPLETE
diff --git a/docs/scripts/maildir-flag-manager.py b/docs/scripts/maildir-flag-manager.py
new file mode 100755
index 0000000..9c4a59c
--- /dev/null
+++ b/docs/scripts/maildir-flag-manager.py
@@ -0,0 +1,345 @@
+#!/usr/bin/env python3
+"""Manage maildir flags (read, starred) across email accounts.
+
+Uses atomic os.rename() for flag operations directly on maildir files.
+Safer and more reliable than shell-based approaches (zsh loses PATH in
+while-read loops, piped mu move silently fails).
+
+Supports the same flag semantics as mu4e: maildir files in new/ are moved
+to cur/ when the Seen flag is added, and flag changes are persisted to the
+filesystem so mbsync picks them up on the next sync.
+
+Usage:
+ # Mark all unread INBOX emails as read
+ maildir-flag-manager.py mark-read
+
+ # Mark specific emails as read (by path)
+ maildir-flag-manager.py mark-read /path/to/message1 /path/to/message2
+
+ # Mark all unread INBOX emails as read, then reindex mu
+ maildir-flag-manager.py mark-read --reindex
+
+ # Star specific emails (by path)
+ maildir-flag-manager.py star /path/to/message1 /path/to/message2
+
+ # Star and mark read
+ maildir-flag-manager.py star --mark-read /path/to/message1
+
+ # Dry run — show what would change without modifying anything
+ maildir-flag-manager.py mark-read --dry-run
+"""
+
+import argparse
+import os
+import shutil
+import subprocess
+import sys
+
+
+# ---------------------------------------------------------------------------
+# Configuration
+# ---------------------------------------------------------------------------
+
+MAILDIR_ACCOUNTS = {
+ 'gmail': os.path.expanduser('~/.mail/gmail/INBOX'),
+ 'cmail': os.path.expanduser('~/.mail/cmail/Inbox'),
+}
+
+
+# ---------------------------------------------------------------------------
+# Core flag operations
+# ---------------------------------------------------------------------------
+
+def parse_maildir_flags(filename):
+ """Extract flags from a maildir filename.
+
+ Maildir filenames follow the pattern: unique:2,FLAGS
+ where FLAGS is a sorted string of flag characters (e.g., "FS" for
+ Flagged+Seen).
+
+ Returns (base, flags_string). If no flags section, returns (filename, '').
+ """
+ if ':2,' in filename:
+ base, flags = filename.rsplit(':2,', 1)
+ return base, flags
+ return filename, ''
+
+
+def build_flagged_filename(filename, new_flags):
+ """Build a maildir filename with the given flags.
+
+ Flags are always sorted alphabetically per maildir spec.
+ """
+ base, _ = parse_maildir_flags(filename)
+ sorted_flags = ''.join(sorted(set(new_flags)))
+ return f"{base}:2,{sorted_flags}"
+
+
+def rename_with_flag(file_path, flag, dry_run=False):
+ """Add a flag to a single maildir message file via atomic rename.
+
+ Handles moving from new/ to cur/ when adding the Seen flag.
+ Returns True if the flag was added, False if already present.
+ """
+ dirname = os.path.dirname(file_path)
+ filename = os.path.basename(file_path)
+ maildir_root = os.path.dirname(dirname)
+ subdir = os.path.basename(dirname)
+
+ _, current_flags = parse_maildir_flags(filename)
+
+ if flag in current_flags:
+ return False
+
+ new_flags = current_flags + flag
+ new_filename = build_flagged_filename(filename, new_flags)
+
+ # Messages with the Seen flag belong in cur/, not new/
+ if 'S' in new_flags and subdir == 'new':
+ target_dir = os.path.join(maildir_root, 'cur')
+ else:
+ target_dir = dirname
+
+ new_path = os.path.join(target_dir, new_filename)
+
+ if dry_run:
+ return True
+
+ os.rename(file_path, new_path)
+ return True
+
+
+def process_maildir(maildir_path, flag, dry_run=False):
+ """Add a flag to all messages in a maildir that don't have it.
+
+ Scans both new/ and cur/ subdirectories.
+ Returns (changed_count, skipped_count, error_count).
+ """
+ if not os.path.isdir(maildir_path):
+ print(f" Skipping {maildir_path} (not found)", file=sys.stderr)
+ return 0, 0, 0
+
+ changed = 0
+ skipped = 0
+ errors = 0
+
+ for subdir in ('new', 'cur'):
+ subdir_path = os.path.join(maildir_path, subdir)
+ if not os.path.isdir(subdir_path):
+ continue
+
+ for filename in os.listdir(subdir_path):
+ file_path = os.path.join(subdir_path, filename)
+ if not os.path.isfile(file_path):
+ continue
+
+ try:
+ if rename_with_flag(file_path, flag, dry_run):
+ changed += 1
+ else:
+ skipped += 1
+ except Exception as e:
+ print(f" Error on {filename}: {e}", file=sys.stderr)
+ errors += 1
+
+ return changed, skipped, errors
+
+
+def process_specific_files(paths, flag, dry_run=False):
+ """Add a flag to specific message files by path.
+
+ Returns (changed_count, skipped_count, error_count).
+ """
+ changed = 0
+ skipped = 0
+ errors = 0
+
+ for path in paths:
+ path = os.path.abspath(path)
+ if not os.path.isfile(path):
+ print(f" File not found: {path}", file=sys.stderr)
+ errors += 1
+ continue
+
+ # Verify file is inside a maildir (parent should be cur/ or new/)
+ parent_dir = os.path.basename(os.path.dirname(path))
+ if parent_dir not in ('cur', 'new'):
+ print(f" Not in a maildir cur/ or new/ dir: {path}",
+ file=sys.stderr)
+ errors += 1
+ continue
+
+ try:
+ if rename_with_flag(path, flag, dry_run):
+ changed += 1
+ else:
+ skipped += 1
+ except Exception as e:
+ print(f" Error on {path}: {e}", file=sys.stderr)
+ errors += 1
+
+ return changed, skipped, errors
+
+
+def reindex_mu():
+ """Run mu index to update the database after flag changes."""
+ mu_path = shutil.which('mu')
+ if not mu_path:
+ print("Warning: mu not found in PATH, skipping reindex",
+ file=sys.stderr)
+ return False
+
+ try:
+ result = subprocess.run(
+ [mu_path, 'index'],
+ capture_output=True, text=True, timeout=120
+ )
+ if result.returncode == 0:
+ print("mu index: database updated")
+ return True
+ else:
+ print(f"mu index failed: {result.stderr}", file=sys.stderr)
+ return False
+ except subprocess.TimeoutExpired:
+ print("mu index timed out after 120s", file=sys.stderr)
+ return False
+
+
+# ---------------------------------------------------------------------------
+# Commands
+# ---------------------------------------------------------------------------
+
+def cmd_mark_read(args):
+ """Mark emails as read (add Seen flag)."""
+ flag = 'S'
+ action = "Marking as read"
+ if args.dry_run:
+ action = "Would mark as read"
+
+ total_changed = 0
+ total_skipped = 0
+ total_errors = 0
+
+ if args.paths:
+ print(f"{action}: {len(args.paths)} specific message(s)")
+ c, s, e = process_specific_files(args.paths, flag, args.dry_run)
+ total_changed += c
+ total_skipped += s
+ total_errors += e
+ else:
+ for name, maildir_path in MAILDIR_ACCOUNTS.items():
+ print(f"{action} in {name} ({maildir_path})")
+ c, s, e = process_maildir(maildir_path, flag, args.dry_run)
+ total_changed += c
+ total_skipped += s
+ total_errors += e
+ if c > 0:
+ print(f" {c} message(s) marked as read")
+ if s > 0:
+ print(f" {s} already read")
+
+ print(f"\nTotal: {total_changed} changed, {total_skipped} already set, "
+ f"{total_errors} errors")
+
+ if args.reindex and not args.dry_run and total_changed > 0:
+ reindex_mu()
+
+ return 0 if total_errors == 0 else 1
+
+
+def cmd_star(args):
+ """Star/flag emails (add Flagged flag)."""
+ flag = 'F'
+ action = "Starring"
+ if args.dry_run:
+ action = "Would star"
+
+ if not args.paths:
+ print("Error: star requires specific message paths", file=sys.stderr)
+ return 1
+
+ print(f"{action}: {len(args.paths)} message(s)")
+ total_changed = 0
+ total_skipped = 0
+ total_errors = 0
+
+ c, s, e = process_specific_files(args.paths, flag, args.dry_run)
+ total_changed += c
+ total_skipped += s
+ total_errors += e
+
+ # Also mark as read if requested
+ if args.mark_read:
+ print("Also marking as read...")
+ c2, _, e2 = process_specific_files(args.paths, 'S', args.dry_run)
+ total_changed += c2
+ total_errors += e2
+
+ print(f"\nTotal: {total_changed} flag(s) changed, {total_skipped} already set, "
+ f"{total_errors} errors")
+
+ if args.reindex and not args.dry_run and total_changed > 0:
+ reindex_mu()
+
+ return 0 if total_errors == 0 else 1
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+def main():
+ parser = argparse.ArgumentParser(
+ description="Manage maildir flags (read, starred) across email accounts."
+ )
+ subparsers = parser.add_subparsers(dest='command', required=True)
+
+ # mark-read
+ p_read = subparsers.add_parser(
+ 'mark-read',
+ help="Mark emails as read (add Seen flag)"
+ )
+ p_read.add_argument(
+ 'paths', nargs='*',
+ help="Specific message file paths. If omitted, marks all unread "
+ "messages in configured INBOX maildirs."
+ )
+ p_read.add_argument(
+ '--reindex', action='store_true',
+ help="Run mu index after changing flags"
+ )
+ p_read.add_argument(
+ '--dry-run', action='store_true',
+ help="Show what would change without modifying anything"
+ )
+ p_read.set_defaults(func=cmd_mark_read)
+
+ # star
+ p_star = subparsers.add_parser(
+ 'star',
+ help="Star/flag emails (add Flagged flag)"
+ )
+ p_star.add_argument(
+ 'paths', nargs='+',
+ help="Message file paths to star"
+ )
+ p_star.add_argument(
+ '--mark-read', action='store_true',
+ help="Also mark starred messages as read"
+ )
+ p_star.add_argument(
+ '--reindex', action='store_true',
+ help="Run mu index after changing flags"
+ )
+ p_star.add_argument(
+ '--dry-run', action='store_true',
+ help="Show what would change without modifying anything"
+ )
+ p_star.set_defaults(func=cmd_star)
+
+ args = parser.parse_args()
+ sys.exit(args.func(args))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/docs/workflows/add-calendar-event.org b/docs/workflows/add-calendar-event.org
new file mode 100644
index 0000000..713a54d
--- /dev/null
+++ b/docs/workflows/add-calendar-event.org
@@ -0,0 +1,208 @@
+#+TITLE: Add Calendar Event Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-02-01
+
+* Overview
+
+Workflow for creating calendar events via gcalcli with natural language input.
+
+* Triggers
+
+- "create an event"
+- "add appointment"
+- "schedule a meeting"
+- "add to my calendar"
+- "calendar event for..."
+
+* Prerequisites
+
+- gcalcli installed and authenticated
+- Google Calendar API credentials configured
+- Test with =gcalcli list= to verify authentication
+
+* CRITICAL: Check All Calendars Before Scheduling
+
+Before creating any event, ALWAYS check for conflicts across ALL calendars by querying the emacs org calendar files:
+
+#+begin_src bash
+grep "2026-02-18" ~/.emacs.d/data/gcal.org # Google calendar
+grep "2026-02-18" ~/.emacs.d/data/dcal.org # DeepSat work calendar
+grep "2026-02-18" ~/.emacs.d/data/pcal.org # Proton calendar
+#+end_src
+
+| File | Calendar |
+|----------+---------------------------|
+| gcal.org | Craig (Google) |
+| dcal.org | Craig DeepSat (work) |
+| pcal.org | Craig Proton |
+
+gcalcli only sees Google calendars — it will miss work and Proton events. Always verify the time slot is free across all three before creating.
+
+To *create* events, use gcalcli with =--calendar "Craig"= (Google).
+
+* Workflow Steps
+
+** 1. Parse Natural Language Input
+
+Interpret the user's request to extract:
+- Event title
+- Date/time (natural language like "tomorrow 3pm", "next Tuesday at 2")
+- Any mentioned location
+- Any mentioned description
+
+Examples:
+- "Create an event tomorrow at 5pm called Grocery Shopping"
+- "Add a meeting with Bob on Friday at 10am"
+- "Schedule dentist appointment next Wednesday at 2pm at Downtown Dental"
+
+** 2. Apply Defaults
+
+| Field | Default Value |
+|------------+----------------------------------|
+| Calendar | Craig (default Google Calendar) |
+| Reminders | 5 minutes before, at event time |
+| Duration | NONE - always ask user |
+| Location | None (optional) |
+
+** 3. Gather Missing Information
+
+*Always ask for:*
+- Duration (required, no default)
+
+*Ask if relevant:*
+- Location (if not provided and seems like an in-person event)
+
+*Never assume:*
+- Duration - this must always be explicitly confirmed
+
+** 4. Show Event Summary
+
+Present the event in plain English (NOT the gcalcli command):
+
+#+begin_example
+Event: Grocery Shopping
+When: Tomorrow (Feb 2) at 5:00 PM
+Duration: 1 hour
+Location: (none)
+Reminders: 5 min before, at event time
+Calendar: Personal
+#+end_example
+
+** 5. Explicit Confirmation
+
+Ask: "Create this event? (yes/no)"
+
+*Do NOT create the event until user confirms.*
+
+** 6. Execute
+
+Once confirmed, run:
+
+#+begin_src bash
+gcalcli --calendar "Calendar Name" add \
+ --title "Event Title" \
+ --when "date and time" \
+ --duration MINUTES \
+ --where "Location" \
+ --description "Description" \
+ --reminder 5 \
+ --reminder 0 \
+ --noprompt
+#+end_src
+
+** 7. Verify
+
+Confirm the event was created by searching:
+
+#+begin_src bash
+gcalcli --calendar "Calendar Name" search "Event Title"
+#+end_src
+
+Report success or failure to user.
+
+* Calendars
+
+| Calendar | Access | Notes |
+|---------------------------+--------+--------------------------------|
+| Craig | owner | Default — use for most events |
+| Christine | owner | Christine's calendar |
+| Todoist | owner | Todoist integration |
+| Craig Jennings (TripIt) | reader | View only, no create |
+| Holidays in United States | reader | View only |
+| Craig Proton | reader | View only (no API access) |
+
+Use =--calendar "Craig"= to specify (default for adding events).
+
+* gcalcli Command Reference
+
+** Add Event
+
+#+begin_src bash
+gcalcli add \
+ --calendar "My Calendar" \
+ --title "Event Title" \
+ --when "tomorrow 3pm" \
+ --duration 60 \
+ --where "123 Main St" \
+ --description "Notes" \
+ --reminder 5 \
+ --reminder 0 \
+ --noprompt
+#+end_src
+
+** Quick Add (Natural Language)
+
+#+begin_src bash
+gcalcli --calendar "My Calendar" quick "Dinner with Eric 7pm tomorrow"
+#+end_src
+
+** Key Options
+
+| Option | Description |
+|---------------+-------------------------------------|
+| --calendar | Calendar name or ID |
+| --title | Event title |
+| --when | Date/time (natural language OK) |
+| --duration | Length in minutes |
+| --where | Location |
+| --description | Event notes |
+| --reminder | Minutes before (can use multiple) |
+| --allday | Create all-day event |
+| --noprompt | Skip interactive confirmation |
+
+* Time Formats
+
+gcalcli accepts natural language times:
+- "tomorrow 3pm"
+- "next Tuesday at 2"
+- "2026-02-15 14:00"
+- "Feb 15 2pm"
+- "today 5pm"
+
+* Duration Shortcuts
+
+| Input | Minutes |
+|--------+---------|
+| 30m | 30 |
+| 1h | 60 |
+| 1.5h | 90 |
+| 2h | 120 |
+| 90 | 90 |
+
+* Error Handling
+
+** Authentication Error
+Run =gcalcli init= to re-authenticate.
+
+** Calendar Not Found
+Check available calendars with =gcalcli list=.
+
+** Invalid Time Format
+Use explicit date format: =YYYY-MM-DD HH:MM=
+
+* Related
+
+- [[file:read-calendar-events.org][Read Calendar Events]] - view events
+- [[file:edit-calendar-event.org][Edit Calendar Event]] - modify events
+- [[file:delete-calendar-event.org][Delete Calendar Event]] - remove events
+- [[file:../calendar-api-research.org][Calendar API Research]] - gcalcli reference
diff --git a/docs/workflows/delete-calendar-event.org b/docs/workflows/delete-calendar-event.org
new file mode 100644
index 0000000..46c5cad
--- /dev/null
+++ b/docs/workflows/delete-calendar-event.org
@@ -0,0 +1,217 @@
+#+TITLE: Delete Calendar Event Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-02-01
+
+* Overview
+
+Workflow for deleting calendar events via gcalcli with explicit confirmation.
+
+* Triggers
+
+- "delete the meeting"
+- "cancel my appointment"
+- "remove the event"
+- "clear my calendar for..."
+
+* Prerequisites
+
+- gcalcli installed and authenticated
+- Event must exist on calendar
+
+* Note: Calendar Visibility
+
+Events can only be deleted from Google calendars via gcalcli. DeepSat (work) and Proton calendar events are visible in =~/.emacs.d/data/{dcal,pcal}.org= but cannot be modified from here.
+
+* Workflow Steps
+
+** 1. Parse User Request
+
+Extract:
+- Which event (title, partial match, or date hint)
+- Date context (if provided)
+
+Examples:
+- "Delete the dentist appointment" → search for "dentist"
+- "Cancel tomorrow's meeting" → search tomorrow's events
+- "Remove the 3pm call" → search by time
+
+** 2. Search for Event
+
+#+begin_src bash
+# Search by title
+gcalcli --calendar "Calendar Name" search "event title"
+
+# Or list events for a date
+gcalcli --calendar "Calendar Name" agenda "date" "date 11:59pm"
+#+end_src
+
+** 3. Handle Multiple Matches
+
+If search returns multiple events:
+
+#+begin_example
+Found 3 events matching "meeting":
+
+1. Team Meeting - Feb 3, 2026 at 9:00 AM
+2. Project Meeting - Feb 4, 2026 at 2:00 PM
+3. Client Meeting - Feb 5, 2026 at 10:00 AM
+
+Which event do you want to delete? (1-3)
+#+end_example
+
+** 4. Display Full Event Details
+
+Show the event that will be deleted:
+
+#+begin_example
+Event to Delete:
+================
+Event: Team Meeting
+When: Monday, Feb 3, 2026 at 9:00 AM
+Duration: 1 hour
+Location: Conference Room A
+Description: Weekly sync
+Calendar: Work
+#+end_example
+
+** 5. Explicit Confirmation
+
+Ask clearly:
+
+#+begin_example
+Delete this event? (yes/no)
+#+end_example
+
+*Do NOT delete until user explicitly confirms with "yes".*
+
+** 6. Execute Delete
+
+gcalcli delete requires interactive confirmation. Since Claude can't interact with the prompt, pipe "y" to confirm:
+
+#+begin_src bash
+echo "y" | gcalcli --calendar "Calendar Name" delete "Event Title"
+#+end_src
+
+Use a date range to narrow matches and avoid deleting the wrong event:
+
+#+begin_src bash
+echo "y" | gcalcli --calendar "Calendar Name" delete "Event Title" 2026-02-14 2026-02-15
+#+end_src
+
+** 7. Verify
+
+Confirm the event is gone:
+
+#+begin_src bash
+gcalcli --calendar "Calendar Name" search "Event Title"
+#+end_src
+
+Report success:
+
+#+begin_example
+Event "Team Meeting" has been deleted from your calendar.
+#+end_example
+
+* gcalcli Delete Command
+
+** Basic Delete (pipe confirmation)
+
+gcalcli delete prompts interactively, which fails in non-interactive shells. Always pipe "y" to confirm:
+
+#+begin_src bash
+echo "y" | gcalcli delete "Event Title"
+#+end_src
+
+** With Date Range (preferred — avoids accidental matches)
+
+#+begin_src bash
+echo "y" | gcalcli delete "Event Title" 2026-02-14 2026-02-15
+#+end_src
+
+** Calendar-Specific Delete
+
+#+begin_src bash
+echo "y" | gcalcli --calendar "Craig" delete "Meeting" 2026-02-14 2026-02-15
+#+end_src
+
+** Skip All Prompts (dangerous)
+
+#+begin_src bash
+gcalcli delete "Event Title" --iamaexpert
+#+end_src
+
+*Warning:* =--iamaexpert= skips all prompts and deletes every match. Avoid unless the search is guaranteed to match exactly one event.
+
+* Handling Multiple Events
+
+If the search pattern matches multiple events, gcalcli may:
+- Delete all matching events (dangerous!)
+- Prompt for each one (interactive mode)
+
+*Best practice:* Use specific titles or search first, then delete by exact match.
+
+* Recurring Events
+
+*Warning:* Deleting a recurring event deletes ALL instances.
+
+For recurring events:
+1. Warn the user that all instances will be deleted
+2. Ask for confirmation specifically mentioning "all occurrences"
+3. Consider if they only want to delete one instance (not supported by simple delete)
+
+#+begin_example
+This is a recurring event. Deleting it will remove ALL occurrences.
+
+Delete all instances of "Weekly Standup"? (yes/no)
+#+end_example
+
+* Error Handling
+
+** Event Not Found
+- Verify spelling
+- Try partial match
+- Check date range
+- May have already been deleted
+
+** Delete Failed
+- Check calendar permissions
+- Verify event exists
+- Try with --calendar flag
+
+** Wrong Event Deleted
+- Cannot undo gcalcli delete
+- Would need to recreate the event manually
+
+* Safety Considerations
+
+1. *Always show full event details* before asking for confirmation
+2. *Never delete without explicit "yes"* from user
+3. *Warn about recurring events* before deletion
+4. *Verify deletion* by searching after
+5. *Read-only calendars* (like Christine's) cannot have events deleted
+
+* Read-Only Calendars
+
+Some calendars are read-only:
+
+| Calendar | Can Delete? |
+|---------------------------+-------------|
+| Craig | Yes |
+| Christine | Yes |
+| Todoist | Yes |
+| Craig Jennings (TripIt) | No |
+| Holidays in United States | No |
+| Craig Proton | No |
+
+If user tries to delete from read-only calendar:
+
+#+begin_example
+Cannot delete from "Craig Proton" - this is a read-only calendar.
+#+end_example
+
+* Related
+
+- [[file:add-calendar-event.org][Add Calendar Event]] - create events
+- [[file:read-calendar-events.org][Read Calendar Events]] - view events
+- [[file:edit-calendar-event.org][Edit Calendar Event]] - modify events
+- [[file:../calendar-api-research.org][Calendar API Research]] - gcalcli reference
diff --git a/docs/workflows/edit-calendar-event.org b/docs/workflows/edit-calendar-event.org
new file mode 100644
index 0000000..13a80a9
--- /dev/null
+++ b/docs/workflows/edit-calendar-event.org
@@ -0,0 +1,255 @@
+#+TITLE: Edit Calendar Event Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-02-01
+
+* Overview
+
+Workflow for editing existing calendar events via gcalcli.
+
+*Note:* gcalcli's edit command is interactive. This workflow uses a delete-and-recreate approach for non-interactive editing.
+
+* Triggers
+
+- "edit the meeting"
+- "change my appointment"
+- "reschedule"
+- "update the event"
+- "move my appointment"
+
+* Prerequisites
+
+- gcalcli installed and authenticated
+- Event must exist on calendar
+
+* CRITICAL: Check All Calendars Before Rescheduling
+
+When rescheduling an event, ALWAYS check for conflicts at the new time across ALL calendars:
+
+#+begin_src bash
+grep "TARGET_DATE" ~/.emacs.d/data/gcal.org # Google calendar
+grep "TARGET_DATE" ~/.emacs.d/data/dcal.org # DeepSat work calendar
+grep "TARGET_DATE" ~/.emacs.d/data/pcal.org # Proton calendar
+#+end_src
+
+gcalcli only sees Google calendars — verify the new time is free across all three files before rescheduling.
+
+* Workflow Steps
+
+** 1. Parse User Request
+
+Extract:
+- Which event (title, partial match, or date hint)
+- What to change (if mentioned)
+
+Examples:
+- "Edit the dentist appointment" → search for "dentist"
+- "Reschedule tomorrow's meeting" → search tomorrow's events
+- "Change the 3pm call to 4pm" → search by time
+
+** 2. Search for Event
+
+#+begin_src bash
+# Search by title
+gcalcli --calendar "Calendar Name" search "event title"
+
+# Or list events for a date
+gcalcli --calendar "Calendar Name" agenda "date" "date 11:59pm"
+#+end_src
+
+** 3. Handle Multiple Matches
+
+If search returns multiple events:
+
+#+begin_example
+Found 3 events matching "meeting":
+
+1. Team Meeting - Feb 3, 2026 at 9:00 AM
+2. Project Meeting - Feb 4, 2026 at 2:00 PM
+3. Client Meeting - Feb 5, 2026 at 10:00 AM
+
+Which event do you want to edit? (1-3)
+#+end_example
+
+** 4. Display Full Event Details
+
+Show the current event state:
+
+#+begin_example
+Event: Team Meeting
+When: Monday, Feb 3, 2026 at 9:00 AM
+Duration: 1 hour
+Location: Conference Room A
+Description: Weekly sync
+Reminders: 5 min, 0 min
+Calendar: Craig
+#+end_example
+
+** 5. Ask What to Change
+
+Options:
+- Title
+- Date/Time
+- Duration
+- Location
+- Description
+- Reminders
+
+Can change one or multiple fields.
+
+** 6. Show Updated Summary
+
+Before applying changes:
+
+#+begin_example
+Updated Event:
+Event: Team Standup (was: Team Meeting)
+When: Monday, Feb 3, 2026 at 9:30 AM (was: 9:00 AM)
+Duration: 30 minutes (was: 1 hour)
+Location: Conference Room A
+Description: Weekly sync
+Reminders: 5 min, 0 min
+Calendar: Craig
+
+Apply these changes? (yes/no)
+#+end_example
+
+** 7. Explicit Confirmation
+
+*Do NOT apply changes until user confirms.*
+
+** 8. Execute Edit (Delete + Recreate)
+
+Since gcalcli edit is interactive, use delete + add:
+
+#+begin_src bash
+# Delete original
+gcalcli --calendar "Calendar Name" delete "Event Title" --iamaexpert
+
+# Recreate with updated fields
+gcalcli --calendar "Calendar Name" add \
+ --title "Updated Title" \
+ --when "new date/time" \
+ --duration NEW_MINUTES \
+ --where "Location" \
+ --description "Description" \
+ --reminder 5 \
+ --reminder 0 \
+ --noprompt
+#+end_src
+
+** 9. Verify
+
+Confirm the updated event exists:
+
+#+begin_src bash
+gcalcli --calendar "Calendar Name" search "Updated Title"
+#+end_src
+
+Report success or failure.
+
+* Common Edit Scenarios
+
+** Reschedule (Change Time)
+
+#+begin_example
+User: "Move my dentist appointment to 3pm"
+
+1. Search for "dentist"
+2. Show current time
+3. Confirm new time: 3pm
+4. Delete + recreate at new time
+#+end_example
+
+** Change Duration
+
+#+begin_example
+User: "Make the meeting 2 hours instead of 1"
+
+1. Find the meeting
+2. Show current duration
+3. Confirm new duration: 2 hours
+4. Delete + recreate with new duration
+#+end_example
+
+** Update Location
+
+#+begin_example
+User: "Change the meeting location to Room B"
+
+1. Find the meeting
+2. Show current location
+3. Confirm new location
+4. Delete + recreate with new location
+#+end_example
+
+** Move to Different Day
+
+#+begin_example
+User: "Move Friday's review to Monday"
+
+1. Find event on Friday
+2. Show full details
+3. Confirm new date (Monday) and time
+4. Delete + recreate on new day
+#+end_example
+
+* gcalcli Command Reference
+
+** Search
+
+#+begin_src bash
+gcalcli search "event title"
+gcalcli --calendar "Craig" search "meeting"
+#+end_src
+
+** Delete (for edit workflow)
+
+#+begin_src bash
+gcalcli --calendar "Calendar" delete "Event Title" --iamaexpert
+#+end_src
+
+** Add (recreate with edits)
+
+#+begin_src bash
+gcalcli --calendar "Calendar" add \
+ --title "Title" \
+ --when "date time" \
+ --duration MINUTES \
+ --where "Location" \
+ --description "Notes" \
+ --reminder 5 \
+ --reminder 0 \
+ --noprompt
+#+end_src
+
+* Handling Recurring Events
+
+*Warning:* The delete+recreate approach deletes ALL instances of a recurring event.
+
+For recurring events:
+1. Warn the user this will affect all instances
+2. Consider using gcalcli's interactive edit mode
+3. Or create a new single event and delete the series
+
+* Error Handling
+
+** Event Not Found
+- Verify spelling
+- Try partial match
+- Check date range
+
+** Multiple Matches
+- Show all matches
+- Ask user to select one
+- Use more specific search terms
+
+** Delete Failed
+- Event may already be deleted
+- Check calendar permissions
+
+* Related
+
+- [[file:add-calendar-event.org][Add Calendar Event]] - create events
+- [[file:read-calendar-events.org][Read Calendar Events]] - view events
+- [[file:delete-calendar-event.org][Delete Calendar Event]] - remove events
+- [[file:../calendar-api-research.org][Calendar API Research]] - gcalcli reference
diff --git a/docs/workflows/open-tasks.org b/docs/workflows/open-tasks.org
new file mode 100644
index 0000000..d93e743
--- /dev/null
+++ b/docs/workflows/open-tasks.org
@@ -0,0 +1,151 @@
+#+TITLE: List Open Tasks Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-02-12
+
+* Overview
+
+This workflow gathers, reconciles, and displays all open tasks across project sources. It ensures nothing falls through the cracks by syncing loose reminders into todo.org, flags tasks that may already be done, and presents a clean priority-grouped list for review.
+
+* When to Use This Workflow
+
+- Craig says "list open tasks" or "show me all tasks"
+- At the start of a planning or prioritization session
+- When deciding what to work on next (complements the what's-next workflow)
+- Periodically to audit and clean up stale tasks
+
+* The Workflow
+
+** Step 1: Write Session Context File
+
+Before anything else, update =docs/session-context.org= with current session state. Task review can surface decisions and status changes — capture context in case of crash.
+
+** Step 2: Gather Tasks from notes.org
+
+Read the following sections from =notes.org=:
+- *Active Reminders* — time-sensitive items, follow-ups
+- *Pending Decisions* — decisions that block work
+- *Last 2-3 Session History entries* — recent next-steps and in-progress items
+
+Extract anything that represents an open task or action item.
+
+** Step 3: Gather Tasks from todo.org
+
+Read the project's =todo.org= file (typically at project root).
+- Collect all entries under the open work header (named =* $Project Open Work=, e.g., =* Homelab Open Work=)
+- Note each task's priority ([#A], [#B], [#C]), deadline, scheduled date, and status
+- Skip anything under the resolved header (named =* $Project Resolved=)
+
+** Step 4: Reconcile — Sync Unique Tasks to todo.org
+
+Compare tasks found in notes.org (reminders, session history, pending decisions) against todo.org entries.
+
+For each task in notes.org that does NOT have a corresponding todo.org entry:
+1. Create a new =** TODO= entry in todo.org under the =* $Project Open Work= header
+2. Assign a priority based on context ([#A] if time-sensitive or blocking, [#B] if important, [#C] if low urgency)
+3. Include:
+ - =:CREATED:= property with today's date
+ - Brief description of what needs to be done
+ - Why it matters (context from the reminder or session notes)
+ - Recommended approach or next steps
+4. If a deadline exists (e.g., RMA expected date), add a =DEADLINE:= line
+
+*Do NOT remove the item from notes.org Active Reminders* — reminders serve a different purpose (surfaced at session start). The todo.org entry is for tracking and prioritization.
+
+*Judgment call:* Not every reminder needs a todo.org entry. Skip items that are:
+- Pure informational notes (e.g., "rsyncshot running with 600s timeout")
+- Waiting-for items with no action Craig can take (e.g., "package arriving Feb 25")
+- Already completed (handle in Step 5)
+
+** Step 5: Review for Completed Tasks
+
+Quickly review all open tasks and check if any appear to already be done, based on:
+- Recent session history mentioning completion
+- Context clues (e.g., "arriving Feb 7" and it's now Feb 12)
+- Work completed in previous sessions that wasn't marked done
+
+Build a list of *suspected completions* — do NOT mark them done yet. These will be confirmed with Craig in Step 7.
+
+** Step 6: Display All Open Tasks
+
+Present all open tasks grouped by priority. Format rules:
+
+- *Group by priority:* A (High), B (Medium), C (Low/Someday)
+- *Default priority:* Tasks without an explicit priority are treated as C
+- *No table structure* — use a flat bulleted list within each group
+- *Include deadlines:* If a task has a =DEADLINE:=, show it inline as =DEADLINE: <date>=
+- *Include scheduled dates:* If a task has a =SCHEDULED:=, show it inline
+- *Keep descriptions concise* — task name + one-line summary, not full details
+- *Note source* if task came from reminders only (not yet in todo.org) vs todo.org
+
+Example format:
+#+begin_example
+**Priority A (High)**
+
+- Complete Sara Essex email setup — add Google Workspace MX records, verify delivery
+- Set up Comet KVMs — remote console for TrueNAS and ratio
+- Complete UPS/TrueNAS integration — USB cable, configure shutdown threshold. DEADLINE: <2026-01-21>
+
+**Priority B (Medium)**
+
+- Design Zettelkasten architecture — resume at Question 4 (Staleness)
+- Compare Ubiquiti UTR vs open source mesh router
+
+**Priority C (Low / Someday)**
+
+- Explore Whisper-to-Claude-Code voice integration
+- Get Keychron Q6 Pro carrying case. SCHEDULED: <2026-02-07>
+#+end_example
+
+** Step 7: Confirm Suspected Completions
+
+After displaying the list, present suspected completions:
+
+#+begin_example
+These tasks may already be completed — can you confirm?
+- "OBSBOT Tiny 3 webcam arriving" — it's past the expected delivery date
+- "Sweetwater order arriving" — expected Feb 7, now Feb 12
+#+end_example
+
+For each task Craig confirms as done:
+1. Add =CLOSED: [YYYY-MM-DD Day]= timestamp (use =date= command for accuracy)
+2. Change status from =TODO= to =DONE=
+3. Add a brief completion note (when/how it was resolved)
+4. Move the entry from =* $Project Open Work= to =* $Project Resolved= in todo.org
+5. If the task also exists in Active Reminders in notes.org, remove it from there
+
+For tasks Craig says are NOT done, leave them as-is.
+
+* Resolving a Task — Format
+
+When moving a task to Resolved, it should look like this:
+
+#+begin_example
+** DONE [#A] Set up Comet KVMs
+CLOSED: [2026-02-12 Thu]
+:PROPERTIES:
+:CREATED: [2026-01-19 Mon]
+:END:
+
+Comet KVMs set up for TrueNAS and ratio. Remote BIOS/console access working.
+
+*Resolution:* Completed during Feb 12 session. Both KVMs connected and tested.
+#+end_example
+
+Key elements:
+- =DONE= replaces =TODO=
+- =CLOSED:= line with completion date
+- Original =:PROPERTIES:= block preserved
+- Brief resolution note explaining when/how
+
+* Common Mistakes
+
+1. *Marking tasks done without confirmation* — always ask Craig first
+2. *Removing reminders from notes.org when adding to todo.org* — they serve different purposes
+3. *Creating todo.org entries for pure informational reminders* — use judgment
+4. *Forgetting to update session context file* — do it in Step 1, before the review starts
+5. *Using a table for the task list* — Craig prefers flat bulleted lists for this
+6. *Not running =date=* — always check current date before evaluating deadlines or completion dates
+
+* Living Document
+
+Update this workflow as task management patterns evolve. If new task sources are added (e.g., external issue trackers, shared task lists), add them to Steps 2-3.
diff --git a/docs/workflows/read-calendar-events.org b/docs/workflows/read-calendar-events.org
new file mode 100644
index 0000000..b1b85d6
--- /dev/null
+++ b/docs/workflows/read-calendar-events.org
@@ -0,0 +1,214 @@
+#+TITLE: Read Calendar Events Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-02-01
+
+* Overview
+
+Workflow for viewing and querying calendar events via gcalcli.
+
+* Triggers
+
+- "what's on my calendar"
+- "show me appointments"
+- "summarize my schedule"
+- "what do I have today"
+- "calendar for this week"
+- "any meetings tomorrow"
+
+* Prerequisites
+
+- gcalcli installed and authenticated
+- Test with =gcalcli list= to verify authentication
+
+* CRITICAL: Cross-Calendar Visibility
+
+gcalcli only sees Google calendars. To see ALL of Craig's calendars (Google, DeepSat work, Proton), you MUST query the emacs org calendar files:
+
+#+begin_src bash
+grep "2026-02-18" ~/.emacs.d/data/gcal.org # Google calendar
+grep "2026-02-18" ~/.emacs.d/data/dcal.org # DeepSat work calendar
+grep "2026-02-18" ~/.emacs.d/data/pcal.org # Proton calendar
+#+end_src
+
+| File | Calendar |
+|----------+---------------------------|
+| gcal.org | Craig (Google) |
+| dcal.org | Craig DeepSat (work) |
+| pcal.org | Craig Proton |
+
+*ALWAYS check all three files* when checking availability or showing the schedule. gcalcli alone will miss work and Proton events, causing an incomplete picture.
+
+To *create* events, use gcalcli with =--calendar "Craig"= (Google). The org files are read-only views.
+
+* Workflow Steps
+
+** 1. Parse Time Range
+
+Interpret the user's request to determine date range:
+
+| Request | Interpretation |
+|--------------------+-------------------------------|
+| "today" | Today only |
+| "tomorrow" | Tomorrow only |
+| "this week" | Next 7 days |
+| "next week" | 7-14 days from now |
+| "this month" | Rest of current month |
+| "April 2026" | That entire month |
+| "next Tuesday" | That specific day |
+| "the 15th" | The 15th of current month |
+
+*No fixed default* - interpret from context. If unclear, ask.
+
+** 2. Determine Calendar Scope
+
+Options:
+- All calendars (default)
+- Specific calendar: use =--calendar "Name"=
+
+** 3. Query Calendar
+
+#+begin_src bash
+# Agenda view (list format)
+gcalcli agenda "start_date" "end_date"
+
+# Weekly calendar view
+gcalcli calw
+
+# Monthly calendar view
+gcalcli calm
+#+end_src
+
+** 4. Format Results
+
+Present events in a readable format:
+
+#+begin_example
+=== Tuesday, February 4, 2026 ===
+
+9:00 AM - 10:00 AM Team Standup
+ Location: Conference Room A
+
+2:00 PM - 3:00 PM Dentist Appointment
+ Location: Downtown Dental
+
+=== Wednesday, February 5, 2026 ===
+
+(No events)
+
+=== Thursday, February 6, 2026 ===
+
+10:00 AM - 11:30 AM Project Review
+ Location: Zoom
+#+end_example
+
+** 5. Summarize
+
+Provide a brief summary:
+- Total number of events
+- Busy days vs free days
+- Any all-day events
+- Conflicts (if any)
+
+* gcalcli Command Reference
+
+** Agenda View
+
+#+begin_src bash
+# Default agenda (next few days)
+gcalcli agenda
+
+# Today only
+gcalcli agenda "today" "today 11:59pm"
+
+# This week
+gcalcli agenda "today" "+7 days"
+
+# Specific date range
+gcalcli agenda "2026-03-01" "2026-03-31"
+
+# Specific calendar
+gcalcli --calendar "Work" agenda "today" "+7 days"
+#+end_src
+
+** Calendar Views
+
+#+begin_src bash
+# Weekly calendar (visual)
+gcalcli calw
+
+# Monthly calendar (visual)
+gcalcli calm
+
+# Multiple weeks
+gcalcli calw 2 # Next 2 weeks
+#+end_src
+
+** Search
+
+#+begin_src bash
+# Search by title
+gcalcli search "meeting"
+
+# Search specific calendar
+gcalcli --calendar "Work" search "standup"
+#+end_src
+
+* Output Formats
+
+gcalcli supports different output formats:
+
+| Option | Description |
+|------------------+--------------------------------|
+| (default) | Colored terminal output |
+| --nocolor | Plain text |
+| --tsv | Tab-separated values |
+
+* Time Range Examples
+
+| User Says | gcalcli Command |
+|------------------------+----------------------------------------------|
+| "today" | agenda "today" "today 11:59pm" |
+| "tomorrow" | agenda "tomorrow" "tomorrow 11:59pm" |
+| "this week" | agenda "today" "+7 days" |
+| "next week" | agenda "+7 days" "+14 days" |
+| "February" | agenda "2026-02-01" "2026-02-28" |
+| "next 3 days" | agenda "today" "+3 days" |
+| "rest of the month" | agenda "today" "2026-02-28" |
+
+* Calendars
+
+| Calendar | Access | Notes |
+|---------------------------+--------+--------------------------------|
+| Craig | owner | Default personal calendar |
+| Christine | owner | Christine's calendar |
+| Todoist | owner | Todoist integration |
+| Craig Jennings (TripIt) | reader | View only |
+| Holidays in United States | reader | View only |
+| Craig Proton | reader | View only (no API access) |
+
+* Handling No Events
+
+If the date range has no events:
+- Confirm the range was correct
+- Mention the calendar is free
+- Offer to check a different range
+
+Example: "No events found for tomorrow (Feb 3). Your calendar is free that day."
+
+* Error Handling
+
+** No Events Found
+Not an error - calendar may simply be free.
+
+** Authentication Error
+Run =gcalcli init= to re-authenticate.
+
+** Invalid Date Range
+Use explicit dates: =YYYY-MM-DD=
+
+* Related
+
+- [[file:add-calendar-event.org][Add Calendar Event]] - create events
+- [[file:edit-calendar-event.org][Edit Calendar Event]] - modify events
+- [[file:delete-calendar-event.org][Delete Calendar Event]] - remove events
+- [[file:../calendar-api-research.org][Calendar API Research]] - gcalcli reference
diff --git a/docs/workflows/summarize-emails.org b/docs/workflows/summarize-emails.org
new file mode 100644
index 0000000..a8ab805
--- /dev/null
+++ b/docs/workflows/summarize-emails.org
@@ -0,0 +1,237 @@
+#+TITLE: Summarize Emails Workflow
+#+AUTHOR: Craig Jennings & Claude
+#+DATE: 2026-02-14
+
+* Overview
+
+This workflow filters out marketing noise and surfaces only emails that matter — messages from real people, businesses Craig works with, or anything that needs his attention. It chains together existing tools: sync-email, mu queries, the extract script, and Claude's judgment to produce a curated summary.
+
+* Problem We're Solving
+
+Craig's inbox contains a mix of important correspondence and marketing noise. Manually scanning through emails to find what matters wastes time and risks missing something important buried under promotional messages. This workflow automates the filtering and presents a concise summary of only the emails that deserve attention.
+
+* Exit Criteria
+
+Summary is complete when:
+1. All qualifying emails in the requested scope have been reviewed
+2. Summary presented to Craig, grouped by account
+3. Temp directory cleaned up
+4. Session context file updated
+
+* When to Use This Workflow
+
+When Craig says:
+- "summarize my emails", "what emails do I have", "anything important in my inbox", "email summary"
+- "any unread emails", "check my unread"
+- "any starred emails", "show flagged emails"
+- "emails from [person]", "what has [person] sent me"
+- "emails also sent to Christine"
+
+* The Workflow
+
+** Step 1: Context Hygiene
+
+Before starting, write out the session context file and ask Craig if he wants to compact first. This workflow is token-heavy (reading multiple full emails). If the context window compresses mid-workflow, we may lose important details. Writing out session context prevents this data loss.
+
+** Step 2: Parse Scope
+
+Determine the mu query from Craig's request. Supported scope types:
+
+| Scope Type | Example Request | mu Query |
+|--------------------+--------------------------------+---------------------------------------|
+| Date range | "last week", "since Monday" | =date:1w..now=, =date:2026-02-10..now= |
+| Unread only | "unread emails" | =flag:unread= |
+| Unread + date | "unread emails this week" | =flag:unread date:1w..now= |
+| Starred/flagged | "starred emails" | =flag:flagged= (with optional date) |
+| From a sender | "emails from Dan" | =from:dan= (with optional =--maxnum=) |
+| Sent to someone | "emails also sent to Christine"| =to:christine OR cc:christine= |
+
+Scopes can be combined. For example, "unread emails from Dan this week" becomes =flag:unread from:dan date:1w..now=.
+
+If no scope is provided or it's ambiguous, *ask Craig* before querying.
+
+** Step 3: Offer to Sync
+
+Ask Craig if he wants to sync first (=mbsync -a && mu index=). Don't auto-sync. If Craig confirms, run the [[file:sync-email.org][sync-email workflow]].
+
+** Step 4: Query mu
+
+Append =NOT flag:list= to the query to exclude emails with List-* headers (catches most mailing list / marketing / bulk mail).
+
+#+begin_src bash
+mu find --sortfield=date --reverse --fields="d f t s l" [query] NOT flag:list
+#+end_src
+
+Output fields: date, from, to, subject, path. Sorted by date, newest first.
+
+** Step 5: Copy Qualifying Emails to Temp Directory
+
+Create an isolated temp directory for the summary work:
+
+#+begin_src bash
+mkdir -p ./tmp/email-summary-YYYY-MM-DD/
+#+end_src
+
+Copy the EML files from their maildir paths into this directory.
+
+*CRITICAL: Copy FROM =~/.mail/=, never modify =~/.mail/=.*
+
+#+begin_src bash
+cp ~/.mail/gmail/INBOX/cur/message.eml ./tmp/email-summary-YYYY-MM-DD/
+#+end_src
+
+** Step 6: Second-Pass Header Inspection
+
+For each copied email, check headers for additional marketing signals that mu's =flag:list= might miss. Discard emails that match any of:
+
+*** Bulk Sender Tools
+=X-Mailer= or =X-Mailtool= containing: Mailchimp, ExactTarget, Salesforce, SendGrid, Constant Contact, Campaign Monitor, HubSpot, Marketo, Brevo, Klaviyo
+
+*** Bulk Precedence
+=Precedence: bulk= or =Precedence: list=
+
+*** Bulk Sender Patterns
+=X-PM-Message-Id= patterns typical of bulk senders
+
+*** Marketing From Addresses
+From address matching: =noreply@=, =no-reply@=, =newsletter@=, =marketing@=, =promotions@=
+
+** Step 7: Verify Addressed to Craig
+
+Check To/CC headers contain one of Craig's addresses:
+- =craigmartinjennings@gmail.com=
+- =c@cjennings.net=
+
+Discard BCC-only marketing blasts where Craig isn't in To/CC.
+
+** Step 8: Run Extract Script on Survivors
+
+Use =eml-view-and-extract-attachments.py= in stdout mode (no =--output-dir=) to read each email's content:
+
+#+begin_src bash
+python3 docs/scripts/eml-view-and-extract-attachments.py ./tmp/email-summary-YYYY-MM-DD/message.eml
+#+end_src
+
+This prints headers and body text to stdout without creating any files in the project.
+
+** Step 9: Triage and Summarize
+
+For each email, apply judgment:
+- *Clearly needs Craig's attention* → summarize it (who, what, any action needed)
+- *Unsure whether important* → summarize it with a note about why it might matter
+- *Clearly unimportant* (automated notifications, receipts for known purchases, etc.) → mention it briefly but don't summarize in detail
+
+** Step 10: Present Summary
+
+Group by account (gmail / cmail). For each email show:
+- From, Subject, Date
+- Brief summary of content and any action needed
+- Flag anything time-sensitive
+
+Example output format:
+
+#+begin_example
+** Gmail
+
+1. From: Dan Smith | Subject: Project update | Date: Feb 14
+ Dan is asking about the timeline for the next milestone. Needs a reply.
+
+2. From: Dr. Lee's Office | Subject: Appointment confirmation | Date: Feb 13
+ Appointment confirmed for Feb 20 at 2pm. No action needed.
+
+** cmail
+
+1. From: Christine | Subject: Weekend plans | Date: Feb 14
+ Asking about Saturday dinner. Needs a reply.
+
+** Skipped (not important)
+- Order confirmation from Amazon (Feb 13)
+- GitHub notification: CI passed (Feb 14)
+#+end_example
+
+** Step 11: Clean Up
+
+Remove the temp directory:
+
+#+begin_src bash
+rm -rf ./tmp/email-summary-YYYY-MM-DD/
+#+end_src
+
+If =./tmp/= is now empty, remove it too.
+
+** Step 12: Post-Summary Actions
+
+After presenting the summary, ask Craig if he wants to:
+
+*** Star emails
+
+Star specific emails by passing their maildir paths:
+
+#+begin_src bash
+python3 docs/scripts/maildir-flag-manager.py star --reindex /path/to/message1 /path/to/message2
+#+end_src
+
+To also mark starred emails as read in one step:
+
+#+begin_src bash
+python3 docs/scripts/maildir-flag-manager.py star --mark-read --reindex /path/to/message1
+#+end_src
+
+*** Mark reviewed emails as read
+
+Mark all unread INBOX emails as read across both accounts:
+
+#+begin_src bash
+python3 docs/scripts/maildir-flag-manager.py mark-read --reindex
+#+end_src
+
+Or mark specific emails as read:
+
+#+begin_src bash
+python3 docs/scripts/maildir-flag-manager.py mark-read --reindex /path/to/message1 /path/to/message2
+#+end_src
+
+Use =--dry-run= to preview what would change without modifying anything.
+
+The script uses atomic =os.rename()= directly on maildir files — the same mechanism mu4e uses. Flag changes are persisted to the filesystem so mbsync picks them up on the next sync.
+
+*** Delete emails (future)
+=mu= supports =mu remove= to delete messages from the filesystem and database. Not yet integrated into this workflow — explore when ready.
+
+** Step 13: Context Hygiene (Completion)
+
+Write out session-context.org again after the summary is presented, capturing what was reviewed and any action items identified.
+
+* Principles
+
+- *=maildir-flag-manager.py= for flag changes* — use the script for mark-read and star operations; it uses atomic =os.rename()= on maildir files (same mechanism as mu4e) and mbsync syncs changes on next run
+- *Ask before syncing* — don't auto-sync; Craig may have already synced or may not want to wait
+- *Ask before querying* — if scope is ambiguous, clarify rather than guess
+- *Filter aggressively, surface generously* — when in doubt about whether an email is marketing, filter it out; when in doubt about whether it's important, include it in the summary
+- *One pass through the extract script* — don't re-read emails; read once and summarize
+- *Stdout mode only* — use the extract script without =--output-dir= to avoid creating files in the project
+- *Clean up always* — remove the temp directory even if errors occur partway through
+
+* Tools Reference
+
+| Tool | Purpose |
+|-------------------------------------+--------------------------------------|
+| mbsync / mu index | Sync and index mail |
+| mu find | Query maildir for matching emails |
+| eml-view-and-extract-attachments.py | Read email content (stdout mode) |
+| maildir-flag-manager.py | Mark read, star (batch flag changes) |
+
+* Files Referenced
+
+| File | Purpose |
+|------------------------------------------------+-------------------------|
+| [[file:sync-email.org][docs/workflows/sync-email.org]] | Sync step |
+| [[file:find-email.org][docs/workflows/find-email.org]] | mu query patterns |
+| docs/scripts/eml-view-and-extract-attachments.py | Extract script |
+| docs/scripts/maildir-flag-manager.py | Flag management script |
+| =~/.mail/gmail/= | Gmail maildir (READ ONLY) |
+| =~/.mail/cmail/= | cmail maildir (READ ONLY) |
+
+* Living Document
+
+Update this workflow as we discover new marketing patterns to filter, useful query combinations, or improvements to the summary format.