1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
|
#+TITLE: Inbox Zero Workflow
#+AUTHOR: Craig Jennings & Claude
#+DATE: 2026-06-13
* Overview
Inbox zero means both inboxes that can feed the current project are checked:
1. The project-local =inbox/= directory, which receives handoffs from other projects, scripts, and Craig. This workflow delegates those items to =process-inbox.org=; it does not duplicate that workflow's value gate or disposition mechanics.
2. The roam global inbox (=~/org/roam/inbox.org=), Craig's cross-project GTD capture: one shared file every project can see. This workflow routes each roam inbox 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 workflow runs, the current project's local handoff inbox has been processed and the shared roam inbox no longer contains items explicitly owned by this project.
This is also 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)
This version routes each item to its one owning project, identified by an explicit =<project>:= heading prefix. The multi-project domain-aware mode, which would 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.
** Three callers
Reused from three callers so the steps live in one place:
- *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* — "inbox zero", "empty the inbox", "process the roam inbox", "triage my roam inbox".
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 workflow must never let two projects fight over an item or let one project 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 (a dot in the name, an alias the inference misses). 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 [[file:process-inbox.org][process-inbox.org]] 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-inbox.org= 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
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.
5. If both claimed and related-unowned are empty, report the total and stop (the common case for most wraps).
* Phase C — File each claimed roam item into todo.org
Apply =process-inbox.org='s 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* — 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* — before any read-modify-write of the roam inbox, run =.ai/scripts/capture-guard "$HOME/org/roam/inbox.org"=. This runs first because the pull in step 2 *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 Phase C, so the next inbox-zero run's Phase C status-check drops the duplicates and its Phase D removes them from the roam inbox — the skip self-heals, it doesn't lose anything.
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 only the claimed items.* Never touch foreign or unowned items.
4. *Commit the roam repo as its own commit* (separate from any project wrap commit): =chore(inbox): route <project> tasks to <project>/todo.org=. 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. 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 and no related-unowned roam items → 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-inbox.org= when needed. It also reads =~/org/roam/inbox.org= and produces the roam scan summary; Phase C surfaces one line: "Roam inbox: N items total, M appear related to this project — say 'inbox zero' to file them." Offered as one of the priority options. Startup never auto-files roam items; it counts and offers.
** 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-inbox= gate still decides whether to file it). This turns "inbox zero" from a per-project aspiration into a single command. Open: where the domain map lives (central registry vs each project's =notes.org=), 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.
|