88# Phase 4 adds C/C++ specific clang-tidy invocation.
99#
1010# Hard constraints:
11- # - Must complete in <5 seconds (else timeout caller skips us )
11+ # - Must complete in <5 seconds (total watchdog, R1 )
1212# - Silent on missing tools (warn-only philosophy, never pollute context with infra errors)
1313# - Never emits permissionDecision/stopReason/block
1414
1515set -uo pipefail # no -e: a single check failure should not abort other checks
1616
17- # 5-second hard wall-clock cap (in case any sub-check hangs).
18- timeout_self () {
19- # If the env supports `timeout`, set a global cap via a trap.
20- # On macOS without coreutils, this is a no-op; the hook is fast anyway.
21- :
22- }
23- timeout_self
17+ # ---- Read stdin BEFORE backgrounding (bash redirects bg stdin to /dev/null) ----
18+ INPUT_FILE=$( mktemp)
19+ cat > " $INPUT_FILE " 2> /dev/null || true
2420
25- # Read stdin JSON; tolerate non-JSON / empty stdin.
26- input=" $( cat 2> /dev/null || true) "
21+ # ---- main() — all logic wrapped for R1 total watchdog ----
22+ main () {
23+ # Read pre-captured stdin from tempfile.
24+ input=" $( cat " $INPUT_FILE " 2> /dev/null || true) "
2725
28- # Extract file_path field (very narrow JSON parse — avoid jq dependency).
29- file_path=" "
30- if [ -n " $input " ]; then
31- file_path=$( printf ' %s' " $input " | python3 -c "
26+ # Extract file_path field (very narrow JSON parse — avoid jq dependency).
27+ file_path=" "
28+ if [ -n " $input " ]; then
29+ file_path=$( printf ' %s' " $input " | python3 -c "
3230import json, sys
3331try:
3432 d = json.load(sys.stdin)
3735except Exception:
3836 pass
3937" 2> /dev/null || echo " " )
40- fi
41-
42- # No file path → nothing to check.
43- if [ -z " $file_path " ]; then
44- exit 0
45- fi
38+ fi
4639
47- # Find project root (where feature_list.json lives) — walk up from file_path.
48- project_root=" "
49- search_dir=" $( dirname " $file_path " ) "
50- while [ " $search_dir " != " /" ] && [ -n " $search_dir " ]; do
51- if [ -f " $search_dir /feature_list.json" ]; then
52- project_root=" $search_dir "
53- break
40+ # No file path → nothing to check.
41+ if [ -z " $file_path " ]; then
42+ return 0
5443 fi
55- search_dir=" $( dirname " $search_dir " ) "
56- done
5744
58- # No anchored project → nothing to check.
59- if [ -z " $project_root " ]; then
60- exit 0
61- fi
45+ # Find project root (where feature_list.json lives) — walk up from file_path.
46+ project_root=" "
47+ search_dir=" $( dirname " $file_path " ) "
48+ while [ " $search_dir " != " /" ] && [ -n " $search_dir " ]; do
49+ if [ -f " $search_dir /feature_list.json" ]; then
50+ project_root=" $search_dir "
51+ break
52+ fi
53+ search_dir=" $( dirname " $search_dir " ) "
54+ done
6255
63- warnings=()
56+ # No anchored project → nothing to check.
57+ if [ -z " $project_root " ]; then
58+ return 0
59+ fi
60+
61+ warnings=()
6462
65- # ---- Check 1: regression-warn ----
66- # If the edited file is referenced in evidence.artifacts of a feature marked
67- # status='pass', that's a regression risk. Surface a one-line warning.
68- rel_path=" ${file_path# $project_root / } "
69- regression=$( python3 - " $project_root /feature_list.json" " $rel_path " << 'PY ' 2>/dev/null || true
63+ # ---- Check 1: regression-warn ----
64+ # If the edited file is referenced in evidence.artifacts of a feature marked
65+ # status='pass', that's a regression risk. Surface a one-line warning.
66+ rel_path=" ${file_path# $project_root / } "
67+ regression=$( python3 - " $project_root /feature_list.json" " $rel_path " << 'PY ' 2>/dev/null || true
7068import json, sys, os
7169flist_path, rel = sys.argv[1], sys.argv[2]
7270try:
@@ -92,73 +90,92 @@ except Exception:
9290PY
9391)
9492
95- if [ -n " $regression " ]; then
96- warnings+=(" $regression " )
97- fi
93+ if [ -n " $regression " ]; then
94+ warnings+=(" $regression " )
95+ fi
9896
99- # ---- Check 2: C/C++ static analysis (clang-tidy on changed files) ----
100- # Phase 4: invoke clang-tidy if file is C/C++ and prerequisites are in place.
101- # Silent skip on anything missing — never pollute agent context with infra errors.
102- case " $file_path " in
103- * .c|* .cc|* .cpp|* .cxx|* .h|* .hpp|* .hh)
104- # Need clang-tidy on PATH
105- if command -v clang-tidy > /dev/null 2>&1 ; then
106- # Need compile_commands.json — check root, .build/, build/
107- cc_dir=" "
108- for d in " $project_root " " $project_root /.build" " $project_root /build" " $project_root /builddir" ; do
109- if [ -f " $d /compile_commands.json" ]; then
110- cc_dir=" $d "
111- break
112- fi
113- done
114- if [ -n " $cc_dir " ]; then
115- # 5-second timeout cap (BSD/macOS `timeout` may be `gtimeout`; try both)
116- tidy_cmd=" clang-tidy"
117- if command -v timeout > /dev/null 2>&1 ; then
118- tidy_cmd=" timeout 5 clang-tidy"
119- elif command -v gtimeout > /dev/null 2>&1 ; then
120- tidy_cmd=" gtimeout 5 clang-tidy"
121- fi
122- # --quiet suppresses progress; capture warnings only
123- tidy_out=$( $tidy_cmd -p " $cc_dir " --quiet " $file_path " 2> /dev/null | grep -E ' (warning|error):' | head -20 || true)
124- if [ -n " $tidy_out " ]; then
125- # Truncate to ~1500 chars to stay under hook budget
126- if [ " ${# tidy_out} " -gt 1500 ]; then
127- tidy_out=" ${tidy_out: 0: 1500}
128- ... (truncated; run clang-tidy directly for full output)"
97+ # ---- Check 2: C/C++ static analysis (clang-tidy on changed files) ----
98+ # Phase 4: invoke clang-tidy if file is C/C++ and prerequisites are in place.
99+ # Silent skip on anything missing — never pollute agent context with infra errors.
100+ case " $file_path " in
101+ * .c|* .cc|* .cpp|* .cxx|* .h|* .hpp|* .hh)
102+ # Need clang-tidy on PATH
103+ if command -v clang-tidy > /dev/null 2>&1 ; then
104+ # Need compile_commands.json — check root, .build/, build/
105+ cc_dir=" "
106+ for d in " $project_root " " $project_root /.build" " $project_root /build" " $project_root /builddir" ; do
107+ if [ -f " $d /compile_commands.json" ]; then
108+ cc_dir=" $d "
109+ break
110+ fi
111+ done
112+ if [ -n " $cc_dir " ]; then
113+ # 5-second timeout cap (BSD/macOS `timeout` may be `gtimeout`; try both)
114+ tidy_cmd=" clang-tidy"
115+ if command -v timeout > /dev/null 2>&1 ; then
116+ tidy_cmd=" timeout 5 clang-tidy"
117+ elif command -v gtimeout > /dev/null 2>&1 ; then
118+ tidy_cmd=" gtimeout 5 clang-tidy"
129119 fi
130- warnings+=(" clang-tidy on $( basename " $file_path " ) :
120+ # --quiet suppresses progress; capture warnings only
121+ tidy_out=$( $tidy_cmd -p " $cc_dir " --quiet " $file_path " 2> /dev/null | grep -E ' (warning|error):' | head -20 || true)
122+ if [ -n " $tidy_out " ]; then
123+ # Truncate to ~1500 chars to stay under hook budget
124+ if [ " ${# tidy_out} " -gt 1500 ]; then
125+ tidy_out=" ${tidy_out: 0: 1500}
126+ ... (truncated; run clang-tidy directly for full output)"
127+ fi
128+ warnings+=(" clang-tidy on $( basename " $file_path " ) :
131129$tidy_out " )
130+ fi
132131 fi
133132 fi
134- fi
135- ;;
136- esac
133+ ;;
134+ esac
137135
138- # ---- Emit output ----
139- if [ " ${# warnings[@]} " -eq 0 ]; then
140- exit 0
141- fi
136+ # ---- Emit output ----
137+ if [ " ${# warnings[@]} " -eq 0 ]; then
138+ return 0
139+ fi
142140
143- # Pure-bash JSON escape (no jq).
144- escape_for_json () {
145- local s=" $1 "
146- s=" ${s// \\ / \\\\ } "
147- s=" ${s// \" / \\\" } "
148- s=" ${s// $' \n ' / \\ n} "
149- s=" ${s// $' \r ' / \\ r} "
150- s=" ${s// $' \t ' / \\ t} "
151- printf ' %s' " $s "
141+ # Pure-bash JSON escape (no jq).
142+ escape_for_json () {
143+ local s=" $1 "
144+ s=" ${s// \\ / \\\\ } "
145+ s=" ${s// \" / \\\" } "
146+ s=" ${s// $' \n ' / \\ n} "
147+ s=" ${s// $' \r ' / \\ r} "
148+ s=" ${s// $' \t ' / \\ t} "
149+ printf ' %s' " $s "
150+ }
151+
152+ # Join warnings with newlines, then escape.
153+ joined=" "
154+ for w in " ${warnings[@]} " ; do
155+ if [ -z " $joined " ]; then joined=" $w " ; else joined=" $joined
156+ $w " ; fi
157+ done
158+ escaped=$( escape_for_json " $joined " )
159+
160+ printf ' {\n "hookSpecificOutput": {\n "hookEventName": "PostToolUse",\n "additionalContext": "<harness-anchor PostToolUse>\\n%s\\n</harness-anchor PostToolUse>"\n }\n}\n' " $escaped "
152161}
153162
154- # Join warnings with newlines, then escape.
155- joined=" "
156- for w in " ${warnings[@]} " ; do
157- if [ -z " $joined " ]; then joined=" $w " ; else joined=" $joined
158- $w " ; fi
159- done
160- escaped=$( escape_for_json " $joined " )
163+ # ---- R1: Total watchdog (5s cap, tempfile-guarded) ----
164+ OUTPUT_FILE=$( mktemp)
165+ main > " $OUTPUT_FILE " 2> /dev/null &
166+ main_pid=$!
161167
162- printf ' {\n "hookSpecificOutput": {\n "hookEventName": "PostToolUse",\n "additionalContext": "<harness-anchor PostToolUse>\\n%s\\n</harness-anchor PostToolUse>"\n }\n}\n' " $escaped "
168+ ( sleep 5 && kill -TERM $main_pid 2> /dev/null ) &
169+ watchdog_pid=$!
170+
171+ wait $main_pid 2> /dev/null
172+ main_rc=$?
173+ kill $watchdog_pid 2> /dev/null
174+ wait $watchdog_pid 2> /dev/null
175+
176+ if [ " $main_rc " -eq 0 ] && [ -f " $OUTPUT_FILE " ] && [ -s " $OUTPUT_FILE " ]; then
177+ cat " $OUTPUT_FILE "
178+ fi
179+ rm -f " $OUTPUT_FILE " " $INPUT_FILE " 2> /dev/null
163180
164181exit 0
0 commit comments