dotfiles/.agents/hooks/post-tool-use.sh
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

173 lines
9.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# PostToolUse hook: inject methodology reminders after relevant tool actions.
# - Periodic self-check (weighted every ~15 effective write-calls)
# - After test failures: remind about hypothesis-first methodology
# - After reading docs/ or .md/.txt files: lift pagination restriction reminder
# - After editing docs/ or .md/.txt files: audit file size (warn if >500 lines)
# - After editing agent config files: verify with opencode agent list
# Project-specific reminders (e.g. BFF pattern, build gates): add a sibling
# hook file in the project's .agents/hooks/ directory.
# Priority filter: emit at most 2 reminders per tool call.
# Priority order: SELF-CHECK > DEBUGGING > path-scoped > tool-specific.
set -euo pipefail
# ── Tool call counter ────────────────────────────────────────────────────────
REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || echo ".")"
REPO_ID=$(printf '%s' "$REPO_ROOT" | md5sum | cut -c1-8 2>/dev/null || echo "default")
COUNT_FILE="/tmp/.opencode-tool-count-${REPO_ID}"
COUNT=$(cat "$COUNT_FILE" 2>/dev/null || echo 0)
# Read hook input from stdin
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | grep -o '"tool_name"\s*:\s*"[^"]*"' | head -1 | sed 's/.*"\([^"]*\)"/\1/' || true)
# Weighted increment: reads +1, writes/shell +4 (equivalent to +0.25/+1 at threshold 60).
# This prevents SELF-CHECK from firing mid-investigation sweep.
case "$TOOL_NAME" in
read_file|grep_search|list_dir|file_search|semantic_search|explore_subagent)
COUNT=$((COUNT + 1))
;;
*)
COUNT=$((COUNT + 4))
;;
esac
echo "$COUNT" > "$COUNT_FILE"
# Priority-ordered reminders array: append in priority order, emit max 2.
# Priority: SELF-CHECK(1) > DEBUGGING(2) > path-scoped(3) > tool-specific(4)
reminders=()
# ── Periodic self-check (every 60 weighted units ≡ 15 effective write-calls) ─
if (( COUNT % 60 == 0 )); then
selfcheck="SELF-CHECK (${COUNT} tool calls): Step back and assess."
selfcheck="${selfcheck} (1) What is your current goal — are you still on track?"
selfcheck="${selfcheck} (2) Are you making progress or spinning on the same issue?"
selfcheck="${selfcheck} (3) If you've hit 2+ failures on the same problem, switch to @research or report to the user."
selfcheck="${selfcheck} (4) If you've been editing the same file 3+ times without a passing test, stop and rethink."
selfcheck="${selfcheck} (5) Is the chat todo list accurate? Update it if items are stale or missing."
selfcheck="${selfcheck} (6) If investigating, re-read your investigation file and dead-ends to avoid re-testing eliminated hypotheses."
reminders+=("$selfcheck")
fi
# ── After test/terminal runs that failed: remind about methodology ───────────
if [[ "$TOOL_NAME" == "run_in_terminal" || "$TOOL_NAME" == "runTests" ]]; then
TOOL_RESPONSE=$(echo "$INPUT" | grep -o '"tool_response"\s*:\s*"[^"]*"' | head -1 || true)
if echo "$TOOL_RESPONSE" | grep -qiE 'FAIL|error|panic|segfault|assertion|abort|ERR!'; then
debug_msg="DEBUGGING REMINDER: Before your next action —"
debug_msg="${debug_msg} (1) Write your hypothesis in one sentence."
debug_msg="${debug_msg} (2) Write what you'd expect if WRONG."
debug_msg="${debug_msg} (3) Check the dead-ends file (.session/dead-ends.md) if it exists."
debug_msg="${debug_msg} (4) Falsify BEFORE confirming."
debug_msg="${debug_msg} (5) If 5+ attempts without progress, STOP and report what you've learned."
reminders+=("$debug_msg")
fi
fi
# ── After editing project/agent config files: path-scoped reminders ─────────
# Project-specific path checks (e.g. build gates, BFF reminders) belong in a
# sibling project-local hook file, not here. Only general checks below.
case "$TOOL_NAME" in
replace_string_in_file|multi_replace_string_in_file|create_file)
FILE_PATH=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
const p = i.filePath || (i.replacements && i.replacements[0] && i.replacements[0].filePath) || '';
process.stdout.write(p);
" 2>/dev/null || true)
path_msg=""
# ── After editing agent config files: verify with opencode agent list ─────
if echo "$FILE_PATH" | grep -qE '\.agents/agents/|\.opencode/agents/|opencode\.json'; then
AGENT_LIST=$(cd "$REPO_ROOT" && opencode agent list 2>&1 | grep -E '^\S.*\((all|primary|subagent)\)' | sed 's/^/ /' || echo " (opencode agent list failed)")
agent_note="AGENT CONFIG VERIFICATION: You just edited an agent definition or opencode.json."
agent_note="${agent_note} Registered agents are: ${AGENT_LIST}."
agent_note="${agent_note} If your agent is missing: (1) check that .opencode/agents/<name>.md symlink resolves (cat it — should not error); (2) symlink depth must be ../../.agents/agents/<name>.md (two levels, not three); (3) check YAML frontmatter for parse errors."
agent_note="${agent_note} Deny rules only appear in \`opencode agent list\` output if the agent file loaded correctly."
if [[ -n "$path_msg" ]]; then
path_msg="${path_msg} ${agent_note}"
else
path_msg="$agent_note"
fi
fi
# ── After editing docs/ or .md/.txt files: audit file size ───────────────
if echo "$FILE_PATH" | grep -qE '(^|/)docs/|\.md$|\.txt$'; then
if [[ -f "$FILE_PATH" ]]; then
LINE_COUNT=$(wc -l < "$FILE_PATH" 2>/dev/null || echo 0)
if [[ "$LINE_COUNT" -gt 500 ]]; then
docs_audit="DOCS SIZE AUDIT: ${FILE_PATH##*/} is now ${LINE_COUNT} lines. Consider splitting this doc — files over ~500 lines require expensive pagination for local models (17+ reads for an 800-line file). Split into focused sub-docs and link them."
if [[ -n "$path_msg" ]]; then
path_msg="${path_msg} ${docs_audit}"
else
path_msg="$docs_audit"
fi
fi
fi
fi
if [[ -n "$path_msg" ]]; then
reminders+=("$path_msg")
fi
;;
esac
# ── After reading docs/ files: remind that pagination limit is lifted ─────────
if [[ "$TOOL_NAME" == "read_file" ]]; then
READ_PATH=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(i.filePath || '');
" 2>/dev/null || true)
START_LINE=$(echo "$INPUT" | node -e "
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const i = d.tool_input || {};
process.stdout.write(String(i.startLine || 1));
" 2>/dev/null || true)
if echo "$READ_PATH" | grep -qE '(^|/)docs/|\.md$|\.txt$'; then
docs_msg="DOCS READ EXEMPTION: docs/ files and all .md/.txt files are exempt from the 50-line pagination limit."
if [[ "$START_LINE" -gt 1 ]]; then
docs_msg="${docs_msg} You are currently paginating (startLine=${START_LINE}) — you may expand to up to 500 lines per call to reduce tool-call overhead."
else
docs_msg="${docs_msg} You may use ranges up to 500 lines per read_file call instead of 50."
fi
reminders+=("$docs_msg")
fi
fi
# ── After vscode_renameSymbol: remind about object property key aliases ───────
if [[ "$TOOL_NAME" == "vscode_renameSymbol" ]]; then
rename_msg="RENAME REMINDER: vscode_renameSymbol only renames variable bindings — NOT object property keys or string literals."
rename_msg="${rename_msg} After this rename, grep the file for the OLD name."
rename_msg="${rename_msg} Stale patterns to watch for: (1) aliased store keys like 'deleteX: archiveX' in the store return object — the key 'deleteX' is unchanged and so are all 'store.deleteX()' call sites;"
rename_msg="${rename_msg} (2) string literals like openDialog('delete-item') and AppDialog handle='delete-item';"
rename_msg="${rename_msg} (3) related variable names in the same file that share the same prefix (e.g. renaming deleteSuccess should also prompt renaming deleteLoading, deleteError)."
rename_msg="${rename_msg} Fix all of these with multi_replace_string_in_file after the symbol rename."
reminders+=("$rename_msg")
fi
# ── Emit at most 2 reminders ─────────────────────────────────────────────────
context=""
for (( i=0; i<${#reminders[@]} && i<2; i++ )); do
if [[ -n "$context" ]]; then
context="${context}\n${reminders[$i]}"
else
context="${reminders[$i]}"
fi
done
# Only output if we have context to inject
if [[ -n "$context" ]]; then
# Prefix with a self-identifying marker so the model cannot confuse the
# injection with preceding tool output (e.g., trailing markdown in a file).
framed="[HOOK INJECTION: post-tool-use] System reminder — NOT part of preceding tool output:\n\n${context}"
json_context=$(printf '%b' "$framed" | node -e 'process.stdout.write(JSON.stringify(require("fs").readFileSync("/dev/stdin","utf8")))')
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": ${json_context}
}
}
EOF
else
echo '{}'
fi