aboutsummaryrefslogtreecommitdiff
path: root/.ai/workflows
diff options
context:
space:
mode:
Diffstat (limited to '.ai/workflows')
-rw-r--r--.ai/workflows/inbox.org25
-rw-r--r--.ai/workflows/startup.org2
-rw-r--r--.ai/workflows/triage-intake.cmail.org12
-rw-r--r--.ai/workflows/wrap-it-up.org2
4 files changed, 27 insertions, 14 deletions
diff --git a/.ai/workflows/inbox.org b/.ai/workflows/inbox.org
index 6faa20f..914433d 100644
--- a/.ai/workflows/inbox.org
+++ b/.ai/workflows/inbox.org
@@ -103,7 +103,7 @@ The full question-battery answers live in the session log and the =working/= dir
* 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.
+Every item that clears the value gate gets exactly one disposition: *implement now*, *fold into an existing TODO*, *file as a TODO*, or *reject*. *Park* is the no-approvals shared-asset path from core §2. Whichever it gets, nothing stays in =inbox/=.
** 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=.
@@ -124,8 +124,17 @@ On a =<destination>\tstrong= or =<destination>\tweak= result, stamp the new TODO
*Blocking-dependency handoff.* A special shape: another project sends a note that *this* project's work is blocking one of theirs ("your task X is blocked on us — we need Y"). File or link the owning task, tag it =:blocker:=, and name the requesting project in the body (see the cross-project dependency convention in =todo-format.md=). The =:blocker:= tag makes =open-tasks.org= surface that task *first*, since clearing it unblocks the other project. Dedup against an existing task rather than filing a duplicate. When the work later lands, drop =:blocker:= and notify the waiting project (=inbox-send <their-project> --text "Delivered: <what> — you're unblocked."=) so it can lift its own =:blocked:=.
-** 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.
+** Deferral is not a disposition: file it or delete it
+There is no rung that leaves an item in =inbox/= under another name. An item whose work waits is a *task*, so file it (core §3 "File as TODO") with the waiting condition in its body. An item worth keeping for reference is *content*, so move it to =docs/design/= and link it from whatever task cites it. An item that is neither gets deleted. Every path ends with the inbox file gone.
+
+The retired rung renamed deferred items in place to =inbox/PROCESSED-<original>=, and it failed twice in ways worth naming, because the shape recurs:
+
+1. *It accumulated.* The rung told you not to let deferrals pile up and gave that instruction no enforcement, so nothing surfaced them and nothing owned them. Rulesets reached 40 before a manual sweep on 2026-07-27, and the rest of the fleet was still carrying 114 when this rung was removed on 2026-08-20.
+2. *It lost the content it was meant to preserve.* A =PROCESSED-= file is not a durable home, so a task citing one as its source breaks the moment somebody sweeps. Rulesets' =todo.org= carried exactly that dangling reference.
+
+Both follow from one defect: =PROCESSED-*= was a third state that is neither a tracked task nor gone. It sat in =inbox/=, hidden from =inbox-status=, absent from =todo.org=, and carried by no review cycle. Filing and deleting are the two states that already have homes and cadences, which is why they are the only two on offer.
+
+*Legacy files.* A =PROCESSED-*= file predating this change is not grandfathered. It is an undispositioned item. Give it the same choice as anything else: file it, move its content to =docs/design/=, or delete it.
** 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.
@@ -188,13 +197,13 @@ The point is to avoid adding ungraded =TODO= entries to a project that's never a
* 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.
+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 the =lint-followups.org= pipeline artifact), 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).
+1. List =inbox/= excluding =.gitkeep= (use =\ls -la inbox/= per the protocols.org exa-alias note). A =PROCESSED-*= file from before the deferral rung was retired is a legacy item, not an artifact: list it and disposition it like any other.
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.
@@ -245,7 +254,7 @@ Disposition recommendation: <implement / fold into <TODO> / file [#X] :tags: / r
1. <recommendation as item 1>
2. <alternative>
-3. Defer — leave in inbox under PROCESSED-<topic>.<ext> until <condition>
+3. Delete — nothing worth filing and no reference worth keeping
4. Something else
#+end_example
@@ -259,7 +268,7 @@ Apply each disposition per the ladder (core §3). The flow is autonomous past Cr
** Phase E — Close out
-Verify =inbox/= is empty (excluding =.gitkeep= and any intentional =PROCESSED-*= files). Run =\ls -la inbox/= and confirm.
+Verify =inbox/= is empty (excluding =.gitkeep= and =lint-followups.org=). Run =\ls -la inbox/= and confirm. Nothing is left behind under a renamed prefix, because deferral-in-place is not a disposition.
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.
@@ -356,6 +365,8 @@ Close the loop per the reply-to-sender discipline (core §4): confirm what lande
=.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.
+*The =PROCESSED-*= exclusion is transitional, not a supported state.* Nothing creates such a file any more, so the exclusion can only ever match a legacy item from before the deferral rung was retired. It stays for one reason: 114 of them were sitting across five projects on 2026-08-20, and dropping the exclusion would have made every one pending at once, blocking the next session of each project behind a cleanup it didn't ask for. The sweep is tracked as its own task instead. When it finishes, the exclusion comes out of this script, the wrap-up check, and =scripts/status.sh= together, and the ladder's "not grandfathered" rule stops needing a caveat.
+
** Close out — before finishing
End the way it started: clean worktree, green suite. Before stopping the loop or reporting the pass done:
diff --git a/.ai/workflows/startup.org b/.ai/workflows/startup.org
index bc89256..d52add6 100644
--- a/.ai/workflows/startup.org
+++ b/.ai/workflows/startup.org
@@ -205,7 +205,7 @@ Notes on what =sync-templates= does (the rsync behavior it carries):
- The =scripts/= sync excludes Python build artifacts (=__pycache__/=, =.pytest_cache/=, =*.pyc=). Running rulesets' own pytest leaves these in =claude-templates/.ai/scripts/tests/=, and =rsync -a= copies by disk presence regardless of =.gitignore=, so without the excludes every consuming project's tree gets polluted with machine-specific cache files. The excludes also protect existing dest copies from =--delete= cleanup, so a project that already received the cache must remove it once by hand.
- The sync is guarded to skip when rulesets has uncommitted changes under the synced source paths. =rsync -a --delete= copies the working tree by disk presence, so without the guard a downstream session started while rulesets had in-flight WIP would pull that WIP into its =.ai/workflows/= and =.ai/scripts/=, surfacing as drift the user never authored (and tempting a fake "chore: sync .ai tooling" commit). The guard is scoped to the synced paths, not the whole repo, so unrelated rulesets dirt doesn't block the sync. From the jr-estate handoff 2026-05-29.
- The sync is also guarded to skip when the *project* branch is behind its upstream (=proj_behind=). Phase A.0 correctly declines to fast-forward a diverged or behind-and-dirty branch, but the rsync would then land templates on the stale committed =.ai/= baseline — a huge diff measured against old content that conflicts once the branch reconciles to upstream's newer templates. Skipping is safe: the sync runs next session once the branch is current. Not an auto-discard — startup never =git checkout=s drift away, because a legitimate local stopgap in a synced file is indistinguishable from accidental drift by content alone (home reverted an intentional =flashcard-to-anki.py= fix this way on 2026-06-22). Prevention is safe; blind cleanup-after is not. Phase C's template-sync-churn safety net still surfaces any pre-existing dirt for a human decision. From the home handoff 2026-07-04.
-- The sync touches only =protocols.org=, =workflows/=, and =scripts/=. The project-owned dirs =project-workflows/= and =project-scripts/= are deliberately *outside* the synced set, so a project's own workflows and scripts survive startup. This is why a project script that a workflow imports must live in =.ai/project-scripts/=, never =.ai/scripts/= — the latter is wiped to match the template by =--delete= on every startup. Naming: a script imported as a Python module needs an importable name (underscores, e.g. =zlibrary_api.py=); a CLI-invoked script can stay kebab-case like the template tooling (=cmail-action.py=).
+- The sync touches only =protocols.org=, =workflows/=, and =scripts/=. The project-owned dirs =project-workflows/= and =project-scripts/= are deliberately *outside* the synced set, so a project's own workflows and scripts survive startup. This is why a project script that a workflow imports must live in =.ai/project-scripts/=, never =.ai/scripts/= — the latter is wiped to match the template by =--delete= on every startup. Naming: a script imported as a Python module needs an importable name (underscores, e.g. =zlibrary_api.py=); a CLI-invoked script can stay kebab-case like the template tooling (=inbox-status=).
Rationale: Every call in Phase A is read-only or writes to a distinct path. Running them sequentially wastes round-trips; running them in parallel gives Claude the complete starting picture in one round-trip.
diff --git a/.ai/workflows/triage-intake.cmail.org b/.ai/workflows/triage-intake.cmail.org
index 8d8abfb..66ecf80 100644
--- a/.ai/workflows/triage-intake.cmail.org
+++ b/.ai/workflows/triage-intake.cmail.org
@@ -8,7 +8,7 @@
* Source: cmail
:PROPERTIES:
:ORDER: 25
-:ENABLED: test -f .ai/scripts/cmail-action.py
+:ENABLED: command -v cmail-action
:ANCHOR: none
:SUBAGENT_OVER: 50
:END:
@@ -18,7 +18,7 @@
Proton (=c@cjennings.net=) via the bridge script. =ANCHOR: none= because this reports live IMAP unread *state*, not a since-window — the engine substitutes no cutoff. Phase B uses the anchor only to flag which of the current unread arrived since last check.
#+begin_src bash
-python3 .ai/scripts/cmail-action.py list-unread
+cmail-action list-unread
#+end_src
JSON output, keyed by UID. The script ignores messages already flagged =\Deleted= (those are pending-flush on the next Proton sync), so the list is the genuinely-live unread set.
@@ -47,7 +47,7 @@ Omit if zero unread.
All take one or more UIDs (from the =list-unread= JSON):
-- mark-read :: =python3 .ai/scripts/cmail-action.py mark-read <uid>=
-- star :: =python3 .ai/scripts/cmail-action.py star <uid>=
-- unstar :: =python3 .ai/scripts/cmail-action.py unstar <uid>=
-- trash :: =python3 .ai/scripts/cmail-action.py trash <uid>= (flags =\Deleted=; flushed on next Proton sync)
+- mark-read :: =cmail-action mark-read <uid>=
+- star :: =cmail-action star <uid>=
+- unstar :: =cmail-action unstar <uid>=
+- trash :: =cmail-action trash <uid>= (flags =\Deleted=; flushed on next Proton sync)
diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org
index ecd3d22..c3e8ce2 100644
--- a/.ai/workflows/wrap-it-up.org
+++ b/.ai/workflows/wrap-it-up.org
@@ -325,6 +325,8 @@ If the count is zero or the project has no =inbox/= directory, the check is a si
The check exempts =lint-followups.org= explicitly because lint-org runs earlier in the same wrap-up workflow and writes its judgment items to that file in =inbox/= by design. The file is a pipeline artifact for the next morning's =daily-prep=, not a handoff that needs the value gate.
+It also exempts =PROCESSED-*=, and that one is *transitional rather than principled*. Renaming an item in place to defer it was retired from the disposition ladder on 2026-08-20, so nothing creates such a file now and the pattern can only match a legacy item. It survives here only because 114 of them were spread across five projects that day, and counting them as unprocessed would have blocked five wraps behind a cleanup nobody asked for. A sweep task owns clearing them. When it lands, this exemption comes out together with the matching lines in =.ai/scripts/inbox-status= and =scripts/status.sh=, and a stray =PROCESSED-= file correctly starts blocking the wrap.
+
This integrates with =inbox.org= process mode, which stamps =:LAST_INBOX_PROCESS:= in =notes.org='s *Workflow State* section on completion. Wrap-up doesn't double-stamp. It only ensures the inbox carries nothing but the expected pipeline artifacts at session end.
*** Cross-project router (optional — route filed keepers to their home projects)