11#!/usr/bin/env node
22
33/**
4- * Stop hook: runs `dotnet build` before the agent yields back to the user and
5- * injects the result as a system message. Build errors surface immediately so
6- * the agent can address them before the next prompt rather than discovering
7- * them later.
4+ * Stop hook: runs `dotnet build` and reports only warnings/errors.
5+ * - No C# changes in working tree: skips the build entirely.
6+ * - Clean build: brief success message via systemMessage.
7+ * - Warnings present: systemMessage with filtered warning lines.
8+ * - Build failure / errors: blocks the agent via hookSpecificOutput so it can
9+ * address errors before concluding.
10+ *
11+ * Reads stdin to check stop_hook_active and avoid infinite loops.
812 */
913
1014const { spawnSync } = require ( 'child_process' ) ;
1115const path = require ( 'path' ) ;
1216
17+ // Read and parse stdin to detect re-entry.
18+ let hookInput = { } ;
19+ try {
20+ const raw = require ( 'fs' ) . readFileSync ( '/dev/stdin' , 'utf-8' ) . trim ( ) ;
21+ if ( raw ) hookInput = JSON . parse ( raw ) ;
22+ } catch ( _ ) {
23+ // stdin unavailable or empty — treat as first invocation.
24+ }
25+
26+ // Prevent infinite loop: if we are already in a stop-hook continuation, exit cleanly.
27+ if ( hookInput . stop_hook_active ) {
28+ process . exit ( 0 ) ;
29+ }
30+
1331const repoRoot = path . resolve ( __dirname , '../../..' ) ;
1432
33+ // Short-circuit if no C#-related files are dirty (staged or unstaged vs HEAD).
34+ const CSHARP_PATTERN = / \. ( c s | c s p r o j | f s p r o j | s l n x | s l n | p r o p s | t a r g e t s ) $ | ^ ( g l o b a l \. j s o n | N u G e t \. C o n f i g ) $ / i;
35+
36+ const gitStatus = spawnSync ( 'git' , [ 'status' , '--porcelain' ] , {
37+ cwd : repoRoot ,
38+ encoding : 'utf-8' ,
39+ } ) ;
40+ const dirtyFiles = ( gitStatus . stdout || '' ) . trim ( ) . split ( '\n' ) . filter ( Boolean ) ;
41+ const hasCSharpChanges = dirtyFiles . some ( line => {
42+ // Each line is "XY filename" or "XY old -> new"; grab the last path segment.
43+ const filePath = line . slice ( 3 ) . trim ( ) . split ( ' -> ' ) . pop ( ) ;
44+ return CSHARP_PATTERN . test ( path . basename ( filePath ) ) ;
45+ } ) ;
46+
47+ if ( ! hasCSharpChanges ) {
48+ process . stdout . write ( JSON . stringify ( {
49+ systemMessage : 'dotnet build: skipped (no C# changes detected in working tree).' ,
50+ } ) ) ;
51+ process . exit ( 0 ) ;
52+ }
53+
1554const result = spawnSync ( 'dotnet' , [ 'build' , 'Flowthru.slnx' ] , {
1655 cwd : repoRoot ,
1756 encoding : 'utf-8' ,
@@ -22,21 +61,40 @@ const stdout = (result.stdout || '').trim();
2261const stderr = ( result . stderr || '' ) . trim ( ) ;
2362const combined = [ stdout , stderr ] . filter ( Boolean ) . join ( '\n' ) ;
2463
25- const succeeded = result . status === 0 ;
26- const headline = succeeded
27- ? 'dotnet build succeeded.'
28- : 'dotnet build FAILED — address these errors before concluding.' ;
29-
30- const output = {
31- systemMessage : [
32- `## Build Check (dotnet build)` ,
33- '' ,
34- headline ,
35- '' ,
36- '```' ,
37- combined ,
38- '```' ,
39- ] . join ( '\n' ) ,
40- } ;
41-
42- process . stdout . write ( JSON . stringify ( output ) ) ;
64+ // Extract compiler diagnostics: lines containing ': warning XXXX' or ': error XXXX'.
65+ const diagnosticLines = combined
66+ . split ( '\n' )
67+ . filter ( line => / : \s * ( w a r n i n g | e r r o r ) \s + [ A - Z a - z ] * \d + / i. test ( line ) ) ;
68+
69+ const hasErrors = result . status !== 0 || diagnosticLines . some ( l => / : \s * e r r o r \s + / i. test ( l ) ) ;
70+ const hasWarnings = diagnosticLines . some ( l => / : \s * w a r n i n g \s + / i. test ( l ) ) ;
71+
72+ if ( hasErrors ) {
73+ // Block the agent so it addresses errors before concluding.
74+ const diagnosticSummary = diagnosticLines . length > 0
75+ ? diagnosticLines . join ( '\n' )
76+ : combined ; // fall back to full output if pattern didn't match anything
77+ process . stdout . write ( JSON . stringify ( {
78+ hookSpecificOutput : {
79+ hookEventName : 'Stop' ,
80+ decision : 'block' ,
81+ reason : [
82+ 'dotnet build FAILED — address these errors before concluding.' ,
83+ '' ,
84+ diagnosticSummary ,
85+ ] . join ( '\n' ) ,
86+ } ,
87+ } ) ) ;
88+ } else if ( hasWarnings ) {
89+ process . stdout . write ( JSON . stringify ( {
90+ systemMessage : [
91+ 'dotnet build succeeded with warnings:' ,
92+ '' ,
93+ diagnosticLines . join ( '\n' ) ,
94+ ] . join ( '\n' ) ,
95+ } ) ) ;
96+ } else {
97+ process . stdout . write ( JSON . stringify ( {
98+ systemMessage : 'dotnet build: succeeded with no warnings or errors.' ,
99+ } ) ) ;
100+ }
0 commit comments