#!/usr/bin/env bash # todo-audit.sh — TODO file & phased-readiness probe for the TODO Architect. # # Locates a TODO.md / TODO.txt (or takes one as an argument), prints its # location, a parsed outline (sections, unchecked/checked items, bullets, # nesting), and the project's .agents/ state, next free phase number # (counting .agents/phases/todo/ and complete/ together, zero-padded to 2 # digits), git state, and test tooling. # # Usage: # bash todo-audit.sh [path/to/TODO.(md|txt)] # bash todo-audit.sh # auto-detect in cwd and git root set -uo pipefail # --- locate the TODO file ------------------------------------------------------ todo="" if [[ $# -ge 1 ]]; then [[ -f "$1" ]] || { echo "✗ ERROR: not a file: $1" >&2; exit 1; } todo="$1" else # search the git root first, then cwd and its ancestors (up to the git # root, or 3 levels when not in a repo) dirs=() gr="$(git rev-parse --show-toplevel 2>/dev/null || true)" if [[ -n "$gr" ]]; then dirs+=("$gr") d="$(pwd)" while [[ "$d" != "$gr" && "$d" != "/" ]]; do dirs+=("$d") d="$(dirname "$d")" done else d="$(pwd)" for _ in 0 1 2 3; do [[ "$d" == "/" ]] && break dirs+=("$d") d="$(dirname "$d")" done fi for r in "${dirs[@]}"; do [[ -d "$r" ]] || continue for f in "$r"/*; do [[ -f "$f" ]] || continue b="$(basename "$f" | tr '[:upper:]' '[:lower:]')" case "$b" in todo.md|todo.txt) todo="$f"; break 2 ;; esac done done fi if [[ -z "$todo" ]]; then echo "no TODO.md / TODO.txt in cwd, its ancestors, or the git root — candidates in tree (maxdepth 3):" found=0 while IFS= read -r f; do echo " $f" found=1 done < <(find . -maxdepth 3 \( -name .git -o -name node_modules -o -name .venv \) -prune -o -type f -iname 'todo.*' -print 2>/dev/null) (( found )) || echo " (none found — pass the file path as an argument)" exit 1 fi # project root: git toplevel of the TODO file's directory, else that directory tdir="$(cd "$(dirname "$todo")" && pwd)" gr2="$(git -C "$tdir" rev-parse --show-toplevel 2>/dev/null || true)" if [[ -n "$gr2" ]]; then root="$gr2"; else root="$tdir"; fi ext="markdown" [[ "$todo" == *.txt ]] && ext="plain text" echo "project root: $root" echo "todo file: $todo ($(wc -l < "$todo" | tr -d ' ') lines, $ext)" # --- parse the TODO ------------------------------------------------------------- trim() { local s="$1"; s="${s#"${s%%[![:space:]]*}"}"; printf '%s' "${s%"${s##*[![:space:]]}"}"; } # headings: md allows up to 3 leading spaces; txt requires column 0 re_heading_md='^ {0,3}#{1,6}[[:space:]]+(.*)$' re_heading_txt='^#{1,6}[[:space:]]+(.*)$' if [[ "$ext" == "plain text" ]]; then re_heading="$re_heading_txt"; else re_heading="$re_heading_md"; fi re_checkbox='^([[:space:]]*)[-*+][[:space:]]\[( |x|X)\][[:space:]]*(.*)$' re_bullet='^([[:space:]]*)[-*+][[:space:]]+(.*)$' re_numbered='^([[:space:]]*)[0-9]+[.)][[:space:]]+(.*)$' ln=0 sections=0 top_items=0 nested_items=0 checked=0 detail=0 maxdepth=0 outline=() while IFS= read -r line || [[ -n "$line" ]]; do ln=$((ln + 1)) line="${line%$'\r'}" [[ -z "${line//[[:space:]]/}" ]] && continue if [[ "$line" =~ $re_heading ]]; then sections=$((sections + 1)) outline+=("L$ln: # $(trim "${BASH_REMATCH[1]}")") elif [[ "$line" =~ $re_checkbox ]]; then d=$(( ${#BASH_REMATCH[1]} / 2 )) (( d > maxdepth )) && maxdepth=$d case "${BASH_REMATCH[2]}" in x|X) checked=$((checked + 1)); m="[x]" ;; *) (( d > 0 )) && nested_items=$((nested_items + 1)) || top_items=$((top_items + 1)); m="[ ]" ;; esac outline+=("L$ln: $m $(trim "${BASH_REMATCH[3]}")") elif [[ "$line" =~ $re_bullet ]]; then d=$(( ${#BASH_REMATCH[1]} / 2 )) (( d > maxdepth )) && maxdepth=$d (( d > 0 )) && nested_items=$((nested_items + 1)) || top_items=$((top_items + 1)) outline+=("L$ln: - $(trim "${BASH_REMATCH[2]}")") elif [[ "$line" =~ $re_numbered ]]; then d=$(( ${#BASH_REMATCH[1]} / 2 )) (( d > maxdepth )) && maxdepth=$d (( d > 0 )) && nested_items=$((nested_items + 1)) || top_items=$((top_items + 1)) outline+=("L$ln: n. $(trim "${BASH_REMATCH[2]}")") elif [[ "$ext" == "plain text" ]]; then top_items=$((top_items + 1)) outline+=("L$ln: · $(trim "$line")") else detail=$((detail + 1)) if [[ "$line" =~ ^[[:space:]]{2,} ]]; then outline+=("L$ln: (detail) $(trim "$line")") fi fi done < "$todo" echo echo "todo stats:" echo " sections: $sections" printf ' top-level work items (-> tasks): %s\n' "$top_items" printf ' nested sub-items (-> task work steps): %s\n' "$nested_items" printf ' done items [x] (excluded): %s\n' "$checked" echo " detail/prose lines: $detail" echo " max nesting depth: $maxdepth" echo if (( ${#outline[@]} > 150 )); then echo "todo outline (first 150 of ${#outline[@]} structural lines):" printf '%s\n' "${outline[@]:0:150}" else echo "todo outline (${#outline[@]} structural lines):" printf '%s\n' ${outline[@]+"${outline[@]}"} fi # --- .agents state ----------------------------------------------------------------- echo echo ".agents state:" a=0 if [[ -f "$root/.agents/PLAN.md" ]]; then echo " .agents/PLAN.md: present" a=1 fi if [[ -f "$root/AGENTS.md" ]]; then echo " AGENTS.md: present" a=1 fi if [[ -f "$root/.agents/validate.sh" ]]; then echo " .agents/validate.sh: present" a=1 fi todo_dir="$root/.agents/phases/todo" complete_dir="$root/.agents/phases/complete" list_phases() { local dir="$1" entry n t any=0 for entry in "$dir"/*; do if [[ ! -e "$entry" ]]; then continue; fi n="$(basename "$entry")" if [[ "$n" =~ ^[0-9] ]]; then if [[ -d "$entry" ]]; then t="$(ls -1 "$entry" 2>/dev/null | grep -cE '\.md$' || true)" echo " $n/ ($t file(s): 00_phase.md + tasks)" else echo " $n (legacy single-file phase)" fi any=1 fi done if (( ! any )); then echo " (empty)"; fi } if [[ -d "$todo_dir" ]]; then echo " .agents/phases/todo:" list_phases "$todo_dir" a=1 fi if [[ -d "$complete_dir" ]]; then echo " .agents/phases/complete:" list_phases "$complete_dir" a=1 fi if (( ! a )); then echo " (no .agents/ artifacts — Protocol A full conversion needed)"; fi max=0 for d in "$todo_dir" "$complete_dir"; do [[ -d "$d" ]] || continue for entry in "$d"/*; do if [[ ! -e "$entry" ]]; then continue; fi n="$(basename "$entry" .md)" if [[ "$n" =~ ^([0-9]+) ]]; then n=$((10#${BASH_REMATCH[1]})) if (( n > max )); then max=$n; fi fi done done echo " next phase number: $(printf '%02d' $((max + 1)))" # --- version control ---------------------------------------------------------------- echo echo "version control:" if [[ -d "$root/.git" ]]; then branch="$(git -C "$root" branch --show-current 2>/dev/null || echo unknown)" dirty="$(git -C "$root" status --porcelain 2>/dev/null | wc -l | tr -d ' ')" echo " git repository (branch: ${branch:-detached}, uncommitted changes: ${dirty})" else echo " (not a git repository — the conversion must git init)" fi # --- test tooling -------------------------------------------------------------------- echo echo "test tooling:" t=0 if [[ -f "$root/pytest.ini" ]] || { [[ -f "$root/pyproject.toml" ]] && grep -q '\[tool\.pytest' "$root/pyproject.toml"; }; then echo " pytest configured" t=1 fi if [[ -f "$root/conftest.py" ]] || [[ -d "$root/tests" ]]; then echo " tests/ or conftest.py present" t=1 fi n_py="$(find "$root" \( -name 'test_*.py' -o -name '*_test.py' \) -not -path '*/.git/*' -not -path '*/node_modules/*' -not -path '*/.venv/*' -not -path '*/venv/*' 2>/dev/null | wc -l | tr -d ' ')" if (( n_py > 0 )); then echo " $n_py python test file(s)"; t=1; fi if [[ -f "$root/package.json" ]] && grep -q '"test"' "$root/package.json"; then echo " npm test script" t=1 fi if [[ -f "$root/ruff.toml" ]] || { [[ -f "$root/pyproject.toml" ]] && grep -q '\[tool\.ruff\]' "$root/pyproject.toml"; }; then echo " ruff configured" t=1 fi if [[ -f "$root/.coveragerc" ]] || { [[ -f "$root/pyproject.toml" ]] && grep -qE '\[tool\.(coverage|pytest-cov)\]' "$root/pyproject.toml"; }; then echo " coverage configured" t=1 fi if (( ! t )); then echo " (none detected — a 01_foundation phase must establish a test baseline)"; fi