From faebc007b69cbbf10073163dcd0f6c3ef3a826c6 Mon Sep 17 00:00:00 2001 From: rlaope Date: Thu, 9 Apr 2026 11:07:29 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20harness=20gate=20=E2=80=94=20clarif?= =?UTF-8?q?y/validate=20PostToolUse=20requirement=20check=20(#50)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostToolUse hook that checks .bestwork/state/clarify.json and validate.json after every Write|Edit. Warns when open requirements gaps exist or when building a feature that failed validation. Accumulates gate statistics to harness-stats.json. - hooks/bestwork-requirement-check.sh with clarify + validate checks - hooks.json updated with new PostToolUse command hook - .bestwork/state/harness-stats.json tracks checksRun, warningsIssued, requirementsMet/Missed Signed-off-by: rlaope --- hooks/bestwork-requirement-check.sh | 94 +++++++++++++++++++++++++++++ hooks/hooks.json | 5 ++ 2 files changed, 99 insertions(+) create mode 100755 hooks/bestwork-requirement-check.sh diff --git a/hooks/bestwork-requirement-check.sh b/hooks/bestwork-requirement-check.sh new file mode 100755 index 0000000..ed17dfe --- /dev/null +++ b/hooks/bestwork-requirement-check.sh @@ -0,0 +1,94 @@ +#!/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 // ""') + +# Only check Write and Edit +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 +if [ ! -f "$STATS_FILE" ]; then + mkdir -p "$BESTWORK_STATE" + echo '{"checksRun":0,"warningsIssued":0,"requirementsMet":0,"requirementsMissed":0,"lastUpdated":""}' > "$STATS_FILE" +fi + +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 + OVERALL=$(jq -r '.overallScore // 0' "$CLARIFY_FILE" 2>/dev/null) + OPEN_GAPS=$(jq -r '.openGaps | length // 0' "$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=$(echo -e "$WARNINGS" | sed 's/\\n$//') + 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 } ] } From 3a843cbdbb21235a1c8175d4e01f9f8dc9413525 Mon Sep 17 00:00:00 2001 From: rlaope Date: Thu, 9 Apr 2026 11:15:25 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20address=20PR=20#53=20review=20?= =?UTF-8?q?=E2=80=94=20jq=20precedence,=20stats=20integrity,=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix CRITICAL jq operator precedence: (.openGaps // []) | length - Add JSON integrity check before jq operations on harness-stats.json - Document race condition as accepted limitation (stats are informational) - Remove unused OVERALL variable in clarify section - Fix printf formatting for output Signed-off-by: rlaope --- hooks/bestwork-requirement-check.sh | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/hooks/bestwork-requirement-check.sh b/hooks/bestwork-requirement-check.sh index ed17dfe..e6a6be0 100755 --- a/hooks/bestwork-requirement-check.sh +++ b/hooks/bestwork-requirement-check.sh @@ -6,7 +6,7 @@ INPUT=$(cat) TOOL=$(echo "$INPUT" | jq -r '.tool_name // ""') -# Only check Write and Edit +# Defensive: hooks.json matcher already filters, but guard here too case "$TOOL" in Edit|Write) ;; *) echo '{}'; exit 0 ;; @@ -16,12 +16,15 @@ PROJECT_ROOT=$(pwd) BESTWORK_STATE="${PROJECT_ROOT}/.bestwork/state" STATS_FILE="${BESTWORK_STATE}/harness-stats.json" -# Initialize stats file if missing -if [ ! -f "$STATS_FILE" ]; then +# 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 @@ -44,10 +47,9 @@ CLARIFY_FILE="${BESTWORK_STATE}/clarify.json" if [ -f "$CLARIFY_FILE" ]; then STATUS=$(jq -r '.status // ""' "$CLARIFY_FILE" 2>/dev/null) if [ "$STATUS" = "complete" ]; then - OVERALL=$(jq -r '.overallScore // 0' "$CLARIFY_FILE" 2>/dev/null) - OPEN_GAPS=$(jq -r '.openGaps | length // 0' "$CLARIFY_FILE" 2>/dev/null) + 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) + 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 @@ -62,7 +64,7 @@ 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) + 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" @@ -86,7 +88,7 @@ fi # === Output === if [ -n "$WARNINGS" ]; then increment_stat "warningsIssued" - CONTEXT=$(echo -e "$WARNINGS" | sed 's/\\n$//') + CONTEXT=$(printf '%s' "$WARNINGS" | sed '/^$/d') jq -n --arg ctx "$CONTEXT" \ '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":$ctx}}' else