diff options
Diffstat (limited to 'claude-rules/todo-format.md')
| -rw-r--r-- | claude-rules/todo-format.md | 315 |
1 files changed, 294 insertions, 21 deletions
diff --git a/claude-rules/todo-format.md b/claude-rules/todo-format.md index b1fb57b..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 @@ -24,8 +79,8 @@ guessing: The section is mandatory. A `todo.org` without it leaves `[#A]` and the tags undefined, so task-audit can't enforce a vocabulary, task-review can't grade -against agreed semantics, and process-inbox can't file new tasks correctly -(its Phase B.1 already checks for this scheme). Each project defines the +against agreed semantics, and the inbox workflow can't file new tasks correctly +(its priority-scheme check already gates on this scheme). Each project defines the scheme its own way; the floor is that priorities and tags are both spelled out under the header. @@ -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,11 +328,13 @@ 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. -`VERIFY` is the documented exception: it follows the dated-rewrite rule at **all** depths (including `**`), because a resolved VERIFY is an answered question rather than a finished task. See the VERIFY section below. +`VERIFY` follows the dated-rewrite rule at `***` and deeper, the same as any sub-task. At `**` it does *not*: a top-level VERIFY completes task-shaped — a `DONE`/`CANCELLED` keyword plus a `CLOSED:` line, exactly like a top-level `TODO`. Dated headers never appear at `**`. Level 2 always carries a terminal keyword; dated headers are a `***`-and-deeper shape only. See the VERIFY section below. ## VERIFY tasks @@ -191,19 +395,35 @@ The sibling rule is the active force that keeps `todo.org` flat. Without it, VERIFYs accumulate one level deeper than their trigger every time — turning a clean parent tree into a long pole of nested sub-headings. -### Completion — dated rewrite + content replacement +### Completion — depth decides the heading shape + +When a VERIFY resolves, **rewrite the heading and body together**. The body +replacement is the same at every depth (step 2 below); the heading shape +depends on the VERIFY's level, mirroring the depth-based rule for ordinary +tasks — dated entries at `***` and deeper, terminal keyword at `**`. + +1. **Replace the heading — by depth.** + + - **At `***` and deeper — dated event-log entry.** Drop the `VERIFY` + keyword (and any priority cookie / tags) and replace with a timestamp + + short description: -When a VERIFY resolves, **rewrite the heading and body together** at the -same depth — regardless of whether the VERIFY is at `**` or `***`: + *** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done> -1. **Replace the heading.** Drop the `VERIFY` keyword (and any priority - cookie / tags) and replace with a timestamp + short description: + 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. - *** 2026-05-15 Fri @ 14:00:00 -0500 <what was answered or done> + - **At `**` — terminal keyword, like any top-level task.** Change + `VERIFY` to `DONE` (answered / check passed) or `CANCELLED` (abandoned), + keep the heading text, priority cookie, and tags, and add a + `CLOSED: [YYYY-MM-DD Day]` line. Never a dated heading — a `**` dated + header is a defect; repair it to `DONE`/`CANCELLED` + `CLOSED:`. - Generate the timestamp with `date "+%Y-%m-%d %a @ %H:%M:%S %z"`. - Match the original depth (a `**` VERIFY becomes `** YYYY-MM-DD ...`; - a `***` VERIFY becomes `*** YYYY-MM-DD ...`). + ** DONE [#B] <original VERIFY topic> :tags: + CLOSED: [2026-05-15 Fri] 2. **Replace the body.** Drop the original question/instruction prose and replace with either: @@ -213,16 +433,18 @@ same depth — regardless of whether the VERIFY is at `**` or `***`: instruction or pending-decision marker — what was done, when, where the artifact lives). -The completed VERIFY becomes an in-place event log entry. The original -question is preserved by the dated heading + body shape; anyone scanning -the agenda or `git log` can see what was asked and what landed. +Either way the completed VERIFY records what was asked and what landed: at +`***` and deeper as a dated event-log entry, at `**` as a `DONE`/`CANCELLED` +task whose body holds the answer. Anyone scanning the agenda or `git log` +can see both. -**Note on the top-level case.** Regular `**` DONE tasks stay task-shaped -with a `DONE` keyword + `CLOSED:` line per *Completion — depth-based* -above. VERIFYs at `**` are the exception — they convert to dated log -entries on completion because a resolved VERIFY isn't a "done task," it's -an answered question. The dated-rewrite rule wins for VERIFYs at all -depths. +**Note on the top-level case.** A `**` VERIFY completes exactly like a `**` +`TODO`: a `DONE`/`CANCELLED` keyword + `CLOSED:` line, with the answer or +action in the body. The earlier habit of dating a resolved top-level VERIFY +— treating "answered question, not a finished task" as license for a `**` +dated header — is retired. It put dated headers at level 2, where the agenda +truncates them out of a clean keyword scan. Dated rewrite is for `***` and +deeper only; `**` always carries a terminal keyword. ### Don't leave stale placeholders @@ -249,3 +471,54 @@ are noise that pollute his `cj:` greps. ** DOING [#A] Kostya's contract :admin:kostya: *** 2026-05-15 Fri @ 14:00:00 -0500 Kostya basis — part-time, 20 hr/week Nerses confirmed 5/15 13:30 CDT: Kostya runs at 20 hr/week part-time, mirroring Vrezh's structure. Plugged into Exhibit A § 2 of the contract draft. + +## Cross-Project Dependency Tags + +A task can be blocked by work that has to happen in a *different project* — a rulesets task that can't finish until `.emacs.d` ships a companion function, say. Left unmarked, two things go wrong: the what's-next workflow keeps recommending the blocked task even though it can't move, and the blocker sits at low priority in the other project, so the dependency stalls silently. + +Two plain org tags track it, one on each side, so neither the waiter nor the blocker loses sight of the dependency: `:blocked:` on the task that's waiting, `:blocker:` on the task that owes the work. The cross-project detail — which project, what work — goes in the task *body*, not a property. This applies to *any* project pair; the convention here and the surfacing in `open-tasks.org` live in the shared rule + workflow layer, not in one project. + +### `:blocked:` — the waiting side + +The task that can't proceed carries `:blocked:`. Its body names the project it's waiting on and what that project owes: + +``` +** DOING [#B] Wrap-teardown feature :feature:blocked: +Blocked on emacsd: needs the ai-term companion functions +(cj/ai-term-quit, -live-count) before the manual validation can run. +``` + +`open-tasks.org` reads the `:blocked:` tag to pull the task out of the "do this next" cascade (it can't be worked) and surface it in a dedicated "Blocked on other projects" section, reading the body for which project to name and nudge. + +### Registering with the blocker — the reciprocal handoff (required) + +Setting `:blocked:` is not complete until the blocking project knows it's blocking. The moment you mark a task `:blocked:` on another project's work, send that project a dependency handoff: + +``` +inbox-send <project> --text "Blocking dependency: <this-project>'s task \"<task>\" is blocked on you — it needs <what>. It stays blocked until this lands. Tag the owning task :blocker: on your side so it surfaces as priority work." +``` + +This is what closes the gap: without it, the blocker only learns it's blocking by accident. The handoff lands in `<project>`'s `inbox/` and its normal inbox processing tags the work (below). A `:blocked:` task with no matching reciprocal handoff is half-done — the dependency is invisible to the one project that can clear it. Skip the send only when the blocker demonstrably already tracks the work (e.g. it's the same handoff that spawned the dependency); it dedups against an existing task either way. + +### `:blocker:` — the blocking side + +When a project processes a blocking-dependency handoff (inbox process mode), it tags the owning task `:blocker:` and names the requesting project in the body: + +``` +** TODO [#B] ai-term wrap-teardown companion :feature:blocker: +Rulesets' wrap-teardown feature is blocked on this — it needs the three +ai-term functions. Surface first so rulesets unblocks. +``` + +The blocking task does *not* carry `:blocked:` — it isn't blocked, it's the blocker. `:blocker:` is a priority signal: `open-tasks.org` surfaces a `:blocker:` task *first*, since clearing it unblocks work in another project, so a dependency that would otherwise stall at low priority gets pulled forward. This is the "surface dependencies first" half of the design. + +### Resolving the dependency + +When the blocker delivers: + +1. The blocking project completes its `:blocker:` task, drops the `:blocker:` tag, and notifies the waiter (`inbox-send <waiter> --text "Delivered: <what> — you're unblocked."`). +2. The waiting project drops the `:blocked:` tag; the task is workable again. Either side noticing the delivery can lift its own tag — the notification just makes it prompt. + +### Not the same as VERIFY + +`:blocked:` marks "waiting on another *project's* work"; `VERIFY` marks "waiting on Craig's input." If Craig's input is what's needed, it's a VERIFY, not `:blocked:`. And `:blocker:` only ever sits on the project that *owes* the work, never the one waiting. |
