aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/task-review-staleness.sh
blob: ed43712e5d4786c7ed9c2f9db34df41898ca9a65 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env bash
#
# task-review-staleness.sh — surface top-level todo.org tasks by review age.
#
# Usage:
#   task-review-staleness.sh <todo-file> <threshold-days>   # count mode (default)
#   task-review-staleness.sh --list <todo-file> <count>     # list mode
#
# Count mode prints a single integer: 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.
#
# List mode prints up to <count> qualifying tasks, oldest-reviewed first,
# one per line, tab-separated as: <line>\t<LAST_REVIEWED-or-NONE>\t<heading>.
# The task-review workflow uses it to pick the batch to walk each day.
#
# 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 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).

set -euo pipefail

die() { echo "task-review-staleness: $*" >&2; exit 2; }

mode=count
if [ "${1:-}" = "--list" ]; then
  mode=list
  shift
fi

[ "$#" -eq 2 ] || die "usage: $(basename "$0") [--list] <todo-file> <number>"

todo_file="$1"
num="$2"

[ -f "$todo_file" ] || die "no such file: $todo_file"
[[ "$num" =~ ^[0-9]+$ ]] || die "expected a non-negative integer, got: $num"

# Emit one line per qualifying top-level task: <line>\t<value>\t<heading>,
# where <value> is its LAST_REVIEWED date or "NONE". Body prose is ignored;
# only the property drawer between a qualifying heading and the next heading
# is scanned.
extract_tasks() {
  awk '
    function flush() {
      if (in_task) printf "%s\t%s\t%s\n", hline, (have_lr ? lr : "NONE"), heading
    }
    /^\*+ / {
      flush()
      have_lr = 0; lr = ""
      if ($0 ~ /^\*\* (TODO|DOING|VERIFY) \[#[ABC]\]/) {
        in_task = 1; hline = NR; heading = $0
        sub(/[ \t]*$/, "", heading)
      } else {
        in_task = 0
      }
      next
    }
    in_task && /^[ \t]*:LAST_REVIEWED:[ \t]*/ {
      v = $0
      sub(/^[ \t]*:LAST_REVIEWED:[ \t]*/, "", v)
      sub(/[ \t]*$/, "", v)
      lr = v; have_lr = 1
      next
    }
    END { flush() }
  ' "$todo_file"
}

if [ "$mode" = "list" ]; then
  # Prefix each task with a sort key (the review date, or 0000-00-00 when
  # missing or unparseable so it sorts oldest), order by key then file line,
  # take the requested count, and drop the key column.
  extract_tasks | awk -F'\t' -v OFS='\t' '
    {
      key = ($2 ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/) ? $2 : "0000-00-00"
      print key, $1, $2, $3
    }
  ' | sort -t"$(printf '\t')" -k1,1 -k2,2n | awk -v n="$num" 'NR <= n' | cut -f2-
  exit 0
fi

# Count mode.
# 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=$'\t' read -r hline value heading; 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 "$num" ]; then
    count=$((count + 1))
  fi
done < <(extract_tasks)

echo "$count"