diff options
Diffstat (limited to '.ai/scripts/task-review-staleness.sh')
| -rwxr-xr-x | .ai/scripts/task-review-staleness.sh | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/.ai/scripts/task-review-staleness.sh b/.ai/scripts/task-review-staleness.sh new file mode 100755 index 0000000..b52cd3d --- /dev/null +++ b/.ai/scripts/task-review-staleness.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# task-review-staleness.sh — count top-level todo.org tasks whose review +# has gone stale. +# +# Usage: task-review-staleness.sh <todo-file> <threshold-days> +# +# Prints a single integer to stdout: the number of qualifying tasks that +# are stale. Shared by the wrap-up health check (threshold 30) and the +# startup reminder (threshold 7) so both count the same way. +# +# A qualifying task is a depth-2 (**) heading carrying a TODO/DOING/VERIFY +# keyword and an [#A]/[#B]/[#C] priority cookie. DONE/CANCELLED tasks, +# deeper headings, and cookie-less headings are not review units. +# +# A qualifying task is stale when its :LAST_REVIEWED: property is missing +# or unparseable (NIL sorts oldest), or when its age strictly exceeds the +# threshold (age > N days; age == N is still fresh). + +set -euo pipefail + +die() { echo "task-review-staleness: $*" >&2; exit 2; } + +[ "$#" -eq 2 ] || die "usage: $(basename "$0") <todo-file> <threshold-days>" + +todo_file="$1" +threshold="$2" + +[ -f "$todo_file" ] || die "no such file: $todo_file" +[[ "$threshold" =~ ^[0-9]+$ ]] || die "threshold must be a non-negative integer: $threshold" + +# Emit one line per qualifying top-level task: its LAST_REVIEWED value, or +# "NONE" when the task has no such property. Body prose is ignored; only +# the property drawer between a qualifying heading and the next heading is +# scanned. +extract_review_dates() { + awk ' + function flush() { + if (in_task) print (have_lr ? lr : "NONE") + } + /^\*+ / { + flush() + have_lr = 0; lr = "" + in_task = ($0 ~ /^\*\* (TODO|DOING|VERIFY) \[#[ABC]\]/) + next + } + in_task && /^[ \t]*:LAST_REVIEWED:[ \t]*/ { + line = $0 + sub(/^[ \t]*:LAST_REVIEWED:[ \t]*/, "", line) + sub(/[ \t]*$/, "", line) + lr = line; have_lr = 1 + next + } + END { flush() } + ' "$todo_file" +} + +# Normalize "today" to local midnight so a task reviewed exactly N days +# ago measures as N, not N-and-a-fraction. LAST_REVIEWED dates parse to +# midnight already, so both ends of the diff sit on day boundaries. +today_epoch=$(date -d "$(date +%F)" +%s) +count=0 + +while IFS= read -r value; do + if [ "$value" = "NONE" ]; then + count=$((count + 1)) + continue + fi + + # Unparseable date → treat as NIL (stale). + if ! rev_epoch=$(date -d "$value" +%s 2>/dev/null); then + count=$((count + 1)) + continue + fi + + # Round to nearest day so a DST hour can't shift a boundary task. + age_days=$(( (today_epoch - rev_epoch + 43200) / 86400 )) + if [ "$age_days" -gt "$threshold" ]; then + count=$((count + 1)) + fi +done < <(extract_review_dates) + +echo "$count" |
