aboutsummaryrefslogtreecommitdiff
path: root/claude-templates/.ai/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'claude-templates/.ai/scripts')
-rwxr-xr-xclaude-templates/.ai/scripts/task-review-staleness.sh40
-rw-r--r--claude-templates/.ai/scripts/tests/task-review-staleness.bats53
2 files changed, 84 insertions, 9 deletions
diff --git a/claude-templates/.ai/scripts/task-review-staleness.sh b/claude-templates/.ai/scripts/task-review-staleness.sh
index ed43712..50e0257 100755
--- a/claude-templates/.ai/scripts/task-review-staleness.sh
+++ b/claude-templates/.ai/scripts/task-review-staleness.sh
@@ -19,9 +19,14 @@
# deeper headings, and cookie-less headings are not review units.
#
# A task is stale (count mode), and sorts oldest (list mode), when its
-# :LAST_REVIEWED: property is missing or unparseable (NIL sorts first), or
-# when its age strictly exceeds the threshold (age > N days; age == N is
-# still fresh).
+# :LAST_REVIEWED: property is missing (NIL sorts first) or when its age
+# strictly exceeds the threshold (age > N days; age == N is still fresh).
+#
+# :LAST_REVIEWED: accepts a bare date (2026-07-09) or an org-native
+# timestamp ([2026-07-09 Thu] or <2026-07-09 Thu ...>); both normalize to
+# the ISO date. A value that is present but parses to neither is a data
+# error: the script warns loudly to stderr (file:line:value) and leaves it
+# out of the stale count rather than silently treating it as never-reviewed.
set -euo pipefail
@@ -46,7 +51,7 @@ num="$2"
# only the property drawer between a qualifying heading and the next heading
# is scanned.
extract_tasks() {
- awk '
+ awk -v fname="$todo_file" '
function flush() {
if (in_task) printf "%s\t%s\t%s\n", hline, (have_lr ? lr : "NONE"), heading
}
@@ -65,7 +70,21 @@ extract_tasks() {
v = $0
sub(/^[ \t]*:LAST_REVIEWED:[ \t]*/, "", v)
sub(/[ \t]*$/, "", v)
- lr = v; have_lr = 1
+ raw = v
+ # Accept org-native stamps: strip a leading [ or < (inactive/active
+ # timestamp bracket) and take the leading ISO date, so [2026-07-09 Thu]
+ # and 2026-07-09 both normalize to 2026-07-09.
+ sub(/^[[<]/, "", v)
+ if (match(v, /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]/)) {
+ lr = substr(v, RSTART, RLENGTH); have_lr = 1
+ } else {
+ # Present but unparseable — a data error, not "never reviewed". Warn
+ # loudly (file:line:value) instead of silently folding it into the
+ # stale count, where a re-review in the same bad format would never
+ # drop the number and nothing would explain why.
+ printf "task-review-staleness: %s:%d: unparseable :LAST_REVIEWED: %s (expected YYYY-MM-DD or [YYYY-MM-DD Day])\n", fname, NR, raw > "/dev/stderr"
+ lr = "INVALID"; have_lr = 1
+ }
next
}
END { flush() }
@@ -98,9 +117,16 @@ while IFS=$'\t' read -r hline value heading; do
continue
fi
- # Unparseable date → treat as NIL (stale).
+ # Malformed stamp: extract_tasks already warned. A data error is not
+ # "never reviewed", so it stays out of the stale count.
+ if [ "$value" = "INVALID" ]; then
+ continue
+ fi
+
+ # A shape-valid but impossible date (e.g. 2026-13-45) slips past the awk
+ # regex; warn and skip rather than silently counting it.
if ! rev_epoch=$(date -d "$value" +%s 2>/dev/null); then
- count=$((count + 1))
+ echo "task-review-staleness: $todo_file: unparseable :LAST_REVIEWED: $value" >&2
continue
fi
diff --git a/claude-templates/.ai/scripts/tests/task-review-staleness.bats b/claude-templates/.ai/scripts/tests/task-review-staleness.bats
index 488b023..79aad79 100644
--- a/claude-templates/.ai/scripts/tests/task-review-staleness.bats
+++ b/claude-templates/.ai/scripts/tests/task-review-staleness.bats
@@ -49,6 +49,16 @@ task_unreviewed() {
printf '** %s [#%s] %s\nBody.\n\n' "$keyword" "$prio" "$title" >> "$TODO"
}
+# Emit a qualifying task whose LAST_REVIEWED is an org-native inactive
+# timestamp — [YYYY-MM-DD Day] — matching the CREATED:/CLOSED: cookies that
+# sit in the same drawer. The date is derived from an ISO date via `date`.
+task_reviewed_org() {
+ local keyword="$1" prio="$2" title="$3" isodate="$4"
+ local org="[$(date -d "$isodate" '+%F %a')]"
+ printf '** %s [#%s] %s\n:PROPERTIES:\n:LAST_REVIEWED: %s\n:END:\nBody.\n\n' \
+ "$keyword" "$prio" "$title" "$org" >> "$TODO"
+}
+
# ---- Normal cases ----------------------------------------------------
@test "staleness: empty file reports zero" {
@@ -85,6 +95,20 @@ task_unreviewed() {
[ "$output" = "2" ]
}
+@test "staleness: org-native bracketed LAST_REVIEWED parses — recent is fresh" {
+ task_reviewed_org TODO A "Reviewed five days ago, org stamp" "$D5"
+ run bash "$SCRIPT" "$TODO" 30
+ [ "$status" -eq 0 ]
+ [ "$output" = "0" ]
+}
+
+@test "staleness: org-native bracketed LAST_REVIEWED parses — old is stale" {
+ task_reviewed_org TODO A "Reviewed forty days ago, org stamp" "$D40"
+ run bash "$SCRIPT" "$TODO" 30
+ [ "$status" -eq 0 ]
+ [ "$output" = "1" ]
+}
+
# ---- Boundary cases --------------------------------------------------
@test "staleness: age exactly equal to threshold is fresh" {
@@ -136,9 +160,23 @@ task_unreviewed() {
[ "$output" = "0" ]
}
-@test "staleness: malformed LAST_REVIEWED is treated as stale" {
+@test "staleness: malformed LAST_REVIEWED warns to stderr and is not counted" {
task_reviewed TODO A "Bad date" "not-a-date"
- run bash "$SCRIPT" "$TODO" 30
+ # stdout carries only the count — the malformed stamp is not folded in.
+ run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null"
+ [ "$status" -eq 0 ]
+ [ "$output" = "0" ]
+ # stderr carries the loud warning naming the offending value.
+ run bash -c "bash '$SCRIPT' '$TODO' 30 2>&1 1>/dev/null"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"not-a-date"* ]]
+ [[ "$output" == *"LAST_REVIEWED"* ]]
+}
+
+@test "staleness: malformed stamp is excluded while real stale tasks still count" {
+ task_reviewed TODO A "Real stale" "$D40"
+ task_reviewed TODO B "Broken stamp" "garbage"
+ run bash -c "bash '$SCRIPT' '$TODO' 30 2>/dev/null"
[ "$status" -eq 0 ]
[ "$output" = "1" ]
}
@@ -161,6 +199,17 @@ task_unreviewed() {
[[ "${lines[2]}" == *"Reviewed recently"* ]]
}
+@test "staleness --list: org-native bracketed stamp sorts by its real date" {
+ task_reviewed TODO A "Bare recent" "$D5"
+ task_reviewed_org TODO B "Org-stamped old" "$D40"
+ run bash "$SCRIPT" --list "$TODO" 10
+ [ "$status" -eq 0 ]
+ # The org-bracketed old stamp must sort ahead of the bare recent one —
+ # proof it parsed to a real date rather than falling to 0000-00-00.
+ [[ "${lines[0]}" == *"Org-stamped old"* ]]
+ [[ "${lines[1]}" == *"Bare recent"* ]]
+}
+
@test "staleness --list: takes only the requested count" {
task_unreviewed TODO A "First"
task_reviewed TODO B "Second" "$D40"