diff options
| author | Craig Jennings <c@cjennings.net> | 2026-06-23 23:06:46 -0400 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-06-23 23:06:46 -0400 |
| commit | 24ca58d764dbcc2bad57a914a10e9e9b89a3f66e (patch) | |
| tree | dc3ab8db840b1562db36cc048e44a391e31fa73f /.ai/workflows/inbox.org | |
| parent | 27da1f1d3290314d32455eac4e049118b00a7a13 (diff) | |
| download | rulesets-24ca58d764dbcc2bad57a914a10e9e9b89a3f66e.tar.gz rulesets-24ca58d764dbcc2bad57a914a10e9e9b89a3f66e.zip | |
feat(inbox): consolidate three inbox workflows into one engine
I merged process-inbox, monitor-inbox, and inbox-zero into one inbox.org engine. A shared core (value gate, skeptical review, disposition ladder, reply discipline, capture-guard, priority-scheme check) holds the logic that used to be duplicated and cross-referenced across the three files. Each mode (process, monitor, roam) references the core by name instead of restating it.
Every trigger phrase still works, now routing to a mode, so there's nothing to relearn. I added the interactive auto inbox zero mode: ask for an interval, run roam mode on /loop, acknowledge-only on an empty cycle, surface a find to a queue gated on a yes. The fully-unattended /schedule pass stays vNext, tracked separately.
I repointed every live caller (INDEX, protocols, startup Phase C, wrap-up Step 3, triage-intake, broadcast) at inbox.org and its modes, then deleted the three old files. triage-intake and no-approvals stay separate by design. The value gate, dispositions, capture-guard, and reply discipline all behave as before.
Built from the Ready spec. Workflow-integrity and sync-check pass on both the canonical and mirror trees, the stale-reference grep is clean, and the full suite is green.
Claude-Session: https://claude.ai/code/session_017PtX1nt1rtYVATuzmzBS4f
Diffstat (limited to '.ai/workflows/inbox.org')
| -rw-r--r-- | .ai/workflows/inbox.org | 481 |
1 files changed, 481 insertions, 0 deletions
diff --git a/.ai/workflows/inbox.org b/.ai/workflows/inbox.org new file mode 100644 index 0000000..26bf008 --- /dev/null +++ b/.ai/workflows/inbox.org @@ -0,0 +1,481 @@ +#+TITLE: Inbox Workflow (Engine) +#+AUTHOR: Craig Jennings & Claude +#+DATE: 2026-06-23 + +* Overview + +One engine for the project's inbox surfaces. Inbox items are *ideas to evaluate*, not orders to execute — each is a proposal that earns a place in =todo.org= or git history only when it passes the value gate. The engine holds the shared disposition machinery once: the three-question value gate, the skeptical review, the disposition ladder, the reply-to-sender discipline, the capture-guard before a roam write, and the priority-scheme check. Each *mode* is a thin section that names which surface it reads, how it enters and exits, and which core steps it runs. + +Two surfaces feed a project, and there is a recurring check over the second: + +1. *Project-local =inbox/= dir* — handoffs from other projects (via =inbox-send=), from scripts, and from Craig (typed directives saved as files). Handled by *process mode*; watched on a cadence by *monitor mode*. +2. *Global roam inbox* (=~/org/roam/inbox.org=) — Craig's cross-project GTD capture, one shared file every project can see. Handled by *roam mode*, which claims only the items this project owns. *Auto inbox zero* runs roam mode on a recurring interactive loop. + +A *third* surface — external accounts (email / calendar / PRs) — is a different domain and stays in its own engine: =triage-intake.org= and its source plugins are *not* part of this engine. "Deal with my inbox dirs" is here; "what's new across my accounts" is there. + +*Two altitudes.* For the user, the trigger phrase picks the mode and the phrases are unchanged (see When to Use). For the implementer, this is one file: the core sections are written once, and each mode references them by name ("run the value gate (core §1) on each item") rather than restating them. + +* When to Use This Workflow + +The trigger phrase selects the mode. Every phrase below still works; it now routes to a mode of this engine. + +** Process mode — the local =inbox/= dir + +- "process inbox" / "process the inbox" +- "handle the inbox" +- "what's in inbox" / "what's in the inbox" +- "let's clear the inbox" / "let's process the inbox items" + +Auto-invocation: startup Phase C delegates here when the local inbox is non-empty — don't ask, just run it. + +** Monitor mode — process mode on a cadence + +- "monitor the inbox" / "watch the inbox" — *the defined meaning:* one process pass now, then loop every 15 minutes (see the Monitor mode Cadence section). The phrase *is* the loop, not an opt-in extra. +- "respond to the handoffs" / "handle the handoffs" — a single pass now, no loop. + +Ambient (always on, even with no loop running): the =inbox-status= task-boundary check (Monitor mode). + +** Roam mode — the global roam inbox + +- "inbox zero" / "empty the inbox" / "process the roam inbox" / "triage my roam inbox" + +Called read-only from startup (count + offer) and as a wrap-up Step 3 sub-step. + +** Auto inbox zero — recurring interactive roam check + +- "auto inbox zero" + +Match this before "inbox zero" — the auto phrase contains the roam phrase as a substring, so the longer match wins. Starts a recurring =/loop=-driven roam-mode pass; see the Auto inbox zero mode. + +** Boundary + +Do *not* invoke this engine for an inbox item that is clearly out-of-scope for the project — that is a cross-project routing problem, handled per the cross-project boundary rule in =protocols.org=. And do not invoke it for external-account triage ("what's new in email/cal/PRs") — that is =triage-intake.org=. + +* Core §1 — The value gate + +Every inbox item (local or roam) passes through three questions. One *yes* is enough to accept. + +1. *Does it advance an existing TODO?* Look up by topic in =todo.org='s open work. If the item extends a filed task, fold it in. If it implements a filed task, do the work. +2. *Does it improve how the project works?* Architecture cleanup, workflow refinement, tooling, rule hygiene, drift detection — anything that makes the project itself more effective. +3. *Does it serve the project's stated mission?* Read =notes.org= *Project-Specific Context* if the mission isn't obvious from the working directory and current task. The item should advance that mission, not orbit it. + +Three *no*s means reject. The rejection isn't lazy — an idea that doesn't help any current task, doesn't improve the system, and doesn't serve the mission is genuine noise, and accepting it inflates =todo.org= without payoff. + +* Core §2 — The skeptical review + +The value gate decides whether an item is worth taking. This review decides whether what it proposes is *right*, *complete*, and *as simple as it should be*. Run it on every task and file that arrives — not only shared-asset change proposals. Pure FYIs and replies that ask for nothing skip it. + +Approach the file with curiosity and skepticism. Work through, in writing — the core pass on every item: + +1. Is the request actually right — does it do what it claims, and is the claim correct for this project? +2. Is it complete, or does it leave a gap — an unhandled case, a missing step, an untested path? +3. Should it be simpler? +4. Can it be enhanced to be more effective than as proposed? +5. Does it conflict with any existing instruction — workflows, skills, rules, protocols, CLAUDE.md? + +When the item proposes a change to *shared assets* — template workflows, rules, skills, scripts, anything synced to consuming projects — or to a substantive convention, add the cross-project battery. It arrived from one project's context; you're evaluating it for all of them: + +6. Does this make sense for *all* consuming projects, or just the sender's situation? +7. How does it change a common activity Craig performs — better, worse, or differently than the sender assumed? +8. Plus at least three more questions specific to this change — what breaks for artifacts already using the old shape, what tooling interacts with it, what's underspecified, what the sender's worked example doesn't exercise. + +Output: a short summary of the thinking and a recommendation (do it / do it with named changes / file / reject). For shared-asset and convention changes the recommendation is surfaced to Craig for approval before applying; for ordinary tasks and files it feeds the act-vs-file and no-approvals-execute decision (Monitor mode). + +** In a no-approvals session: shared-asset changes defer and stage + +Shared-asset and convention changes still don't self-apply when Craig has put the session in no-approvals mode — they need his decision, so they fail the *solo* test in Monitor mode's executing-in-no-approvals criteria. Ordinary tasks and files that pass the review and are quick + solo execute under that criteria instead; this defer-and-stage path is for the shared-asset and convention changes that don't qualify. Run the review, prepare the edits in =working/<task-slug>/= (a patch file or the worked-out diff), file a =[#B]= VERIFY carrying the decision package, and reply to the sender that it's parked. The sender's local stopgap (per =cross-project.md='s propagation process) means the delay costs nothing — the canonical update is about durability, not speed. + +Wording-only fixes — no consuming project acts differently — may proceed even then, logged in the session log. + +The VERIFY shape (top-level, =[#B]= so startup's A/B surfacing catches it; no =SCHEDULED= unless the proposal names a real deadline): + +#+begin_example +** VERIFY [#B] Parked: <proposal topic> (from <sender>) +What arrived: <one line — what the handoff proposes>. +Recommendation: <accept as-is / accept with changes / reject> — <2-3 line +skeptical-review summary: what's right, what to change, what was checked>. +Prepared diff: [[file:working/<slug>/proposed.diff]] — apply is mechanical on +your go. +Say "approve the parked <topic>" (or adjust / reject) and it gets applied. +#+end_example + +The full question-battery answers live in the session log and the =working/= dir, not the task body — the body carries the conclusion, with the trail one link away. + +* Core §3 — The disposition ladder + +Every item that clears the value gate gets one disposition. The first six are the per-item outcomes; *park* is the no-approvals shared-asset path from core §2. + +** Implement now +Small, scoped, clear, no design call required. The work is the disposition. Do the work, commit per the project's commit flow, delete the inbox file. The commit message references the inbox item by filename so the provenance lands in =git log=. + +** Fold into existing TODO +The item extends a task already filed. Update the parent TODO's body with a dated reconciliation sub-entry per =todo-format.md= (=*** YYYY-MM-DD Day @ HH:MM:SS -ZZZZ <what landed>=). Move substantive content to =docs/design/<date>-<topic>.<ext>= if it's worth keeping; reference from the TODO body. Delete the inbox file. + +** File as TODO +Substantive but waits, or needs design/triage before implementation. Add the TODO under =* <Project> Open Work= with priority + tags per the priority-scheme check (core §6). Body summarizes the proposal and links the inbox content if it's been moved to =docs/design/=. Delete the inbox file (or move it to =docs/design/= first if the content survives). + +** Defer +Rename in place to =inbox/PROCESSED-<original-filename>= and add a brief comment line at the top: =# Deferred YYYY-MM-DD: <condition>=. Don't accumulate deferred items indefinitely — sweep them on a future process pass when the condition is met or the deferral has aged out. + +** Reject — by source +- *From Craig* — push back honestly in chat. State why you won't implement; offer the conditions under which you would, if any. The inbox file stays until Craig confirms — override re-enters as accept, acknowledgment deletes the file. Don't theatre the pushback: if you don't genuinely think Craig is wrong, just do the work. +- *From another project (handoff)* — write a response file at =/tmp/inbox-response-<topic>.org=: a heading naming the original handoff and date, one paragraph on the rejection rationale (*which* value-gate question failed and why), one paragraph on the condition under which you'd reconsider (or "never, this misreads the project's mission" if that's the truth). Deliver via =inbox-send <sender> --file /tmp/inbox-response-<topic>.org=. Delete the local inbox file after the response lands. Silent rejection on a handoff trains the sender to escalate around the channel — always close the loop. +- *From a script or automated system* — just delete. No notification. + +** Park (skeptical review in a no-approvals session) +Move the proposal file into =working/<task-slug>/= alongside the prepared diff, file the =[#B]= VERIFY per core §2, reply to the sender that it's parked for Craig's review, and delete the inbox file. On Craig's approval the apply is mechanical: apply the prepared edits, run the normal verify-and-publish flow, close the parked =**= VERIFY per =todo-format.md= (a top-level VERIFY resolves to =DONE= + =CLOSED:=, not a dated header), and send the acceptance reply. On rejection, the reject-from-another-project flow above runs unchanged. + +* Core §4 — Reply-to-sender discipline + +A handoff came from another project's agent (or the user). Close the loop: + +- *Accepted and acted on* — send a confirmation to the sender via =inbox-send <sender> --text "..."=, naming what landed and the commit, so they're not left guessing (they can't see this project's git log). =inbox-send= excludes the current project as a target, so a self-sourced item is handled in-session, not sent. +- *Accepted and filed* — a short confirmation that it's filed and where, so the sender knows it wasn't dropped. +- *Rejected* — always state the why (which value-gate question failed), per the reject-by-source ladder (core §3). + +Cross-project boundary: never act on a file under another project's =.ai/= scope from here — route it back as a handoff (see =cross-project.md=). + +* Core §5 — Capture-guard before a roam write + +Before *any* read-modify-write of =~/org/roam/inbox.org=, run =.ai/scripts/capture-guard "$HOME/org/roam/inbox.org"=. This runs first because a =git pull= on the roam repo *also* rewrites the file on disk, and a fast-forward landing underneath a live capture wedges it just as a hand edit would. + +- *Exit 0* → no live capture (or no reachable Emacs). Proceed. +- *Exit 1* → an indirect org-capture buffer is cloned from the roam inbox (the script prints the offending buffer name). Editing or fast-forwarding the file underneath it would leave the capture pointing at stale state and unable to finalize with =C-c C-c= (see =emacs.md=). Behavior depends on the caller: + - *On-demand / interactive run* → stop and surface: "You have a live org-capture session open against the roam inbox (=<buffer>=) — finalize it (=C-c C-c=) or abort it (=C-c C-k=) and I'll continue." Re-run the guard and resume once it returns clean. + - *Wrap-up sub-step* → don't block the wrap. Skip the roam reconcile for this run and surface one line: "Skipped roam-inbox reconcile — a live org-capture is open against it; claimed items stay and get caught next run." The items were already filed into =todo.org= in roam mode Phase C, so the next roam run's Phase C status-check drops the duplicates and its Phase D removes them — the skip self-heals. + +* Core §6 — Priority-scheme check + +This gates filing whenever there are accept-and-file items. Check whether =todo.org= has a top-of-file priority scheme (an explicit legend defining =[#A]= through =[#D]= semantics and mandatory/optional tag conventions — a =* <Project> Priority Scheme= section or similar). + +- *Scheme present* — file new TODOs per the scheme. Every TODO gets a priority cookie matching the legend's rules, the mandatory type tag, and any applicable effort/autonomy tags. +- *Scheme absent* — surface one sentence: "This project has no priority scheme. We should adopt one before filing the new TODOs from this inbox pass — want me to propose one based on the rulesets scheme?" If Craig says yes, do that first (the =/research-priority-scheme= research subagent pattern in rulesets is the reference). If Craig says no, file the TODOs without grading but flag in the commit message that they're un-prioritized pending a scheme. + +The point is to avoid adding ungraded =TODO= entries to a project that's never agreed on what =[#A]= means. + +* Mode: process + +Reads the project-local =inbox/= dir. Entry: a trigger phrase, or startup Phase C on a non-empty inbox. Exit: inbox empty (excluding =.gitkeep= and intentional =PROCESSED-*=), session log updated, =:LAST_INBOX_PROCESS:= stamped. + +** Phase A — Inventory (one parallel batch) + +Issue these reads in one parallel batch: + +1. List =inbox/= excluding =.gitkeep= and =PROCESSED-*= prefixes (use =\ls -la inbox/= per the protocols.org exa-alias note). +2. Read =notes.org= *Project-Specific Context* if mission isn't already loaded in the session. +3. Read =todo.org='s top-of-file priority scheme if present. + +For each inbox file, parse the filename for sender. Two common patterns: + +- =YYYY-MM-DD-HHMM-from-<sender>-<topic>.<ext>= — from another project via =inbox-send=. +- =<topic>.org= — typically from Craig directly, or from a script. + +Note the file type. =.eml= files need the extract script (not raw =Read=): + +#+begin_src bash +# View mode +python3 .ai/scripts/eml-view-and-extract-attachments.py inbox/<file>.eml + +# Pipeline mode (extract attachments to a directory) +python3 .ai/scripts/eml-view-and-extract-attachments.py inbox/<file>.eml --output-dir assets/<target>/ +#+end_src + +Everything else, read directly. + +** Phase B — Evaluate each item + +For each inbox file: + +1. *Read it.* Full read for substantive proposals (org files with TODO entries, design notes, multi-section docs); skim short FYIs and one-liner asks. +2. *Identify the shape.* Instruction, question, proposal, FYI, or handoff — shapes guide disposition. +3. *Apply the value gate* (core §1). One yes → candidate accept. Three nos → candidate reject. +4. *Run the skeptical review* (core §2) on the item before classifying — the core pass on every accepted task and file, plus the cross-project battery when it proposes a shared-asset or convention change. Its summary + recommendation rides along to Phase C; in a no-approvals session it gates whether the item self-applies (quick + solo + agreed, per Monitor mode) or, for shared-asset and convention changes, defers and stages. +5. *Within accept, classify* by the disposition ladder (core §3): implement now / fold into existing TODO / file as TODO. +6. *Within reject, classify by source* (core §3): from Craig / from another project / from a script. + +** Phase B.1 — Priority-scheme check + +Run core §6. This gates Phase C filing when there are accept-and-file items. + +** Phase C — Surface dispositions + +Numbered options inline per =interaction.md= (no popup). Recommendation at item 1. + +Batch trivial items (one-line rejections of script noise, obvious file-as-TODO accepts where the scheme is already settled) into a single confirm-all prompt. Walk substantive items one at a time so the decision is visible. + +Per-item template: + +#+begin_example +<filename> from <sender>: <one-line summary> +Value-gate read: <yes/no on each of the three questions, one phrase each> +Disposition recommendation: <implement / fold into <TODO> / file [#X] :tags: / reject> + +1. <recommendation as item 1> +2. <alternative> +3. Defer — leave in inbox under PROCESSED-<topic>.<ext> until <condition> +4. Something else +#+end_example + +For items that went through the skeptical review, the surfaced disposition includes its summary + recommendation, and approval here is what authorizes the apply. In a no-approvals session those items are reported as parked (the =[#B]= VERIFY) rather than surfaced for live approval. + +For pure FYIs that need no action, surface as a single line and recommend delete-with-acknowledgment. + +** Phase D — Apply + +Apply each disposition per the ladder (core §3). The flow is autonomous past Craig's Phase C approval. + +** Phase E — Close out + +Verify =inbox/= is empty (excluding =.gitkeep= and any intentional =PROCESSED-*= files). Run =\ls -la inbox/= and confirm. + +Update the session log per =protocols.org= with one short paragraph: count processed, count accepted (implement/fold/file split), count rejected (Craig/handoff/script split), and the commit SHA if a commit landed. + +Stamp =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section if it exists, so future workflows that gate on freshness can read it. Same format as =:LAST_AUDIT:= (=YYYY-MM-DD=). + +* Mode: monitor + +Process mode on a cadence. This is the *when, how-often, and act-vs-file* layer; the per-item disposition mechanics are the core sections, run via process mode — not restated here. Monitor decides *that* an item gets handled and *how I respond*; the core decides *what disposition* each item gets. + +The gap it closes: handoffs that arrive mid-session used to sit unseen until the user asked or the next startup ran. A handoff the sender can't see being handled trains them to escalate around the inbox channel. + +** Preconditions — before starting + +Never begin monitoring on a dirty worktree or a failing test suite. A dirty tree means the auto-commit at the end of an executed item sweeps up unrelated changes; a red suite means you can't tell whether the monitor broke something. At the start: + +1. =git status --porcelain= is empty (clean worktree). +2. A full test run is all green (=make test= here, or the project's full-suite command). + +If *dirty*: offer to commit the pending changes in discrete, logical batches before starting. If *red*: offer to investigate the failures first. Surface the blocker with inline numbered options per =interaction.md= and wait — monitoring does not start until the tree is clean and the suite is green. + +** Cadence — how often to check + +*"Monitor the inbox" = run now, then loop every 15 minutes.* Do one process pass over any pending handoffs immediately, then start the loop: + +#+begin_src +/loop 15m check the inbox with inbox-status and run inbox.org process mode over any pending handoffs +#+end_src + +Each firing runs the cheap =inbox-status= check first and only does a full process pass when items are pending. The loop is the monitoring; it runs until Craig stops it or the session ends. Honor the Preconditions gate before the first pass and the Close-out gate when the loop stops. + +*Ambient task-boundary check (always on, even without a loop).* After finishing a unit of work, before reporting back or asking "what's next," run the cheap status check: + +#+begin_src bash +.ai/scripts/inbox-status -q +#+end_src + +Exit 1 means handoffs are pending — list them (drop =-q=) and run process mode. Exit 0 means clean; say nothing. This is one =find=; it costs nothing to run often, and it's the fix for handoffs piling up unseen during long sessions. + +*Startup and wrap-up already cover their ends.* Startup Phase C processes a non-empty inbox; the wrap-up sanity check refuses to wrap with unprocessed handoffs. The task-boundary cadence fills the middle. + +*Mid-task arrivals.* If a handoff lands while you're mid-task and it's urgent (blocks the current work, or is time-sensitive), surface it right away. Otherwise batch it to the next task boundary so the current work isn't thrashed. + +** The act-vs-file decision + +Every accepted handoff (one that clears the value gate) is then either acted on now or filed as a task. + +*Act immediately — and just do it, no asking — when all of these hold:* +- *Clear* — the action is unambiguous; no design decision or option-choice is needed. +- *Bounded* — small, finishable this session, ideally a tight file set. +- *Low-risk and verifiable now* — not a risky change to load-bearing infra (or trivially revertible), and testable/lintable this session. +- *In-scope and safe* — within this project, not destructive or outward-facing without confirmation, not across a project boundary. +- *Cheaper than deferring* — doing it now costs less than filing plus re-triaging later. + +When you decide to act, queue the work and do it. Don't ask first. + +*Exception:* a proposal to change a shared asset (template workflow, rule, skill, synced script) or a substantive convention never qualifies for silent act-now, however clear and bounded it looks — it routes through the skeptical review (core §2), which carries its own approval (or, in a no-approvals session, park) step. + +*File a task when any of these hold:* +- It needs a judgment call, a design decision, or an option the user would pick. +- It's large, multi-session, or sprawls across many files. +- It's blocked (a dependency, an external thing, the user is away). +- It's risky enough to want the user's eyes before it lands. +- It's off the session's active goal and acting now would derail it (file and keep going, unless it's urgent). + +When you decide to file, *ask first* — inline numbered options per =interaction.md=, with *filing as option 1 (the recommendation)* and *"do it now" as option 2*: + +#+begin_example +<handoff> wants <X>. My read: file it (needs <reason>). + +1. File as a TODO ([#?] :tags:) — Recommended +2. Do it now instead +3. Something else + +Pick a number. +#+end_example + +*Always ask if you're unsure* which side of the line an item falls on. Decisiveness on clear act-now items is the point of the rule; the ask is for genuine ambiguity and for filing. + +** Executing in no-approvals mode + +When Craig has put the session in no-approvals mode, an accepted item may be implemented automatically — but only when all three of these hold: + +1. *Agreed* — you've run the value gate and the full skeptical review and concluded the change should be done, not merely that it's harmless. +2. *Quick* — the whole implementation, including verification, is under ~15 minutes. +3. *Solo* — you can carry it end to end without a decision from Craig. Manual verification you perform yourself is fine; needing Craig to choose an option, approve a design, or resolve an ambiguity is not. + +All three → implement it, verify, then commit and push at the end of that item (the Step 0 reconcile and pre-push check from =commits.md= still run). Miss any one and it doesn't self-apply: a shared-asset or convention change needs Craig's decision, so it fails *solo* and routes to the defer-and-stage park (core §2 / core §3); an oversized item fails *quick* and gets filed. + +** Replying to handoffs + +Close the loop per the reply-to-sender discipline (core §4): confirm what landed (accepted-and-acted), confirm where it's filed (accepted-and-filed), or state the why (rejected). + +** The inbox-status script + +=.ai/scripts/inbox-status= lists unprocessed handoffs and exits nonzero when any are pending. Exclusions match the wrap-up sanity check (=.gitkeep=, =lint-followups.org=, =PROCESSED-*=). Exit 0 = clean, 1 = pending, 2 = no inbox/ or bad usage. Use =-q= for the count-only form the cadence check calls. + +** Close out — before finishing + +End the way it started: clean worktree, green suite. Before stopping the loop or reporting the pass done: + +1. Commit or revert everything left in the worktree — nothing uncommitted remains. +2. Run the full test suite once more and confirm all green. + +If either can't be satisfied — a half-done item, a failure introduced during the pass — surface it rather than leaving it. The next monitor run assumes a clean, green starting state (the Preconditions gate). + +* Mode: roam + +Reads the *global roam inbox* (=~/org/roam/inbox.org=), Craig's cross-project GTD capture: one shared file every project can see. This mode routes each roam item to the project that owns it. The current session claims only the items belonging to THIS project, files them into the project's =todo.org=, and removes them from the shared inbox. Everything it doesn't own stays. + +The aspiration is inbox zero: after this mode runs, the current project's local handoff inbox has been processed (Phase A delegates to process mode) and the shared roam inbox no longer contains items explicitly owned by this project. + +This is distinct from the wrap-up inbox/transcript routing feature (which moves session-filed keepers between projects). This routes the shared roam capture file by ownership prefix. + +** Scope: single-destination (v1) + +Routes each item to its one owning project, identified by an explicit =<project>:= heading prefix. The multi-project domain-aware mode (guess the owner of every unprefixed item and empty the whole inbox in one run) is deferred — see "Deferred: domain-aware routing" at the end. v1 claims only what's prefixed for the current project, surfaces the rest, and never guesses. + +** Callers + +The steps live here so three callers reuse them: +- *Startup* (read-only nudge) — count the items, identify which appear related to this project, surface both numbers, offer processing as one of the startup options. Never auto-files. +- *Wrap-up* (Step 3 sub-step) — sweep items that belong here before the cleanup scripts, so imported tasks lint and ride the wrap commit. +- *On demand* — the roam-mode trigger phrases. + +Each project touches the roam inbox at least twice a session this way: once at startup, once at wrap-up. + +** The ownership rule (the coordination primitive) + +The inbox is shared, so the mode must never let two projects fight over an item or let one grab another's. Ownership is by explicit prefix: + +- =<project>: ...= heading → owned by that project. The current project claims only items prefixed with its own identifier. +- Prefixed for *another* project → leave untouched (cross-project boundary, =protocols.org=). +- *No prefix* → unowned. Never auto-claim. Surface as candidates a human can claim or prefix. + +The prefix partition is what makes concurrent triage across projects safe: each project only ever removes its own items, so two sessions editing the inbox touch disjoint lines. + +*Resolving this project's identifier (v1).* Use the project root basename plus its common aliases (=.emacs.d= ↔ =emacs=, and the obvious ones: =rulesets=, =work=, =home=). A project may override the inferred set with an =:INBOX_PREFIX:= line in =notes.org='s *Workflow State* section when the basename is fragile. The explicit override is optional in v1; the durable multi-project resolution is part of the deferred domain-aware mode. + +** Phase A — Process the project-local inbox + +1. Check the project-local =inbox/= with =.ai/scripts/inbox-status -q=. +2. If pending handoffs exist, run *process mode* before touching the roam inbox. Project handoffs are already addressed to this project, so they are higher-confidence and cheaper to clear than shared roam captures. +3. If =inbox-status= reports no =inbox/= directory, note it and continue to the roam inbox. Some projects only participate in the shared roam capture flow. +4. If process mode cannot finish because it needs Craig's decision, stop after surfacing that decision. Do not remove roam items in the same run; the project still does not have a clean inbox. + +** Phase B — Identify, count, and match roam items + +1. Resolve the current project's identifier and aliases (above). +2. Read =~/org/roam/inbox.org=. If absent, silent no-op (the file lives only on machines with the roam clone). +3. Bucket every item under the inbox heading: + - *claimed* — prefixed for this project + - *foreign* — prefixed for another project → leave + - *unowned* — no project prefix + - *empty* — a heading with no title and no body: just stars, optionally a =TODO=/keyword, and whitespace (e.g. =** =, =** TODO =, =*** TODO =). These are aborted or accidental captures, owned by nobody, and safe to delete regardless of project. A heading with any title text or any body content is never empty. +4. *Summarize the scan* (Craig's requirement, every scan): report the total item count in the inbox, then the count that appears related to this project. "Appears related" is the union of claimed items (exact prefix) and any unowned item whose topic plainly concerns this project's domain (a content judgment, surfaced as a candidate, never auto-claimed). Foreign-prefixed items are not "related" — they belong to their owner. Note the empty count separately. +5. If claimed, related-unowned, *and* empty are all absent, report the total and stop (the common case for most wraps). Empty entries on their own are enough to enter Phase D — the cleanup runs even when this project owns nothing else, since empties belong to nobody and removing them is what "check the inbox" should always do. + +** Phase C — File each claimed roam item into todo.org + +Apply the core disposition discipline against the project's =todo.org=; don't reinvent it: + +1. *Status check first.* Already done, or already a task in =todo.org=? → drop it, or fold into the existing task (dated sub-entry per =todo-format.md=). Don't duplicate. +2. *Rewrite* to terse-heading + body per =todo-format.md=. +3. *Priority + tags from THIS project's scheme* (core §6) — the legend at the top of its =todo.org=, tags from that scheme's allowed set only. The project expresses someday-maybe with =[#D]=; there's no special someday-maybe routing. +4. *File* under the project's Open Work section. + +** Phase D — Reconcile the shared roam inbox + +The roam inbox lives in a git repo (=~/org/roam=, auto-synced by the =roam-sync= timer). Edit it carefully: + +1. *Guard against a live org-capture session* — run the capture-guard (core §5) before any read-modify-write. The guard runs first because the pull in step 2 also rewrites the file on disk. On exit 1 the caller-specific behavior (interactive stop-and-surface vs wrap-up skip-and-self-heal) is in core §5. +2. *Pull first* (=git -C ~/org/roam pull --ff-only=). If it can't fast-forward (dirty tree, divergence), surface and stop. Don't auto-stash, auto-merge, or force. Resolve before removing items. +3. *Remove the claimed items and the empty entries.* Never touch foreign or unowned (titled) items. Empty entries (Phase B's =empty= bucket) are removed on every triage regardless of who would own a titled version, since an aborted capture belongs to nobody. The claimed-item removal and the empty sweep happen in the same edit. +4. *Commit the roam repo as its own commit* (separate from any project wrap commit). Subject by what changed: =chore(inbox): route <project> tasks to <project>/todo.org= when items were claimed, =chore(inbox): drop empty entries= when the run only swept empties, or both clauses when it did both. Push, or leave for the =roam-sync= timer. Surface a blocked push; don't force. + +** Phase E — Surface + +Report: local project inbox disposition first (processed count and whether it is clear), then roam disposition: moved (with their new priorities and tags), folded, dropped-as-done, and empty entries swept (count). Then the residue: foreign items (left for their owners, count only) and unowned items (count plus the headings that appear related to this project, for manual claim or prefix). Same "summarize what we kept" shape. + +If triaging this batch surfaced a durable, cross-project fact (a reference pointer worth keeping, a pattern worth recording), consider writing it to the agent KB as one =:agent:= node (see =knowledge-base.md=; personal projects only). Skip silently when nothing durable came up — never pad an empty run with a KB line. + +** Skip conditions + +- No project-local =inbox/= and no =~/org/roam/inbox.org= → silent no-op. +- Project-local =inbox/= exists but has no pending handoffs → continue to roam scan. +- No =~/org/roam/inbox.org= after the local inbox check → report the local inbox disposition and stop. +- No claimed, no related-unowned, and no empty roam entries → report the total, stop. +- Roam pull blocked → surface, stop before editing. + +** Caller integration + +*Startup (read-only nudge).* Startup already checks the project-local =inbox/= via =inbox-status= and processes it through process mode when needed. It also reads =~/org/roam/inbox.org= and produces the roam scan summary; one line surfaces: "Roam inbox: N items total, M appear related to this project (K empty entries to sweep) — say 'inbox zero' to file them." Offered as one of the priority options. The empty count rides along so a clean-up-only run still gets offered. Startup never auto-files or auto-sweeps roam items; it counts and offers (the read-only nudge never edits, so empties are reported, not removed, until a real triage runs). + +*Wrap-up (Step 3 sub-step).* A sub-step at the start of wrap-up Step 3 (before the cleanup scripts, so imported tasks get linted and ride the wrap commit) delegates here for the claimed set. Skip-fast when nothing matches. + +** Deferred: domain-aware routing (future work, multi-project) + +v1 handles the single-destination case via the prefix rule. The multi-project parts are deferred until the need is real: + +1. *Domain-aware empty-it-all mode.* If rulesets held a description of each project's domain, one run could guess the owner of every item (prefixed or not) and empty the whole inbox at once, delivering each item to its owning project's =inbox/= via =inbox-send= (where that project's process-mode gate still decides whether to file it). Open: where the domain map lives, how confident a guess must be before auto-routing vs surfacing, and whether a low-confidence item stays put. +2. *Explicit per-project =:INBOX_PREFIX:= as the durable resolver*, replacing basename inference. +3. *Unowned-item lifecycle* once domain-aware routing exists (no item stays unrouted indefinitely). +4. *Concurrent push contention* on the shared roam repo: the pull-before-edit + ff-only + surface-on-conflict floor may want a retry-once-after-pull. + +Take these up when the single-destination version is in use and the multi-project pain is concrete. + +* Mode: auto inbox zero + +A recurring, *interactive* roam check. Trigger phrase: "auto inbox zero" (match before "inbox zero" — the longer phrase wins). On invocation, *ask Craig for the interval* (e.g. 30 min, 2 hours), then drive the loop with =/loop <interval>= running roam mode. It is in-session and interactive by design — each cycle reports, and a find waits for Craig's go before any work happens. + +** Per cycle + +1. Run roam mode's scan (Phase A local check + Phase B roam scan). The capture-guard and pull-before-edit discipline (core §5, roam Phase D) still gate any write. +2. *Nothing found* → no inbox summary. One acknowledgement line: =ran at HH:MM, nothing found=. Nothing else. The acknowledge-only-on-empty rule keeps a quiet inbox quiet. +3. *Items found* → summarize the found items, file them as tasks (roam Phase C), and *append them to a displayed queue* — the harness task list, via =TaskCreate= — so the queue accumulates across cycles. Then ask: "run this batch next?" + - *Yes* → launch into implementing the found items, each through the normal disposition ladder (core §3) + verify flow. + - *No* → they stay queued for a later go. +4. *Cross-cycle dedup.* Subsequent cycles add only *newly-found* items to the same displayed queue, never re-surfacing what's already there. Dedup against the queue (the =TaskCreate= list), not against what's already been implemented — a find that was queued-but-not-yet-run must not reappear, and one already filed into =todo.org= is dropped by roam Phase C's status check. + +A find is always surfaced and gated on Craig's yes; a quiet inbox produces only the timestamped acknowledgement. =auto inbox zero= is inherently in-session because its execute step waits for a yes. + +** Fully-unattended pass (=/schedule=) — vNext, not v1 + +A fully-unattended cron pass (firing while Craig is away) is a *different contract* and is deferred. It can't wait for a yes, so it has to decide up front whether it may mutate =todo.org= and the roam inbox or stays read-only, how a find reaches Craig asynchronously, how dedup state survives across runs that don't share a session, and what session/auth context a cron run carries. + +The =/schedule= recipe, once that contract is designed, would look like: + +#+begin_src +/schedule <cron-expression> run inbox.org roam mode read-only, and <surface-mechanism> any finds +#+end_src + +v1 ships only the interactive =/loop= shape above; the unattended contract is logged to =todo.org= for its own design pass. Don't invent the unattended behavior here — route a request for it to that task. + +* Common Mistakes + +1. *Treating items as orders.* Inbox content is a proposal. The value gate is the rule. Implementing every item without evaluation inflates =todo.org= and trains senders to keep sending noise. +2. *Filing without applying the value gate.* "File as TODO" is not a default — it's the disposition for proposals that pass the gate but wait. A reject is also a valid answer. +3. *Filing raw TODOs when the project has a priority scheme.* Core §6 is mandatory when the scheme exists. An un-graded TODO in a project with a legend is a defect. +4. *Silently deleting a project handoff.* Send a response naming which value-gate question failed. Silent rejection trains the sender to escalate to Craig instead of through the inbox channel. +5. *Pushing back on a Craig directive only to immediately implement it anyway.* If you genuinely think Craig is wrong, say so and wait. If you don't, just do the work — don't theatre the pushback. +6. *Skipping the implement-vs-fold-vs-file classification.* Defaulting every accept to "file as TODO" turns the inbox into a queue that flows into =todo.org= without filtering. +7. *Not propagating value-gate failure to the response.* When you reject a handoff, name *which* gate question failed so the sender can recalibrate, not just resend. +8. *Forgetting to delete the inbox file after acting.* The local inbox should be empty when process mode ends. Files left behind become noise on the next startup. +9. *Applying a shared-asset change proposal without the skeptical review.* The value gate alone asks whether to take the change, never whether the change is right, complete, or as simple as it should be. (Worked example: the 2026-06-12 spec-decisions handoff was applied as-is and the after-the-fact review surfaced a lost state, a vacuous gate pass, and an enhancement — all catchable up front.) +10. *Editing the roam inbox without the capture-guard.* A disk write under a live org-capture wedges the capture (core §5). Guard first, every roam write. +11. *Auto inbox zero re-surfacing queued items.* The loop must dedup against the displayed queue, not just against what's been implemented — or every cycle re-lists the same un-run finds. + +* Living Document + +Refine the value gate's three questions if the project's mission sharpens. Tune the per-source rejection-response template if =inbox-send= response loops surface a pattern. Tune the monitor cadence if task-boundary checking proves too frequent or too sparse. Capture the auto-loop interval that worked once the pattern recurs. + +If a mode wants real depth — enough that it bloats the core — it can become an =inbox.<mode>.org= plugin under this engine's namespace (the pattern =triage-intake= uses) rather than swelling this file. The principle that inbox items are *ideas to evaluate* is the part that doesn't change. |
