Brydon DeWitt 6b07e4ccb2 feat: add shared agent infrastructure (.agents/)
- AGENTS.md: design principles, enforcement hierarchy, deferred loading
- agents/: brainstorm, build, orchestrator, research (auto-discovered by MCP server)
- skills/: research methodology (auto-discovered by MCP server)
- hooks/: pre-tool-use, post-tool-use (BFF block removed), session-start,
  stop, pre-compact, user-prompt-submit
- frameworks/: opencode/plugin.ts (resolves hooks via import.meta.url — works
  as project-local or global plugin), github/hooks.json
- mcp/index.ts: auto-discovers agents/*.md and skills/*.md from frontmatter
  (replaces hand-maintained registry); server renamed all-agents
- docs/: agent-infrastructure.md (generalized), research docs (7 files),
  ai_architectures.md, llama-server-cuda-wsl2.md
- install.sh: idempotent setup — Copilot global hooks, OpenCode global plugin +
  AGENTS.md + MCP entry, VS Code global MCP config
2026-05-22 13:13:43 -04:00

177 lines
8.1 KiB
Bash
Executable File

#!/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 <<EOF
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"decision": "block",
"reason": ${msg}
}
}
EOF
exit 0
fi
# ── Lessons learned prompt (non-blocking) ────────────────────────────────────
# Check whether all questions in the user's last prompt were answered
unanswered_reminder=""
LAST_PROMPT_FILE="/tmp/.last-user-prompt.txt"
if [[ -f "$LAST_PROMPT_FILE" ]]; then
last_prompt=$(cat "$LAST_PROMPT_FILE" 2>/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 <<EOF
{
"hookSpecificOutput": {
"hookEventName": "Stop",
"additionalContext": ${json_prompt}
}
}
EOF