#!/usr/bin/env bash # Stop hook: # 1. Validates TODO.md and COMPLETED.md — blocking if violations found. # 2. Prompts agent to record lessons learned before session ends (non-blocking). set -euo pipefail REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")" SESSION_DIR="$REPO_ROOT/.session" TODO_FILE="$REPO_ROOT/docs/TODO.md" COMPLETED_FILE="$REPO_ROOT/docs/projects/COMPLETED.md" # ── Validation (blocking) ──────────────────────────────────────────────────── problems="" # Check TODO.md for completed [x] or ✅ numbered items — they should be moved to COMPLETED.md if [[ -f "$TODO_FILE" ]]; then todo_completed=$(grep -n '^\s*[0-9]\+\. \(\[x\]\|✅\)' "$TODO_FILE" 2>/dev/null || true) if [[ -n "$todo_completed" ]]; then count=$(echo "$todo_completed" | wc -l | tr -d ' ') problems="docs/TODO.md contains ${count} completed [x]/✅ task(s). Move them to docs/projects/COMPLETED.md and remove from TODO.md.\nLines:\n${todo_completed}" fi fi # Check COMPLETED.md for inline [x] items — it should contain stubs/summaries only if [[ -f "$COMPLETED_FILE" ]]; then completed_inline=$(grep -n '^\s*\(-\|[0-9]\+\.\) \[x\]' "$COMPLETED_FILE" 2>/dev/null || true) if [[ -n "$completed_inline" ]]; then count=$(echo "$completed_inline" | wc -l | tr -d ' ') if [[ -n "$problems" ]]; then problems="${problems}\n\n" fi problems="${problems}docs/projects/COMPLETED.md contains ${count} inline [x] item(s). Archive to a completed/*.md file and replace with a stub entry." fi fi if [[ -n "$problems" ]]; then msg=$(printf '%b' "$problems" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))') cat </dev/null || true) if [[ -n "$last_prompt" ]]; then unanswered_reminder="$last_prompt" fi fi # Check whether the dev server is running (ports 3000/3001) dev_server_running="" if ss -tlnp 2>/dev/null | grep -qE ':300[01]\s'; then dev_server_running="yes" fi dead_ends_active="" DEAD_ENDS_FILE="$SESSION_DIR/dead-ends.md" if [[ -f "$DEAD_ENDS_FILE" ]]; then entry_count=$(grep -c '^\s*- \*\*' "$DEAD_ENDS_FILE" 2>/dev/null || echo "0") if [[ "$entry_count" -gt 0 ]]; then dead_ends_active="yes" fi fi # Check for investigation files that may need status updates open_investigations="" EXPLORATIONS_DIR="$REPO_ROOT/docs/explorations" if [[ -d "$EXPLORATIONS_DIR" ]]; then inv_files=$(find "$EXPLORATIONS_DIR" -name "*.md" -not -empty 2>/dev/null || true) if [[ -n "$inv_files" ]]; then # Check for explorations NOT yet marked complete (status line doesn't contain 'complete') active=$(echo "$inv_files" | while read -r f; do if ! grep -qi '^\*\*Status\*\*.*complete\|^Status:.*complete' "$f" 2>/dev/null; then echo "$f" fi done || true) if [[ -n "$active" ]]; then active_names=$(echo "$active" | xargs -I{} basename {} .md | sed 's/^/ - /' || true) open_investigations="yes" fi fi fi # Check if pre-compact-state.md still lists active investigations stale_compact="" COMPACT_FILE="$SESSION_DIR/pre-compact-state.md" if [[ -f "$COMPACT_FILE" ]]; then active_in_compact=$(grep -A20 '## Active Investigations' "$COMPACT_FILE" 2>/dev/null | grep -v '##' | grep -v '^\s*$' | head -5 || true) if [[ -n "$active_in_compact" ]]; then stale_compact="yes" fi fi # Build the lessons-learned prompt prompt="SESSION END — QUALITY GATE" # Remind agent to verify it answered every question in the user's last message if [[ -n "$unanswered_reminder" ]]; then prompt="${prompt}\n\nANSWER CHECK: Before finishing, re-read the user's last message below and confirm" prompt="${prompt} you addressed EVERY question and request in it — not just the primary task:" prompt="${prompt}\n---" prompt="${prompt}\n${unanswered_reminder}" prompt="${prompt}\n---" fi if [[ -n "$dev_server_running" ]]; then prompt="${prompt}\nThe dev server IS running (port 3000/3001 detected). Run: npm test && npm run lint" prompt="${prompt}\nThen ask the user to confirm the build is clean in their terminal." else prompt="${prompt}\nThe dev server is NOT running. Run: npm run build:strict" prompt="${prompt}\nThis runs build + lint + format:check + tests. Do NOT skip this if you changed any source files." fi prompt="${prompt}\n" prompt="${prompt}\n---" prompt="${prompt}\nLESSONS LEARNED CAPTURE — Before finishing, consider recording reusable insights:" prompt="${prompt}\n" prompt="${prompt}\n1. **Process insights** (what worked, what didn't, workflow improvements) → write to /memories/repo/ or /memories/" if [[ -n "$dead_ends_active" ]]; then prompt="${prompt}\n2. **Dead-ends file** has entries — verify all have results recorded (ELIMINATED/CONFIRMED with reasons)" fi if [[ -n "$open_investigations" ]]; then prompt="${prompt}\n3. **Open explorations** not yet marked complete — update their status or mark complete:" prompt="${prompt}\n${active_names}" fi if [[ -n "$stale_compact" ]]; then prompt="${prompt}\n3. **pre-compact-state.md** still lists active investigations — clear or update it now that the session is ending" fi prompt="${prompt}\n4. **TODO.md review** — Check docs/TODO.md: did any work this session complete or partially complete an item?" prompt="${prompt}\n - If an item is done: mark it [x], then move the whole entry to docs/projects/COMPLETED.md and remove it from TODO.md" prompt="${prompt}\n - If partially done: update the description to reflect current state" prompt="${prompt}\n - Don't wait for the blocking check — proactively keep TODO.md accurate" prompt="${prompt}\n5. **New tasks discovered** → note them for the user or add to docs/TODO.md" prompt="${prompt}\n" prompt="${prompt}\n---" prompt="${prompt}\nEFFORT REFLECTION: If this session required significant effort (many tool calls, multiple dead-ends, complex investigation):" prompt="${prompt}\n Ask yourself: What information, if it had existed at the start, would have prevented most of that work?" prompt="${prompt}\n First, determine scope — is this globally applicable, or specific to certain files/patterns?" prompt="${prompt}\n Then lean toward hooks as the solution:" prompt="${prompt}\n • Hard stops via PreToolUse blocks (best when the bad action is a terminal command)" prompt="${prompt}\n • PostToolUse reminders (fire right after editing a relevant file — effective because they appear mid-task)" prompt="${prompt}\n • applyTo: instructions files scoped to a file glob (fire when the agent opens matching files)" prompt="${prompt}\n • PreCompact saves investigation state before context compression (PostCompact does not exist)" prompt="${prompt}\n • Stop / SessionStart: on-demand summary injection — less precise than PostToolUse but good for broad reminders" prompt="${prompt}\n These are all more reliable than AGENTS.md sections (lost-in-the-middle problem)." prompt="${prompt}\n Record the insight in the right hook/instructions file, NOT just in AGENTS.md." prompt="${prompt}\n" prompt="${prompt}\nSkip categories that don't apply. Only record genuinely new insights." # Prefix with a self-identifying marker so the model cannot confuse the # injection with the user's own message or prior tool output. prompt="[HOOK INJECTION: stop] System reminder — injected at session end, not part of any user message or tool output:\n\n${prompt}" json_prompt=$(printf '%b' "$prompt" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))') cat <