diff --git a/hooks/bestwork-requirement-check.sh b/hooks/bestwork-requirement-check.sh new file mode 100755 index 0000000..e6a6be0 --- /dev/null +++ b/hooks/bestwork-requirement-check.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# bestwork requirement check — PostToolUse on Write|Edit +# Checks clarify/validate state files for unmet requirements +# Outputs warning if requirements are not yet covered + +INPUT=$(cat) +TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""') + +# Defensive: hooks.json matcher already filters, but guard here too +case "$TOOL" in + Edit|Write) ;; + *) echo '{}'; exit 0 ;; +esac + +PROJECT_ROOT=$(pwd) +BESTWORK_STATE="${PROJECT_ROOT}/.bestwork/state" +STATS_FILE="${BESTWORK_STATE}/harness-stats.json" + +# Initialize stats file if missing or malformed +if [ ! -f "$STATS_FILE" ] || ! jq empty "$STATS_FILE" 2>/dev/null; then + mkdir -p "$BESTWORK_STATE" + echo '{"checksRun":0,"warningsIssued":0,"requirementsMet":0,"requirementsMissed":0,"lastUpdated":""}' > "$STATS_FILE" +fi + +# NOTE: increment_stat uses read-modify-write without locking. +# Stats are informational (not gating), so occasional lost increments +# under concurrent Write/Edit are accepted. flock is not portable to macOS. +increment_stat() { + local key="$1" + local val + val=$(jq -r ".$key" "$STATS_FILE" 2>/dev/null) + [ -z "$val" ] || [ "$val" = "null" ] && val=0 + val=$((val + 1)) + local tmp + tmp=$(mktemp) + jq --arg k "$key" --argjson v "$val" --arg ts "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + '.[$k] = $v | .lastUpdated = $ts' "$STATS_FILE" > "$tmp" && mv "$tmp" "$STATS_FILE" +} + +# Track that we ran a check +increment_stat "checksRun" + +WARNINGS="" + +# === Check clarify state === +CLARIFY_FILE="${BESTWORK_STATE}/clarify.json" +if [ -f "$CLARIFY_FILE" ]; then + STATUS=$(jq -r '.status // ""' "$CLARIFY_FILE" 2>/dev/null) + if [ "$STATUS" = "complete" ]; then + OPEN_GAPS=$(jq -r '(.openGaps // []) | length' "$CLARIFY_FILE" 2>/dev/null) + if [ "$OPEN_GAPS" -gt 0 ] 2>/dev/null; then + GAP_LIST=$(jq -r '(.openGaps // []) | join(", ")' "$CLARIFY_FILE" 2>/dev/null) + WARNINGS="${WARNINGS}[BW gate] clarify: ${OPEN_GAPS} open gap(s) remaining — ${GAP_LIST}\n" + increment_stat "requirementsMissed" + else + increment_stat "requirementsMet" + fi + fi +fi + +# === Check validate state === +VALIDATE_FILE="${BESTWORK_STATE}/validate.json" +if [ -f "$VALIDATE_FILE" ]; then + STATUS=$(jq -r '.status // ""' "$VALIDATE_FILE" 2>/dev/null) + if [ "$STATUS" = "complete" ]; then + VERDICT=$(jq -r '.verdict // ""' "$VALIDATE_FILE" 2>/dev/null) + OVERALL=$(jq -r '(.scores.overall // 0)' "$VALIDATE_FILE" 2>/dev/null) + case "$VERDICT" in + REJECTED) + WARNINGS="${WARNINGS}[BW gate] validate: REJECTED (${OVERALL}%) — building a feature that failed validation\n" + increment_stat "requirementsMissed" + ;; + WEAK) + WARNINGS="${WARNINGS}[BW gate] validate: WEAK (${OVERALL}%) — thin evidence for this feature\n" + increment_stat "requirementsMissed" + ;; + CONDITIONAL) + # Info only, not a hard warning + increment_stat "requirementsMet" + ;; + VALIDATED) + increment_stat "requirementsMet" + ;; + esac + fi +fi + +# === Output === +if [ -n "$WARNINGS" ]; then + increment_stat "warningsIssued" + CONTEXT=$(printf '%s' "$WARNINGS" | sed '/^$/d') + jq -n --arg ctx "$CONTEXT" \ + '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}' +else + echo '{}' +fi diff --git a/hooks/hooks.json b/hooks/hooks.json index 507828f..a98a12b 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -17,6 +17,11 @@ "type": "agent", "prompt": "You are bestwork's validation agent. A file was just modified via $ARGUMENTS.\nCheck for:\n1. TypeScript errors: run `npx tsc --noEmit` and report any errors in the changed file\n2. If the file imports modules, verify the imports actually exist (grep for them)\nDo NOT fix anything. Only report issues concisely (under 3 lines). If no issues, say nothing.", "timeout": 30 + }, + { + "type": "command", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/bestwork-requirement-check.sh\"", + "timeout": 5 } ] }