11#!/usr/bin/env node
22
33/**
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.
4+ * Stop hook: runs `nx affected -t test --base="HEAD"` and blocks on failures.
5+ * - No affected projects: skips entirely.
6+ * - All tests pass: brief success message via systemMessage.
7+ * - Any test failures: blocks the agent via hookSpecificOutput so it can
8+ * address failures before concluding.
109 *
1110 * Reads stdin to check stop_hook_active and avoid infinite loops.
1211 */
@@ -30,71 +29,65 @@ if (hookInput.stop_hook_active) {
3029
3130const repoRoot = path . resolve ( __dirname , '../../..' ) ;
3231
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;
32+ // Determine which projects are affected by uncommitted working tree changes.
33+ const affectedResult = spawnSync (
34+ 'pnpm' ,
35+ [ 'nx' , 'show' , 'projects' , '--affected' , '--base=HEAD' , '--json' ] ,
36+ { cwd : repoRoot , encoding : 'utf-8' , timeout : 30000 } ,
37+ ) ;
3538
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- } ) ;
39+ let affectedProjects = [ ] ;
40+ try {
41+ affectedProjects = JSON . parse ( affectedResult . stdout || '[]' ) ;
42+ } catch ( _ ) {
43+ // Parse failure — treat as no affected projects.
44+ }
4645
47- if ( ! hasCSharpChanges ) {
46+ if ( affectedProjects . length === 0 ) {
4847 process . stdout . write ( JSON . stringify ( {
49- systemMessage : 'dotnet build : skipped (no C# changes detected in working tree).' ,
48+ systemMessage : 'nx affected test : skipped (no affected projects in working tree).' ,
5049 } ) ) ;
5150 process . exit ( 0 ) ;
5251}
5352
54- const result = spawnSync ( 'dotnet' , [ 'build' , 'Flowthru.slnx' ] , {
55- cwd : repoRoot ,
56- encoding : 'utf-8' ,
57- timeout : 180000 ,
58- } ) ;
53+ // Run tests for all affected projects.
54+ const result = spawnSync (
55+ 'pnpm' ,
56+ [
57+ 'nx' , 'affected' , '-t' , 'test' ,
58+ '--base=HEAD' ,
59+ '--output-style=stream' ,
60+ '--logger' , 'console;verbosity=minimal' ,
61+ ] ,
62+ { cwd : repoRoot , encoding : 'utf-8' , timeout : 300000 } ,
63+ ) ;
5964
6065const stdout = ( result . stdout || '' ) . trim ( ) ;
6166const stderr = ( result . stderr || '' ) . trim ( ) ;
6267const combined = [ stdout , stderr ] . filter ( Boolean ) . join ( '\n' ) ;
6368
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 ) ) ;
69+ if ( result . status !== 0 ) {
70+ // Extract failed test lines for a focused summary.
71+ const failureLines = combined
72+ . split ( '\n' )
73+ . filter ( line => / f a i l e d | e r r o r | F A I L E D | E R R O R / i. test ( line ) )
74+ . slice ( 0 , 40 ) ; // cap at 40 lines to avoid overwhelming the agent
6875
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 ) ) ;
76+ const summary = failureLines . length > 0 ? failureLines . join ( '\n' ) : combined ;
7177
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
7778 process . stdout . write ( JSON . stringify ( {
7879 hookSpecificOutput : {
7980 hookEventName : 'Stop' ,
8081 decision : 'block' ,
8182 reason : [
82- 'dotnet build FAILED — address these errors before concluding.' ,
83+ `nx affected test FAILED (affected: ${ affectedProjects . join ( ', ' ) } ) — address these failures before concluding.` ,
8384 '' ,
84- diagnosticSummary ,
85+ summary ,
8586 ] . join ( '\n' ) ,
8687 } ,
8788 } ) ) ;
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- } ) ) ;
9689} else {
9790 process . stdout . write ( JSON . stringify ( {
98- systemMessage : 'dotnet build: succeeded with no warnings or errors.' ,
91+ systemMessage : `nx affected test: passed ( ${ affectedProjects . length } project(s): ${ affectedProjects . join ( ', ' ) } ).` ,
9992 } ) ) ;
10093}
0 commit comments