1- import type { ValidationResult } from "./round-state"
1+ import type { ValidationFailureKind , ValidationResult } from "./round-state"
2+
3+ export type ValidationCommandSource = "package_script" | "builtin" | "agents_md" | "user"
4+ export type ValidationScriptDialect = "posix"
5+
6+ export type ValidationCommand =
7+ | {
8+ readonly id : string
9+ readonly source : ValidationCommandSource
10+ readonly transport : "argv"
11+ readonly executable : string
12+ readonly args : readonly string [ ]
13+ readonly display : string
14+ }
15+ | {
16+ readonly id : string
17+ readonly source : ValidationCommandSource
18+ readonly transport : ValidationScriptDialect
19+ readonly script : string
20+ readonly display : string
21+ }
22+
23+ export type ValidationCommandInput = string | ValidationCommand
224
325export type ValidationPlan = {
4- readonly commands : readonly string [ ]
26+ readonly commands : readonly ValidationCommand [ ]
527 readonly timeout_ms : number
628 readonly failFast : boolean
729}
830
931export type ValidationConfig = {
1032 readonly cwd : string
11- readonly commands : readonly string [ ]
33+ readonly commands : readonly ValidationCommandInput [ ]
1234 readonly timeout_ms ?: number
1335}
1436
15- export const inferValidationCommands = ( context : {
37+ export const inferValidationPlan = ( context : {
1638 readonly cwd : string
1739 readonly packageJson ?: { scripts ?: Record < string , string > }
1840 readonly agentsMd ?: string
@@ -21,40 +43,105 @@ export const inferValidationCommands = (context: {
2143 // The package-script runner for this workspace (e.g. "npm run", "bun run"). Defaults to npm.
2244 // P2-7: single inference impl; the deepagent-code production path passes "bun run".
2345 readonly runner ?: string
24- } ) : string [ ] => {
25- const commands : string [ ] = [ ]
46+ } ) : ValidationCommand [ ] => {
47+ const commands : ValidationCommand [ ] = [ ]
2648 const run = context . runner ?? "npm run"
27- const runnerBin = run . split ( / \s + / ) [ 0 ] ?? "npm" // "bun"/"npm" for the bare typecheck fallback
49+ const runner = run . trim ( ) . split ( / \s + / ) . filter ( Boolean )
50+ const runnerBin = runner [ 0 ] ?? "npm"
51+ const packageScript = ( name : string ) : ValidationCommand => ( {
52+ id : `package:${ name } ` ,
53+ source : "package_script" ,
54+ transport : "argv" ,
55+ executable : runnerBin ,
56+ args : [ ...runner . slice ( 1 ) , name ] ,
57+ display : `${ run } ${ name } ` ,
58+ } )
2859
2960 if ( context . packageJson ?. scripts ) {
3061 const scripts = context . packageJson . scripts
31- if ( scripts . typecheck ) commands . push ( `${ run } typecheck` )
32- else if ( scripts [ "type-check" ] ) commands . push ( `${ run } type-check` )
33- else if ( context . hasTypeScript ) commands . push ( runnerBin === "bun" ? "bun typecheck" : "npx tsc --noEmit" )
62+ if ( scripts . typecheck ) commands . push ( packageScript ( "typecheck" ) )
63+ else if ( scripts [ "type-check" ] ) commands . push ( packageScript ( "type-check" ) )
64+ else if ( context . hasTypeScript )
65+ commands . push (
66+ runnerBin === "bun"
67+ ? {
68+ id : "builtin:typecheck" ,
69+ source : "builtin" ,
70+ transport : "argv" ,
71+ executable : "bun" ,
72+ args : [ "typecheck" ] ,
73+ display : "bun typecheck" ,
74+ }
75+ : {
76+ id : "builtin:typecheck" ,
77+ source : "builtin" ,
78+ transport : "argv" ,
79+ executable : "npx" ,
80+ args : [ "tsc" , "--noEmit" ] ,
81+ display : "npx tsc --noEmit" ,
82+ } ,
83+ )
3484
35- if ( scripts . lint ) commands . push ( ` ${ run } lint` )
85+ if ( scripts . lint ) commands . push ( packageScript ( " lint" ) )
3686 // P1-3: the test command is part of the micro-round validation gate — a failing test means
3787 // "not done". Only added when a test script actually exists (no blind test runs).
38- if ( scripts . test ) commands . push ( ` ${ run } test` )
39- if ( scripts . build && ! scripts . test ) commands . push ( ` ${ run } build` )
88+ if ( scripts . test ) commands . push ( packageScript ( " test" ) )
89+ if ( scripts . build && ! scripts . test ) commands . push ( packageScript ( " build" ) )
4090 } else if ( context . hasTypeScript ) {
41- commands . push ( "npx tsc --noEmit" )
91+ commands . push ( {
92+ id : "builtin:typecheck" ,
93+ source : "builtin" ,
94+ transport : "argv" ,
95+ executable : "npx" ,
96+ args : [ "tsc" , "--noEmit" ] ,
97+ display : "npx tsc --noEmit" ,
98+ } )
4299 }
43100
44101 if ( context . hasPython ) {
45- commands . push ( "python -m py_compile *.py" )
102+ commands . push ( {
103+ id : "builtin:python-compile" ,
104+ source : "builtin" ,
105+ transport : "argv" ,
106+ executable : "python" ,
107+ args : [ "-m" , "compileall" , "-q" , "." ] ,
108+ display : "python -m compileall -q ." ,
109+ } )
46110 }
47111
48112 if ( context . agentsMd ) {
49113 const inferredFromAgents = extractCommandsFromAgentsMd ( context . agentsMd )
50- for ( const cmd of inferredFromAgents ) {
51- if ( ! commands . includes ( cmd ) ) commands . push ( cmd )
52- }
114+ for ( const cmd of inferredFromAgents )
115+ if ( ! commands . some ( ( item ) => item . display === cmd ) )
116+ commands . push ( {
117+ id : `agents:${ commands . length } ` ,
118+ source : "agents_md" ,
119+ transport : "posix" ,
120+ script : cmd ,
121+ display : cmd ,
122+ } )
53123 }
54124
55125 return commands
56126}
57127
128+ export const inferValidationCommands = ( context : Parameters < typeof inferValidationPlan > [ 0 ] ) : string [ ] =>
129+ inferValidationPlan ( context ) . map ( ( command ) => command . display )
130+
131+ export const normalizeValidationCommand = ( command : ValidationCommandInput ) : ValidationCommand =>
132+ typeof command === "string"
133+ ? {
134+ id : `user:${ command } ` ,
135+ source : "user" ,
136+ transport : "posix" ,
137+ script : command ,
138+ display : command ,
139+ }
140+ : command
141+
142+ export const validationCommandDisplay = ( command : ValidationCommandInput ) : string =>
143+ normalizeValidationCommand ( command ) . display
144+
58145// P2-7: the single AGENTS.md command extractor (was duplicated in workspace-context with a
59146// drifting regex). Matches both "`cmd` - typecheck" list items and "run `cmd` to typecheck" prose.
60147export const extractCommandsFromAgentsMd = ( content : string ) : string [ ] => {
@@ -70,7 +157,10 @@ export const extractCommandsFromAgentsMd = (content: string): string[] => {
70157}
71158
72159export const buildValidationPlan = ( config : ValidationConfig ) : ValidationPlan => ( {
73- commands : config . commands . length > 0 ? config . commands : [ "echo 'no validation commands configured'" ] ,
160+ commands :
161+ config . commands . length > 0
162+ ? config . commands . map ( normalizeValidationCommand )
163+ : [ normalizeValidationCommand ( "echo 'no validation commands configured'" ) ] ,
74164 timeout_ms : config . timeout_ms ?? 60_000 ,
75165 failFast : true ,
76166} )
@@ -80,9 +170,11 @@ export const parseValidationOutput = (
80170 exitCode : number ,
81171 output : string ,
82172 duration_ms : number ,
173+ kind : ValidationFailureKind = "command_exit" ,
83174) : ValidationResult => ( {
84175 command,
85- passed : exitCode === 0 ,
176+ passed : kind === "command_exit" && exitCode === 0 ,
177+ kind,
86178 exit_code : exitCode ,
87179 output : output . slice ( - 4000 ) ,
88180 duration_ms,
0 commit comments