aboutsummaryrefslogtreecommitdiff
path: root/claude-rules/todo-format.md
diff options
context:
space:
mode:
Diffstat (limited to 'claude-rules/todo-format.md')
-rw-r--r--claude-rules/todo-format.md208
1 files changed, 208 insertions, 0 deletions
diff --git a/claude-rules/todo-format.md b/claude-rules/todo-format.md
index 5c34966..8038b98 100644
--- a/claude-rules/todo-format.md
+++ b/claude-rules/todo-format.md
@@ -1,3 +1,8 @@
+---
+paths:
+ - "**/*.org"
+---
+
# Todo Entry Format
Applies to: `**/*.org` (org-mode todo and inbox files)
@@ -5,6 +10,56 @@ Applies to: `**/*.org` (org-mode todo and inbox files)
How task entries are structured in org-mode todo files (`todo.org`,
`inbox.org`, any GTD-style org file). Same shape across every project.
+## Stamp `:LAST_REVIEWED:` when you create the task, not a cycle later
+
+Every task filed at `**` with a priority cookie carries a `:LAST_REVIEWED:`
+property from the moment it's written:
+
+```
+** TODO [#B] Terse topic phrase :tag:
+:PROPERTIES:
+:LAST_REVIEWED: 2026-07-23
+:END:
+Body.
+```
+
+Use today's date, from `date +%F`. The org-native `[YYYY-MM-DD Day]` form is
+equally valid; both parse.
+
+**Why.** Writing a task *is* reviewing it. Whoever files it has just written
+the body, chosen the wording, and graded the priority against the scheme — that
+is the same judgment `task-review` applies, made with better context, because
+the reason for the task is still in the room. Leaving the stamp off asserts the
+opposite: `task-review-staleness.sh` sorts a missing property first, as
+never-reviewed, so a task filed today arrives at the top of tomorrow's review
+batch and gets "reviewed" by someone re-deriving what its author knew a day
+earlier. That is ceremony, and ceremony teaches people to click through the
+real thing.
+
+The concrete case: the 2026-07-23 sweep filed eight tasks in one night. Every
+one landed unstamped, and the staleness count went 13 → 22 while the list got
+*more* accurate, not less. The number stopped measuring drift and started
+measuring recent activity.
+
+**This applies to every path that files a task**, not just the inbox: inbox
+filing, triage intake, spec decomposition, task audit, a bug found mid-session,
+a task you write by hand. If you wrote a task body today, stamp it today.
+
+**What it does not do.** The stamp never means "correct forever" — it means
+"a person judged this on that date." A task filed today still enters the
+review rotation on the normal cycle; it just enters it on the *next* cycle
+rather than immediately. And it's a claim about a real event, so don't stamp
+a task you didn't actually consider: a bulk import of someone else's list
+is genuinely unreviewed, and stamping it would convert "nobody has read
+these" into a false "reviewed today."
+
+**Enforcement.** `lint-org.el`'s `task-missing-last-reviewed` checker flags any
+open `**` task with a priority cookie and no stamp, scoped to exactly the
+headings `task-review-staleness.sh` selects, so the checker and the staleness
+count never disagree. It's judgment-only and never auto-fixes: nothing can know
+when an unstamped task was actually last considered, and writing today's date
+onto an old one would destroy the very signal the property carries.
+
## Priority and Tag Scheme Header
Every project's `todo.org` opens with a top-level section named
@@ -33,6 +88,152 @@ When a project's `todo.org` lacks the section, add it before filing or
grading further tasks — propose the priority semantics and tag set from the
project's existing usage, and confirm with Craig.
+### Hard definitions: `:solo:` and `:quick:` (fixed across projects)
+
+A project's scheme may add or rename its other tags, but these two carry
+fixed definitions everywhere, because autonomous execution
+(work-the-backlog / the no-approvals speedrun) reads `:solo:` as its
+eligibility gate and trusts the author's tag rather than re-deriving
+autonomy at run time.
+
+“Speedrunnable” is shorthand for `:solo:`. The `:quick:` tag plays no part
+in that definition.
+
+- **`:solo:` — autonomy.** The task can be completed *and verified* without
+ Craig's involvement beyond at most one or two quick decisions that can be
+ stated and answered before work starts. No open design question, no
+ "weigh these approaches," no waiting on Craig mid-task. Three gates, all
+ must hold: *buildable* (the agent has the capability and access),
+ *verifiable by the agent* (an objective or local check it can run itself —
+ handing off a residual human-in-the-loop confirmation as a structured
+ manual-testing reminder does not disqualify), and *no deliberation* (a
+ quick, upfront-answerable factual question is allowed — it gets batched
+ into the speedrun's pre-flight Q&A; a genuine design or preference call
+ is not). A wrong `:solo:` is worse than none: it tells Craig he can hand
+ the task off and walk away when he can't.
+- **`:quick:` — effort hint only.** Likely 30 minutes or less from start
+ through verification. Informational, for batching and estimating a run's
+ duration; never an eligibility gate. `:quick:` and `:solo:` are
+ orthogonal — a bounded refactor can be `:solo:` but slow; a five-minute
+ change hinging on a preference call is `:quick:` but not `:solo:`.
+
+Both tags are applied at task creation and **re-checked as a mandatory
+step** in the task-review and task-audit workflows, so the run-time gate
+can trust the tag. A review or audit that skips the `:solo:`/`:quick:`
+assessment is incomplete.
+
+### Making an open-ended task measurable (so it can be `:solo:`)
+
+A task phrased as the *absence* of something — "find bugs until none are
+visible," "refactor until no worthwhile opportunities remain," "clean this
+up until it's good" — cannot be `:solo:`, because it fails the
+*verifiable-by-the-agent* gate. Absence isn't falsifiable: an agent can
+always look once more, so "done" is a judgment call, which is exactly what
+`:solo:` forbids. The fix is not to drop the task but to give it an
+objective completion criterion. Four moves convert a fuzzy goal into a
+measurable one:
+
+1. **Bound the surface.** Enumerate the concrete units the task covers (the
+ N functions, the M files, the named code paths). The done-set is that
+ list, not the platonic set of all possible defects. Every claim is made
+ against the list, so "covered" is checkable where "found everything"
+ isn't.
+2. **Net the behavior.** Bring the surface under characterization tests
+ (Normal/Boundary/Error per unit — see `testing.md`, and the
+ `testing-standards` skill for the characterization recipe) before changing
+ anything. This is the objective floor: writing a characterization test is
+ mechanical (record what the code does, not what it should), so it scales
+ across the surface, and it doubles as the safety net that makes any
+ later refactor falsifiable.
+3. **Disposition every finding.** Run the relevant audits (a fixed
+ footgun/OWASP checklist, `/refactor`, `/review-code`) and give **every**
+ finding a verdict: fixed, filed as its own task, or declined with a
+ one-line reason. "Looked and it's fine" is not a disposition. The
+ measurement is zero undispositioned findings, not zero findings.
+4. **Gate on an objective floor.** Static analysis clean (linter,
+ type-checker, `shellcheck`), the test suite green before and after, and
+ coverage of the enumerated surface (a per-unit test checklist, or a real
+ coverage number where the tooling exists).
+
+The **qualifying answer** is then a dispositioned report — surface split
+covered/uncovered, tests before → after, static-analysis result, the audit
+matrix fully dispositioned, all green — not a claim of perfection. The
+honest limit stays honest ("no visible bugs" means "every enumerated path
+passes its characterization set and clears the audit," never "zero bugs
+exist"), but the criterion is now falsifiable, which is what lets the task
+carry `:solo:`. Write these criteria into the task body at creation or
+review time; a task that can't be given them stays non-`:solo:` until it
+can.
+
+### Bug priority from severity × frequency (mandatory where a codebase exists)
+
+Some projects carry a codebase — source the project maintains under version
+control — even when the project isn't primarily a "code project." home and work
+both have one. Wherever a project has a codebase, a task representing a bug
+*against that codebase* does not get its priority argued: it is **dictated** by
+the severity × frequency matrix below. This is not opt-in. Features keep their
+per-project `[#A]`–`[#D]` roadmap judgment; codebase bugs do not.
+
+Two facts set a bug's priority:
+
+- **Severity** — how bad it is when it occurs (service down / data loss /
+ security or privacy leak at one end; a cosmetic nit at the other).
+- **Frequency** — how often a user hits it (every user every time → rare edge
+ case).
+
+```
+| Frequency / Severity | Critical | Major | Minor | Cosmetic |
+|------------------------+----------+-------+-------+----------|
+| Every user, every time | P1 | P1 | P2 | P3 |
+| Most users, frequently | P1 | P2 | P3 | P4 |
+| Some users, sometimes | P2 | P3 | P3 | P4 |
+| Rare edge case | P2 | P3 | P4 | P4 |
+```
+
+P-level → priority letter (fixed — not a per-project knob):
+
+- P1 → `[#A]`
+- P2 → `[#B]`
+- P3 → `[#C]`
+- P4 → `[#D]`
+
+Release vehicle is illustrative and project-dependent: P1 = current
+release/patch, P2 = next patch, P3 = next major, P4 = backlog. A project with no
+release train maps the letters and skips the vehicle. A "no open `[#A]` bugs"
+release gate therefore means "no open P1."
+
+Each project still defines, in its scheme header, what
+Critical/Major/Minor/Cosmetic and the frequency rows mean *for its own
+codebase* — the bands are concrete per codebase, but the matrix structure and
+the letter mapping are fixed.
+
+**Severity-alone carve-out:** privacy or security leaks, compliance violations,
+and safety issues are graded on severity alone — one occurrence with the right
+consequences is a showstopper no matter how rarely it would be hit.
+
+**Don't double-count rarity.** Grade severity by the rate of harm once the
+failure state is entered, not by how rare it is to enter. Frequency already
+carries the rarity; letting it discount severity too grades the same fact twice,
+and that buries exactly the bugs that compound — the ones where a rare trigger
+produces unbounded harm. A leak that repeats every timeout period until the
+process restarts is Major even when reaching that state is a rare edge case
+("accumulates slowly" describes a bounded trickle, not a fixed-rate leak with no
+workaround). Grade the *being-in-it*, and let the frequency row carry the
+*getting-into-it*.
+
+**Record the grading in the task body.** State the severity band, the frequency
+row, and the arithmetic (e.g. "Major severity × rare edge case = P3 = [#C]"). A
+bare priority cookie can't be argued with; a stated read can be re-checked
+against the source and corrected. This is what lets a misgrade move — a chime
+watchdog bug went [#D] → [#C] an hour after grading precisely because the read
+was written down and re-checked.
+
+**Disagreeing with a grade means fixing an input.** If a letter looks wrong,
+don't override the letter — re-read the severity band and the frequency row
+against the source and correct whichever input is wrong. Overriding the cookie
+directly turns the matrix into a formality and puts you back to grading by
+instinct, which is the thing it exists to replace.
+
## The Rule
A todo entry has two parts:
@@ -117,6 +318,7 @@ A completed sub-task disappears as a task and becomes an in-place event-log entr
2. Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`.
3. Reword the original imperative title into the past-tense action that landed. Trim or restate if the original wording doesn't fit the action.
4. Drop the `TODO`/`DOING` keyword, the priority cookie, and the tags. The body stays as the record of what was done (if useful).
+5. Remove any `SCHEDULED:`/`DEADLINE:` planning line. The completion time lives in the heading now, so `CLOSED:` is redundant and an active planning date on a historical log entry is always wrong. Org renders any headline carrying an active `<...>` `SCHEDULED`/`DEADLINE` on the agenda, keyword or not, so a stale one pins the finished entry there as weeks-overdue forever. An interactive close (`org-log-done`) stamps `CLOSED:` but never strips a pre-existing planning line, which is exactly how the stale dates survive.
**Example:**
@@ -126,6 +328,8 @@ becomes
*** 2026-05-15 Fri @ 12:58:08 -0500 Wired yasnippet for universal availability
+**Enforcement.** This is applied at close time by whoever closes the task, but an interactive org close (`org-log-done` flips the keyword to `DONE` and stamps `CLOSED:`) never applies the dated rewrite, so level-3+ closes accumulate as `DONE` keywords. `todo-cleanup.el --convert-subtasks` (run in the `clean-todo` and wrap-up cleanup passes) normalizes them mechanically: it rewrites any level-3+ `DONE`/`CANCELLED`/`FAILED` heading into the dated form above, pulling the timestamp from the `CLOSED` cookie, dropping the whole planning line (`CLOSED`, `SCHEDULED`, and `DEADLINE` together — step 5), and keeping the heading text verbatim (a batch tool can't reliably past-tense a title — polish wording by hand where it matters). `lint-org.el` flags any that slip through: checker `subtask-done-not-dated` for a still-keyworded sub-task, and `dated-log-heading-active-timestamp` for a dated entry that kept an active `SCHEDULED`/`DEADLINE`. So the depth rule holds even when tasks are closed interactively rather than by an agent applying this section.
+
### Why depth-based
The agenda view (`org-agenda`) shows entries at the section + top-task level. Letting `**` tasks stay task-shaped preserves their visibility as "things that recently shipped." Letting `***+` sub-tasks flip to dated entries keeps the agenda from being clogged with a long list of completed sub-tasks at every depth — those become history within their parent instead.
@@ -207,6 +411,10 @@ tasks — dated entries at `***` and deeper, terminal keyword at `**`.
*** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done>
Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`.
+ Remove any `SCHEDULED:`/`DEADLINE:` planning line too, same as the
+ sub-task rule above — a dated event-log entry carries its date in the
+ heading, and an active planning date left on it pins the finished entry
+ to the agenda forever.
- **At `**` — terminal keyword, like any top-level task.** Change
`VERIFY` to `DONE` (answered / check passed) or `CANCELLED` (abandoned),