Files
ducoterra e505df62e8 refactor(skills): use .agents/ instead of .agent/ for phased execution
Standardize on the .agents/ directory across all phased-execution
skills (phase state, reports, sessions, validate.sh, PLAN.md, and
per-story/feature/workflow trees). Legacy dot-less agent/ fallbacks
in migration scripts are untouched.
2026-09-05 10:52:37 -04:00

242 lines
8.2 KiB
Bash
Executable File

#!/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