aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-20 13:41:13 -0400
committerCraig Jennings <c@cjennings.net>2026-05-20 13:41:13 -0400
commit3627d5e61e2adc8cd196958f62579fcdf9d20889 (patch)
tree414ff2f1968a19c4fa4becdb702dbe027357434d
parent845fbd5753a3ca13e9d348d2ef84da8cb9ee2b3a (diff)
downloadrulesets-3627d5e61e2adc8cd196958f62579fcdf9d20889.tar.gz
rulesets-3627d5e61e2adc8cd196958f62579fcdf9d20889.zip
docs(workflows): swap wrap-up date-coverage scan for task-review health check
The date-coverage scan flagged every open [#A]/[#B] task with no DEADLINE or SCHEDULED, on the assumption that high-priority work needs a date. That assumption is wrong (research and watch-list tasks are legitimately dateless), so the scan generated dismiss-or-paper-over cleanup at every wrap-up. The replacement watches the daily task-review habit instead. It calls task-review-staleness.sh todo.org 30 and, when the count is non-zero, writes one summary line to the follow-ups file: N top-level tasks unreviewed for >30 days. There's no per-task dump, because the per-task walk is the review habit's job. Staleness, not datelessness, is the signal worth surfacing. The change lands in both the canonical workflow and the project mirror.
-rw-r--r--.ai/workflows/wrap-it-up.org33
-rw-r--r--claude-templates/.ai/workflows/wrap-it-up.org33
2 files changed, 20 insertions, 46 deletions
diff --git a/.ai/workflows/wrap-it-up.org b/.ai/workflows/wrap-it-up.org
index e2a535b..fc8c398 100644
--- a/.ai/workflows/wrap-it-up.org
+++ b/.ai/workflows/wrap-it-up.org
@@ -174,11 +174,11 @@ Preview without writing — same flags as =--check= on the other scripts:
The wrap-up never blocks on judgment items — they're deferred by design.
For an interactive walk of the judgments mid-day, run =/lint-org todo.org=.
-*** Date-coverage scan (surface =[#A]= / =[#B]= tasks lacking a timestamp)
+*** Review-habit health check (surface a slipped daily task-review)
-Scan =todo.org= for open =[#A]= and =[#B]= tasks that have neither a =DEADLINE:= nor a =SCHEDULED:= line directly under the heading, and surface the candidates to the follow-ups file so the morning's daily-prep flags them for review.
+The daily task-review habit walks the open top-level tasks on a rotating cycle, stamping =:LAST_REVIEWED:= as it goes (see =task-review.org=). This check is the watchdog for that habit. When tasks have gone too long unreviewed, the habit has slipped, and the wrap-up says so in one line — it does not re-list the tasks.
-The two timestamps mean different things (=DEADLINE:= = external, consequence-bearing; =SCHEDULED:= = social, accountability-bearing — see the priority spec at the top of =todo.org=). High-priority work that carries neither is suspicious: either it has an implicit deadline that should be made explicit, or it has someone waiting that should surface in the agenda, or its priority is wrong. The scan flags candidates; the operator decides.
+=task-review-staleness.sh= counts top-level =[#A]= / =[#B]= / =[#C]= tasks (TODO/DOING/VERIFY) whose =:LAST_REVIEWED:= is missing or older than the threshold. Threshold 30 days is about 2.5 review cycles of slack at the default batch size — one missed week is fine, three weeks signals a problem.
#+begin_src bash
if [ -n "$LINT_ORG_FOLLOWUPS" ]; then
@@ -188,29 +188,16 @@ elif [ -d "./inbox" ]; then
else
followups=".ai/lint-followups.org"
fi
-[ -f todo.org ] && awk '
- /^\*\* (TODO|DOING|VERIFY) \[#[AB]\]/ {
- if (heading != "" && !has_date) print line ": " heading
- heading = $0
- line = NR
- has_date = 0
- next
- }
- /^(SCHEDULED|DEADLINE|CLOSED):/ { has_date = 1; next }
- /^\*+ / { if (heading != "" && !has_date) print line ": " heading; heading = ""; next }
- END { if (heading != "" && !has_date) print line ": " heading }
-' todo.org > /tmp/date-coverage.out
-if [ -s /tmp/date-coverage.out ]; then
- {
- printf "\n* %s — Date coverage: [#A] / [#B] tasks without DEADLINE or SCHEDULED\n" "$(date '+%Y-%m-%d %a')"
- printf "%s\n" "Review each: add a date, drop the priority, or confirm 'no-date by intent' inline."
- sed 's/^/- /' /tmp/date-coverage.out
- } >> "$followups"
+if [ -f todo.org ]; then
+ stale=$(.ai/scripts/task-review-staleness.sh todo.org 30 2>/dev/null || echo 0)
+ if [ "$stale" -gt 0 ]; then
+ printf "\n* %s — Task-review health: %s top-level [#A]/[#B]/[#C] tasks unreviewed for >30 days (daily review may have slipped)\n" \
+ "$(date '+%Y-%m-%d %a')" "$stale" >> "$followups"
+ fi
fi
-rm -f /tmp/date-coverage.out
#+end_src
-The scan is intentionally conservative — it surfaces every candidate. False positives (tasks that legitimately have no date) are cheap to dismiss; false negatives would let high-priority work drift undated. No-date is a valid resting state for some tasks (research, watch-list), and the operator can mark those as such in the daily-prep review rather than tagging them in =todo.org=.
+A non-zero count writes one summary line and nothing else — the per-task walk is the review habit's job, not the wrap-up's. This supersedes the old date-coverage scan, which flagged every dateless =[#A]= / =[#B]= task on the wrong assumption that high-priority work needs a date. No-date is a valid resting state for research and watch-list tasks; staleness, not datelessness, is the real signal.
** Step 3.5: Linear ticket-state hygiene (skip if project doesn't use Linear)
diff --git a/claude-templates/.ai/workflows/wrap-it-up.org b/claude-templates/.ai/workflows/wrap-it-up.org
index e2a535b..fc8c398 100644
--- a/claude-templates/.ai/workflows/wrap-it-up.org
+++ b/claude-templates/.ai/workflows/wrap-it-up.org
@@ -174,11 +174,11 @@ Preview without writing — same flags as =--check= on the other scripts:
The wrap-up never blocks on judgment items — they're deferred by design.
For an interactive walk of the judgments mid-day, run =/lint-org todo.org=.
-*** Date-coverage scan (surface =[#A]= / =[#B]= tasks lacking a timestamp)
+*** Review-habit health check (surface a slipped daily task-review)
-Scan =todo.org= for open =[#A]= and =[#B]= tasks that have neither a =DEADLINE:= nor a =SCHEDULED:= line directly under the heading, and surface the candidates to the follow-ups file so the morning's daily-prep flags them for review.
+The daily task-review habit walks the open top-level tasks on a rotating cycle, stamping =:LAST_REVIEWED:= as it goes (see =task-review.org=). This check is the watchdog for that habit. When tasks have gone too long unreviewed, the habit has slipped, and the wrap-up says so in one line — it does not re-list the tasks.
-The two timestamps mean different things (=DEADLINE:= = external, consequence-bearing; =SCHEDULED:= = social, accountability-bearing — see the priority spec at the top of =todo.org=). High-priority work that carries neither is suspicious: either it has an implicit deadline that should be made explicit, or it has someone waiting that should surface in the agenda, or its priority is wrong. The scan flags candidates; the operator decides.
+=task-review-staleness.sh= counts top-level =[#A]= / =[#B]= / =[#C]= tasks (TODO/DOING/VERIFY) whose =:LAST_REVIEWED:= is missing or older than the threshold. Threshold 30 days is about 2.5 review cycles of slack at the default batch size — one missed week is fine, three weeks signals a problem.
#+begin_src bash
if [ -n "$LINT_ORG_FOLLOWUPS" ]; then
@@ -188,29 +188,16 @@ elif [ -d "./inbox" ]; then
else
followups=".ai/lint-followups.org"
fi
-[ -f todo.org ] && awk '
- /^\*\* (TODO|DOING|VERIFY) \[#[AB]\]/ {
- if (heading != "" && !has_date) print line ": " heading
- heading = $0
- line = NR
- has_date = 0
- next
- }
- /^(SCHEDULED|DEADLINE|CLOSED):/ { has_date = 1; next }
- /^\*+ / { if (heading != "" && !has_date) print line ": " heading; heading = ""; next }
- END { if (heading != "" && !has_date) print line ": " heading }
-' todo.org > /tmp/date-coverage.out
-if [ -s /tmp/date-coverage.out ]; then
- {
- printf "\n* %s — Date coverage: [#A] / [#B] tasks without DEADLINE or SCHEDULED\n" "$(date '+%Y-%m-%d %a')"
- printf "%s\n" "Review each: add a date, drop the priority, or confirm 'no-date by intent' inline."
- sed 's/^/- /' /tmp/date-coverage.out
- } >> "$followups"
+if [ -f todo.org ]; then
+ stale=$(.ai/scripts/task-review-staleness.sh todo.org 30 2>/dev/null || echo 0)
+ if [ "$stale" -gt 0 ]; then
+ printf "\n* %s — Task-review health: %s top-level [#A]/[#B]/[#C] tasks unreviewed for >30 days (daily review may have slipped)\n" \
+ "$(date '+%Y-%m-%d %a')" "$stale" >> "$followups"
+ fi
fi
-rm -f /tmp/date-coverage.out
#+end_src
-The scan is intentionally conservative — it surfaces every candidate. False positives (tasks that legitimately have no date) are cheap to dismiss; false negatives would let high-priority work drift undated. No-date is a valid resting state for some tasks (research, watch-list), and the operator can mark those as such in the daily-prep review rather than tagging them in =todo.org=.
+A non-zero count writes one summary line and nothing else — the per-task walk is the review habit's job, not the wrap-up's. This supersedes the old date-coverage scan, which flagged every dateless =[#A]= / =[#B]= task on the wrong assumption that high-priority work needs a date. No-date is a valid resting state for research and watch-list tasks; staleness, not datelessness, is the real signal.
** Step 3.5: Linear ticket-state hygiene (skip if project doesn't use Linear)