add todo to phased
This commit is contained in:
Executable
+241
@@ -0,0 +1,241 @@
|
||||
#!/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 .agent/ state, next free phase number
|
||||
# (counting .agent/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
|
||||
|
||||
# --- .agent state -----------------------------------------------------------------
|
||||
echo
|
||||
echo ".agent state:"
|
||||
a=0
|
||||
if [[ -f "$root/.agent/PLAN.md" ]]; then
|
||||
echo " .agent/PLAN.md: present"
|
||||
a=1
|
||||
fi
|
||||
if [[ -f "$root/AGENTS.md" ]]; then
|
||||
echo " AGENTS.md: present"
|
||||
a=1
|
||||
fi
|
||||
if [[ -f "$root/.agent/validate.sh" ]]; then
|
||||
echo " .agent/validate.sh: present"
|
||||
a=1
|
||||
fi
|
||||
todo_dir="$root/.agent/phases/todo"
|
||||
complete_dir="$root/.agent/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 " .agent/phases/todo:"
|
||||
list_phases "$todo_dir"
|
||||
a=1
|
||||
fi
|
||||
if [[ -d "$complete_dir" ]]; then
|
||||
echo " .agent/phases/complete:"
|
||||
list_phases "$complete_dir"
|
||||
a=1
|
||||
fi
|
||||
if (( ! a )); then echo " (no .agent/ 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
|
||||
Reference in New Issue
Block a user