diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-31 11:31:44 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-31 11:31:44 -0500 |
| commit | 3640664e0fa11d7eb99c2900df57734b411e2d2b (patch) | |
| tree | ff566090a5c09fa459816d563f42caff0282f5df /claude-templates | |
| parent | 5438c3172ceba7740d28480d161dc68b1a1af4c8 (diff) | |
| download | rulesets-3640664e0fa11d7eb99c2900df57734b411e2d2b.tar.gz rulesets-3640664e0fa11d7eb99c2900df57734b411e2d2b.zip | |
refactor(workflows): restructure startup and triage-intake into reading lanes
I split each into lanes so a reader can stop at the level that answers the question: Summary for "what does this do and what does it produce", Execution for the steps to follow, Reference for examples and edge cases, History for old decisions. Both files are large enough that an agent loading them at routing time pays for context it doesn't need yet.
startup.org keeps Summary, Execution, and Reference (workflow discovery and common mistakes moved under Reference). triage-intake.org gets all four, including a History lane for its design notes. Every instruction is preserved. The triage reorder ran through a content-preservation check that compared the multiset of content lines before and after, so only heading depth and lane grouping moved. Nothing was dropped or reworded.
workflow-integrity.py now counts "Summary" as a valid orientation heading, since that's the new top section both files lead with.
This is the pilot from the codex backlog, scoped to the two largest workflows. Whether the lanes actually cut session token use gets evaluated before any wider rollout.
Diffstat (limited to 'claude-templates')
| -rw-r--r-- | claude-templates/.ai/workflows/startup.org | 16 | ||||
| -rw-r--r-- | claude-templates/.ai/workflows/triage-intake.org | 231 |
2 files changed, 137 insertions, 110 deletions
diff --git a/claude-templates/.ai/workflows/startup.org b/claude-templates/.ai/workflows/startup.org index 3d5ac66..c08bd8a 100644 --- a/claude-templates/.ai/workflows/startup.org +++ b/claude-templates/.ai/workflows/startup.org @@ -2,13 +2,19 @@ #+AUTHOR: Craig Jennings & Claude #+DATE: 2026-04-25 -* Overview +* Summary This workflow runs automatically at the beginning of EVERY session. It gives Claude project context, syncs templates, discovers available workflows, and determines session priorities. Do NOT ask Craig if he wants to run it — just execute it. The workflow is structured into four phases. *Phase A.0* is a sequential pre-flight; *Phase A and Phase B should each run as a single batch of parallel tool calls* — sending one message with multiple Bash / Read calls in it, not sequential round-trips. Phase C is interactive and runs sequentially. -* The Workflow +Quick contract — runs / produces: +- *Phase A.0* (sequential): refresh rulesets, then the project repo. +- *Phase A* (parallel batch): timestamp, session-context check, guarded =.ai/= sync, recent sessions, inbox-status, cross-agent status, notes.org, staleness, language-bundle freshness. +- *Phase B* (parallel batch): read the crash-recovery anchor if present, the recent session summaries, new inbox items, pending cross-agent messages. +- *Phase C* (interactive): surface findings, process the inbox, run project startup-extras, ask priorities. + +* Execution ** Phase A.0 — Pre-flight: refresh rulesets and project repo (sequential, runs first) @@ -169,7 +175,9 @@ This phase touches the user and runs sequentially: Rationale: User-facing work and decisions can't be parallelized — they have to happen one at a time so the user can react. -* Workflow discovery (on demand, not at startup) +* Reference + +** Workflow discovery (on demand, not at startup) Two directories hold workflows: - =.ai/workflows/= — template workflows (synced from claude-templates, never edit in project). @@ -189,7 +197,7 @@ When the user says "let's run/do the [X] workflow" (or otherwise references a wo The index is the catalog; the directory is the truth. Drift between them is a bug — catching it on demand keeps the index honest without paying the read cost on every session. -* Common Mistakes +** Common Mistakes 1. *Running Phase A sequentially.* Send all Phase A calls in one message — sequential rsync + ls + read costs round-trips for nothing. 2. *Reading the entire notes.org file* — only Project-Specific Context, Active Reminders, Pending Decisions. diff --git a/claude-templates/.ai/workflows/triage-intake.org b/claude-templates/.ai/workflows/triage-intake.org index 58b6709..cfcf9d7 100644 --- a/claude-templates/.ai/workflows/triage-intake.org +++ b/claude-templates/.ai/workflows/triage-intake.org @@ -2,7 +2,8 @@ #+AUTHOR: Craig Jennings & Claude #+DATE: 2026-05-01 -* Overview +* Summary + Lightweight, between-meetings sweep across whatever sources are plugged in — email, calendar, chat, open PRs, ticketing. Classifies what came in since the last check (Action / FYI / Noise-keep / Noise-trash), produces a single synthesized summary, and offers to execute the routine actions (trash, mark-read, star, respond, merge, attachment fetch). @@ -14,7 +15,10 @@ 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?" -* When to Use This Workflow + +Quick contract — what it does: fans out across source plugins, classifies every item into Action / FYI / Noise-keep / Noise-trash, synthesizes one deduped summary, writes each Action item to =todo.org= as a =:quick:reactive:= task, and executes star/mark-read/trash on confirmation. + +** When to Use This Workflow Trigger phrases: @@ -33,49 +37,10 @@ Typical timing: Do *not* use when running daily-prep — daily-prep already does this as Phase 3. -* Source Plugin Contract - -A source plugin is a file named =triage-intake.<source>.org=. The first dot after =triage-intake= is the engine/plugin boundary; the segment after it is the source id. Hyphens stay *inside* a segment (=triage-intake.personal-gmail.org= is engine =triage-intake=, source =personal-gmail=). Deeper dots (=triage-intake.<source>.<sub>.org=) are reserved for sub-adapters — YAGNI for now, but the namespace accommodates them at no cost. - -A plugin file declares exactly one source through a fixed shape: - -*Property drawer* on the top-level =* Source:= heading: -- =ORDER= — integer. Output ordering in the per-source breakdown (lower = earlier). -- =ENABLED= — the precondition the engine evaluates before loading the source. The source is skipped — *with an announced reason* — when it's false. Forms: =always=, a shell test (=command -v gh && gh auth status=), or =mcp <server> present=. -- =ANCHOR= — the cutoff format this source consumes: =epoch=, =iso8601=, =day=, or =none= (state-based source with no since-window — e.g. live IMAP unread, or open-PR state). The engine computes the anchor once and substitutes it in the requested format. -- =SUBAGENT_OVER= — integer. If the scan is expected to return more than this many items, dispatch a subagent for the source so its raw output stays out of main context. The subagent applies Phase B and returns buckets only, not the raw list. - -*Body sections:* -- =** Scan= — the command(s) that fetch new/unread items since =<anchor>=, emitting raw items. -- =** Classify= — the source's per-bucket bias and noise patterns. *Deltas* from the engine's shared four-bucket model below, not a re-derivation. -- =** Render= — the source's block in the Phase C summary. "Omit if empty." -- =** Actions= — the executable state-changes, one verb per line: =verb :: command template (parameterized by item id)=. - -Template: - -#+begin_example -* Source: <id> -:PROPERTIES: -:ORDER: <n> -:ENABLED: <precondition> -:ANCHOR: epoch | iso8601 | day | none -:SUBAGENT_OVER: <n> -:END: -** Scan -<command(s) that fetch new/unread items since <anchor>> +* Execution -** Classify -<bias + noise patterns; deltas from the shared four-bucket model> - -** Render -"<Source label> — N <unit>" block; omit if empty. - -** Actions -- <verb> :: <command, parameterized by <id>> -#+end_example - -* Phase 0 — Load source plugins (MANDATORY — do not skip) +** Phase 0 — Load source plugins (MANDATORY — do not skip) The engine has no sources baked in. It discovers them by globbing *two* directories, and you MUST glob *both*: @@ -106,61 +71,16 @@ Loaded 5 source plugins: If the project directory glob returns nothing, say so explicitly ("no project plugins in .ai/project-workflows/") rather than staying silent — silence is indistinguishable from forgetting to look. -* Anchor: Since When? - -The workflow needs a "scan since" timestamp. Resolution order: - -1. *Sentinel file content:* first whitespace-delimited token in =.ai/last-triage-intake= is the Phase A scan-kickoff epoch from the most recent successful run (see "Capture the Phase A timestamp" below). Most accurate. -2. *Sentinel file mtime* (back-compat): if the file exists but is empty, read its mtime — that's the older mtime-based convention that pre-dates the content-based change. Still accurate on the machine that wrote it. -3. *Most recent prep doc:* if no sentinel content or readable mtime, anchor on the latest =inbox/YYYY-MM-DD-daily-prep.org= or =daily-prep/YYYY-MM-DD-daily-prep.org= mtime. -4. *Most recent session file:* if none of the above, anchor on the most recent =.ai/sessions/= file's mtime. -5. *Session start:* fall back to the current session's start time. Last resort. - -The engine computes the anchor *once* and exposes it in every format a plugin might request (=epoch=, =iso8601=, =day=). Each plugin's =ANCHOR= field says which it consumes; the engine substitutes that form into the plugin's =<anchor>= placeholder. Sources with =ANCHOR: none= are state-based (live unread, open-PR state) and get no cutoff substituted — they report current state, and Phase B uses the anchor only to flag what's *new since* last check. - -** Capture the Phase A timestamp - -Just before issuing the Phase A batch, capture the current epoch seconds: - -#+begin_src bash -PHASE_A_TS=$(date +%s) -#+end_src - -Hold this value through Phases B, C, and D. At end of run, *write* the captured timestamp into the sentinel's content (not its mtime): - -#+begin_src bash -echo "$PHASE_A_TS $(date -d "@$PHASE_A_TS" '+%Y-%m-%d %H:%M:%S %z')" > .ai/last-triage-intake -#+end_src - -The file ends up with a single line like =1778683109 2026-05-13 09:38:29 -0500= — epoch first (machine-readable, parsed by reading the first token), human-readable timestamp second. - -*Why content, not mtime:* the sentinel is checked into git. Git tracks content, not mtime, so an mtime-based sentinel is per-machine: one machine's anchor stays on that machine; a fresh clone gets the file but the mtime is whenever the clone happened, not the actual triage time. Writing the epoch as content means the anchor travels with the repo and stays accurate after a fetch + pull on any machine. - -*Why Phase A and not end-of-run:* Phase A runs at one moment, but Phases B-D may take 5-30 minutes. Items posted to any source /during/ Phases B-D land between the Phase A scan time and the eventual end-of-run time. If the sentinel were set to the end-of-run time, those items would silently fall through the cracks: the next triage's Phase A would skip the gap window and never see them. Anchoring the sentinel to Phase A's scan time guarantees the next run's window starts where this run's window ended, with zero gap. -** Reading the sentinel +** Approach: Phases A → D -When the workflow needs the anchor at the start of a new run: - -#+begin_src bash -# Content-first, mtime-fallback. -ANCHOR_EPOCH=$(awk 'NR==1 {print $1; exit}' .ai/last-triage-intake 2>/dev/null) -if [ -z "$ANCHOR_EPOCH" ] && [ -f .ai/last-triage-intake ]; then - ANCHOR_EPOCH=$(stat -c %Y .ai/last-triage-intake) -fi -#+end_src - -If both fail, fall through to the resolution order above (prep doc → session file → session start). - -* Approach: Phases A → D - -** Phase A: Fan-out (one parallel batch) +*** Phase A: Fan-out (one parallel batch) Issue every enabled source's =Scan= command in a single message, with the anchor substituted in each source's declared format. They have no dependencies and benefit from running concurrently. Per-source subagent escalation: if a source's scan is expected to return more than its =SUBAGENT_OVER= count (e.g. personal Gmail after a multi-day gap), dispatch a subagent for that source. The subagent applies Phase B classification and returns the synthesized buckets, not the raw item list. -** Phase B: Classify per source (shared four-bucket model) +*** Phase B: Classify per source (shared four-bucket model) Every item lands in one bucket. Plugins refine these with source-specific bias and noise patterns in their =Classify= section; they do not redefine the buckets. @@ -171,7 +91,7 @@ Every item lands in one bucket. Plugins refine these with source-specific bias a Per-source bias (a work email account leans keep for audit value; a personal account leans trash on high noise volume) lives in each plugin's =Classify= section. Read it from there; don't re-derive it here. -** Phase C: Synthesize a single summary +*** Phase C: Synthesize a single summary One markdown summary surfaced inline to Craig. Order: @@ -181,7 +101,7 @@ One markdown summary surfaced inline to Craig. Order: Format target: scannable in 30 seconds, full read in 2 minutes. Don't pad. If a source returned zero items, write "Calendar — quiet" or "PRs — nothing new since 14:30 yesterday." -*** Sub-step: write each Action item into =todo.org= as its own =:quick:= task +**** Sub-step: write each Action item into =todo.org= as its own =:quick:= task After surfacing the summary inline, append every Action item — regardless of source — to =todo.org= as its own top-level =** TODO= heading carrying the =:quick:= tag plus =:reactive:= and any relevant person/entity tag. @@ -190,9 +110,9 @@ Each Action item is one task. Don't group items by source under =** Email Respon Format: #+begin_example -** TODO [#B] Merge PR #42 on archsetup (approved, CI green) — [[https://github.com/<user>/archsetup/pull/42][PR #42]] :quick:reactive: -** TODO [#B] Respond to the 2pm reschedule invite from Dana :quick:reactive: -** TODO [#B] Reply to the contract-terms email thread :quick:reactive: +*** TODO [#B] Merge PR #42 on archsetup (approved, CI green) — [[https://github.com/<user>/archsetup/pull/42][PR #42]] :quick:reactive: +*** TODO [#B] Respond to the 2pm reschedule invite from Dana :quick:reactive: +*** TODO [#B] Reply to the contract-terms email thread :quick:reactive: #+end_example Rules: @@ -206,7 +126,7 @@ Rules: This sub-step makes triage-intake's findings *persist* in =todo.org= instead of evaporating after the inline summary. -** Phase D: Execute actions on confirmation +*** Phase D: Execute actions on confirmation Wait for Craig's go-ahead before running any state changes. Default to single-confirmation for the whole batch ("yes" → run everything proposed). Craig may also pick a subset ("trash personal but hold the work account") or hand back a different plan ("trash all but star the expense thread and queue PR merges for after lunch"). @@ -216,7 +136,7 @@ After actions complete, write the Phase A capture into the sentinel's *content* *Do not close the workflow yet.* See Exit Criteria below. -** Exit Criteria +*** Exit Criteria The workflow stays open until Craig has *explicitly* either: @@ -229,7 +149,101 @@ If Craig has been silent for a while after Phase D and the surface looks closed- This rule prevents the failure mode where the workflow self-declares done and the next exchange has to relitigate what state things are in. -* Output Template + +* Reference + +** Source Plugin Contract + +A source plugin is a file named =triage-intake.<source>.org=. The first dot after =triage-intake= is the engine/plugin boundary; the segment after it is the source id. Hyphens stay *inside* a segment (=triage-intake.personal-gmail.org= is engine =triage-intake=, source =personal-gmail=). Deeper dots (=triage-intake.<source>.<sub>.org=) are reserved for sub-adapters — YAGNI for now, but the namespace accommodates them at no cost. + +A plugin file declares exactly one source through a fixed shape: + +*Property drawer* on the top-level =* Source:= heading: +- =ORDER= — integer. Output ordering in the per-source breakdown (lower = earlier). +- =ENABLED= — the precondition the engine evaluates before loading the source. The source is skipped — *with an announced reason* — when it's false. Forms: =always=, a shell test (=command -v gh && gh auth status=), or =mcp <server> present=. +- =ANCHOR= — the cutoff format this source consumes: =epoch=, =iso8601=, =day=, or =none= (state-based source with no since-window — e.g. live IMAP unread, or open-PR state). The engine computes the anchor once and substitutes it in the requested format. +- =SUBAGENT_OVER= — integer. If the scan is expected to return more than this many items, dispatch a subagent for the source so its raw output stays out of main context. The subagent applies Phase B and returns buckets only, not the raw list. + +*Body sections:* +- =** Scan= — the command(s) that fetch new/unread items since =<anchor>=, emitting raw items. +- =** Classify= — the source's per-bucket bias and noise patterns. *Deltas* from the engine's shared four-bucket model below, not a re-derivation. +- =** Render= — the source's block in the Phase C summary. "Omit if empty." +- =** Actions= — the executable state-changes, one verb per line: =verb :: command template (parameterized by item id)=. + +Template: + +#+begin_example + +** Source: <id> +:PROPERTIES: +:ORDER: <n> +:ENABLED: <precondition> +:ANCHOR: epoch | iso8601 | day | none +:SUBAGENT_OVER: <n> +:END: + +*** Scan +<command(s) that fetch new/unread items since <anchor>> + +*** Classify +<bias + noise patterns; deltas from the shared four-bucket model> + +*** Render +"<Source label> — N <unit>" block; omit if empty. + +*** Actions +- <verb> :: <command, parameterized by <id>> +#+end_example + + +** Anchor: Since When? + +The workflow needs a "scan since" timestamp. Resolution order: + +1. *Sentinel file content:* first whitespace-delimited token in =.ai/last-triage-intake= is the Phase A scan-kickoff epoch from the most recent successful run (see "Capture the Phase A timestamp" below). Most accurate. +2. *Sentinel file mtime* (back-compat): if the file exists but is empty, read its mtime — that's the older mtime-based convention that pre-dates the content-based change. Still accurate on the machine that wrote it. +3. *Most recent prep doc:* if no sentinel content or readable mtime, anchor on the latest =inbox/YYYY-MM-DD-daily-prep.org= or =daily-prep/YYYY-MM-DD-daily-prep.org= mtime. +4. *Most recent session file:* if none of the above, anchor on the most recent =.ai/sessions/= file's mtime. +5. *Session start:* fall back to the current session's start time. Last resort. + +The engine computes the anchor *once* and exposes it in every format a plugin might request (=epoch=, =iso8601=, =day=). Each plugin's =ANCHOR= field says which it consumes; the engine substitutes that form into the plugin's =<anchor>= placeholder. Sources with =ANCHOR: none= are state-based (live unread, open-PR state) and get no cutoff substituted — they report current state, and Phase B uses the anchor only to flag what's *new since* last check. + +*** Capture the Phase A timestamp + +Just before issuing the Phase A batch, capture the current epoch seconds: + +#+begin_src bash +PHASE_A_TS=$(date +%s) +#+end_src + +Hold this value through Phases B, C, and D. At end of run, *write* the captured timestamp into the sentinel's content (not its mtime): + +#+begin_src bash +echo "$PHASE_A_TS $(date -d "@$PHASE_A_TS" '+%Y-%m-%d %H:%M:%S %z')" > .ai/last-triage-intake +#+end_src + +The file ends up with a single line like =1778683109 2026-05-13 09:38:29 -0500= — epoch first (machine-readable, parsed by reading the first token), human-readable timestamp second. + +*Why content, not mtime:* the sentinel is checked into git. Git tracks content, not mtime, so an mtime-based sentinel is per-machine: one machine's anchor stays on that machine; a fresh clone gets the file but the mtime is whenever the clone happened, not the actual triage time. Writing the epoch as content means the anchor travels with the repo and stays accurate after a fetch + pull on any machine. + +*Why Phase A and not end-of-run:* Phase A runs at one moment, but Phases B-D may take 5-30 minutes. Items posted to any source /during/ Phases B-D land between the Phase A scan time and the eventual end-of-run time. If the sentinel were set to the end-of-run time, those items would silently fall through the cracks: the next triage's Phase A would skip the gap window and never see them. Anchoring the sentinel to Phase A's scan time guarantees the next run's window starts where this run's window ended, with zero gap. + +*** Reading the sentinel + +When the workflow needs the anchor at the start of a new run: + +#+begin_src bash +# Content-first, mtime-fallback. +ANCHOR_EPOCH=$(awk 'NR==1 {print $1; exit}' .ai/last-triage-intake 2>/dev/null) +if [ -z "$ANCHOR_EPOCH" ] && [ -f .ai/last-triage-intake ]; then + ANCHOR_EPOCH=$(stat -c %Y .ai/last-triage-intake) +fi +#+end_src + +If both fail, fall through to the resolution order above (prep doc → session file → session start). + + +** Output Template The summary follows this shape (omit any source that genuinely returned nothing; render one block per *loaded* source in =ORDER=, using each plugin's =Render= shape): @@ -253,7 +267,8 @@ The summary follows this shape (omit any source that genuinely returned nothing; Order matters: top-signals first because that's what Craig reads in 30 seconds between meetings. Per-source detail second. Suggested actions last because they require a decision. -* Common Mistakes + +** Common Mistakes 1. *Globbing only =.ai/workflows/= and missing the project plugins.* The single most damaging failure mode — the sweep runs with half its sources and the omission is invisible (a missing source looks identical to a quiet one). Phase 0 globs *both* =.ai/workflows/triage-intake.*.org= and =.ai/project-workflows/triage-intake.*.org=, every run, and announces the loaded set. 2. *Running Phase A sequentially.* Send every enabled source's scan in one message — the whole point is parallelism. @@ -264,20 +279,24 @@ Order matters: top-signals first because that's what Craig reads in 30 seconds b 7. *Running this alongside daily-prep.* Daily-prep already does this as Phase 3 — don't duplicate. 8. *Mixing Action and FYI in the top-signals list.* Top signals = Action only. FYI lives in the per-source detail. -* Living Document + +* History / Design Notes + +** Living Document Update the engine as the orchestration pattern evolves; update a plugin as its source evolves. Source-specific learnings belong in the plugin's own file, not here. -** Updates and Learnings +*** Updates and Learnings -*** 2026-05-01: Initial creation +**** 2026-05-01: Initial creation Extracted from daily-prep's Phase 3 pattern as a standalone, lightweight, between-meetings sweep. -*** 2026-05-07: Anchor the sentinel to Phase A scan time, not run-end time +**** 2026-05-07: Anchor the sentinel to Phase A scan time, not run-end time Gap-window bug: a run had Phase A fire at 13:35 and the sentinel set at 15:04, so an item posted at 14:20 would be skipped by the next run (the sentinel claimed everything before 15:04 was scanned when Phase A only reached 13:35). Fix: capture =PHASE_A_TS= just before Phase A, hold it through B-D, write it to the sentinel at end of run. The sentinel means "everything before this timestamp has been scanned," the only invariant that prevents items falling through the cracks. -*** 2026-05-13: Move the sentinel from mtime to content (cross-machine survivability) +**** 2026-05-13: Move the sentinel from mtime to content (cross-machine survivability) The sentinel is checked into git, but git tracks content, not mtime — so an mtime anchor is per-machine. Fix: write the captured epoch into the sentinel's content (=EPOCH ISO-8601=), read with =awk 'NR==1 {print $1}'=, mtime as back-compat fallback. -*** 2026-05-26: Refactor into engine + source plugins +**** 2026-05-26: Refactor into engine + source plugins Split the monolithic workflow into a source-agnostic engine (this file) and per-source plugins named =triage-intake.<source>.org=. The engine carries the anchor/sentinel logic, the four-bucket model, the Phase A-D orchestration, the todo.org persistence convention, and the exit criteria. Each source's scan/classify/render/action knowledge moved to its own plugin. General plugins (personal-gmail, personal-calendar, cmail, github-prs) live in =.ai/workflows/= and are template-synced; project-specific plugins (a work project's Linear, work Gmail, work Slack, enterprise PRs) live in the project's =.ai/project-workflows/= and are never synced. Phase 0 globs *both* directories — the loud requirement, because missing the project dir silently halves the sweep. Naming convention: first dot is the engine/plugin boundary, deeper dots reserved for sub-adapters. This removed all DeepSat/Linear specifics from the engine; they become work-project plugins. + |
