@@ -8,7 +8,6 @@ import { SkillWatcher } from "./watcher.js";
88import { evaluate , ExpectSchema , type AssertionContext } from "./assertions.js" ;
99
1010export const DEFAULT_STEP_TIMEOUT_SECONDS = 300 ;
11-
1211// --- Schemas ---
1312
1413const InvokeSchema = z . object ( {
@@ -39,6 +38,12 @@ const DeleteAgentSchema = z.object({
3938 name : z . string ( ) ,
4039} ) ;
4140
41+ const StepConditionSchema = z . object ( {
42+ agent : z . string ( ) . optional ( ) ,
43+ language : z . string ( ) . optional ( ) ,
44+ os : z . string ( ) . optional ( ) ,
45+ } ) ;
46+
4247const ACTION_FIELDS = [
4348 "prompt" ,
4449 "invoke" ,
@@ -71,6 +76,8 @@ const StepSpecSchema = z
7176 trigger : TriggerSchema . optional ( ) ,
7277 create_agent : CreateAgentSchema . optional ( ) ,
7378 delete_agent : DeleteAgentSchema . optional ( ) ,
79+ only_if : StepConditionSchema . optional ( ) ,
80+ skip_if : StepConditionSchema . optional ( ) ,
7481 } )
7582 . refine (
7683 ( step ) => {
@@ -94,7 +101,6 @@ const SettingsSchema = z
94101 cleanup : z . boolean ( ) . optional ( ) ,
95102 } )
96103 . optional ( ) ;
97-
98104const PrerequisitesSchema = z
99105 . object ( {
100106 env : z . record ( z . string ( ) ) . optional ( ) ,
@@ -120,6 +126,8 @@ interface StepCommon {
120126 deploy ?: boolean ;
121127 } ;
122128 expect ?: z . infer < typeof ExpectSchema > ;
129+ only_if ?: StepCondition ;
130+ skip_if ?: StepCondition ;
123131}
124132
125133type InvokeSpec = { agent : string ; function : string ; args ?: string } ;
@@ -248,6 +256,57 @@ export interface ScenarioRunResult {
248256
249257export interface ScenarioExecutorOptions {
250258 globalTimeoutSeconds ?: number ;
259+ agent ?: string ;
260+ language ?: string ;
261+ abortSignal ?: AbortSignal ;
262+ }
263+
264+ // --- Template variable substitution ---
265+
266+ export function substituteVariables (
267+ text : string ,
268+ variables : Record < string , string > ,
269+ ) : string {
270+ return text . replace ( / \{ \{ ( \w + ) \} \} / g, ( match , name : string ) => {
271+ return variables [ name ] ?? match ;
272+ } ) ;
273+ }
274+
275+ // --- Conditional step execution ---
276+
277+ export interface StepCondition {
278+ agent ?: string ;
279+ language ?: string ;
280+ os ?: string ;
281+ }
282+
283+ function normalizePlatform ( platform : string ) : string {
284+ if ( platform === "darwin" ) return "macos" ;
285+ if ( platform === "win32" ) return "windows" ;
286+ return platform ;
287+ }
288+
289+ export function shouldRunStep (
290+ step : StepSpec ,
291+ context : { agent ?: string ; language ?: string ; os : string } ,
292+ ) : boolean {
293+ const normalizedOs = normalizePlatform ( context . os ) ;
294+
295+ if ( step . only_if ) {
296+ const cond = step . only_if ;
297+ if ( cond . agent && cond . agent !== context . agent ) return false ;
298+ if ( cond . language && cond . language !== context . language ) return false ;
299+ if ( cond . os && cond . os !== normalizedOs ) return false ;
300+ }
301+
302+ if ( step . skip_if ) {
303+ const cond = step . skip_if ;
304+ if ( cond . agent && cond . agent === context . agent ) return false ;
305+ if ( cond . language && cond . language === context . language ) return false ;
306+ if ( cond . os && cond . os === normalizedOs ) return false ;
307+ }
308+
309+ return true ;
251310}
252311
253312export class ScenarioExecutor {
@@ -271,6 +330,64 @@ export class ScenarioExecutor {
271330 this . options = options ?? { } ;
272331 }
273332
333+ private buildVariables ( scenarioName : string ) : Record < string , string > {
334+ const vars : Record < string , string > = {
335+ workspace : this . workspace ,
336+ scenario : scenarioName ,
337+ } ;
338+ if ( this . options . agent ) vars [ "agent" ] = this . options . agent ;
339+ if ( this . options . language ) vars [ "language" ] = this . options . language ;
340+ return vars ;
341+ }
342+
343+ private substituteStepVariables (
344+ step : StepSpec ,
345+ variables : Record < string , string > ,
346+ ) : StepSpec {
347+ const sub = ( s : string | undefined ) =>
348+ s ? substituteVariables ( s , variables ) : s ;
349+ const subArr = ( arr : string [ ] | undefined ) =>
350+ arr ?. map ( ( s ) => substituteVariables ( s , variables ) ) ;
351+
352+ return {
353+ ...step ,
354+ prompt : sub ( step . prompt ) ,
355+ shell : step . shell
356+ ? {
357+ command : substituteVariables ( step . shell . command , variables ) ,
358+ args : subArr ( step . shell . args ) ,
359+ cwd : sub ( step . shell . cwd ) ,
360+ }
361+ : step . shell ,
362+ invoke : step . invoke
363+ ? {
364+ agent : substituteVariables ( step . invoke . agent , variables ) ,
365+ function : substituteVariables ( step . invoke . function , variables ) ,
366+ args : sub ( step . invoke . args ) ,
367+ }
368+ : step . invoke ,
369+ trigger : step . trigger
370+ ? {
371+ agent : substituteVariables ( step . trigger . agent , variables ) ,
372+ function : substituteVariables ( step . trigger . function , variables ) ,
373+ args : sub ( step . trigger . args ) ,
374+ }
375+ : step . trigger ,
376+ create_agent : step . create_agent
377+ ? {
378+ ...step . create_agent ,
379+ name : substituteVariables ( step . create_agent . name , variables ) ,
380+ }
381+ : step . create_agent ,
382+ delete_agent : step . delete_agent
383+ ? {
384+ ...step . delete_agent ,
385+ name : substituteVariables ( step . delete_agent . name , variables ) ,
386+ }
387+ : step . delete_agent ,
388+ } as StepSpec ;
389+ }
390+
274391 async execute ( spec : ScenarioSpec ) : Promise < ScenarioRunResult > {
275392 const results : StepResult [ ] = [ ] ;
276393 const savedEnv : Record < string , string | undefined > = { } ;
@@ -304,11 +421,37 @@ export class ScenarioExecutor {
304421
305422 // Build extra env for commands from settings
306423 const commandEnv = this . buildCommandEnv ( spec ) ;
424+ const variables = this . buildVariables ( spec . name ) ;
425+ const conditionContext = {
426+ agent : this . options . agent ,
427+ language : this . options . language ,
428+ os : process . platform ,
429+ } ;
307430
308431 const startTime = Date . now ( ) ;
309432 let isFirstPrompt = true ;
310433 try {
311- for ( const step of spec . steps ) {
434+ for ( const originalStep of spec . steps ) {
435+ // Check abort signal
436+ if ( this . options . abortSignal ?. aborted ) break ;
437+
438+ // Substitute template variables
439+ const step = this . substituteStepVariables ( originalStep , variables ) ;
440+
441+ // Conditional execution
442+ if ( ! shouldRunStep ( step , conditionContext ) ) {
443+ console . log (
444+ `Step ${ step . id ?? "(unnamed)" } : skipped (condition not met)` ,
445+ ) ;
446+ results . push ( {
447+ step : originalStep ,
448+ success : true ,
449+ durationSeconds : 0 ,
450+ expectedSkills : step . expectedSkills ?? [ ] ,
451+ activatedSkills : [ ] ,
452+ } ) ;
453+ continue ;
454+ }
312455 const stepStartTime = Date . now ( ) ;
313456 let stepSuccess = true ;
314457 const stepErrors : string [ ] = [ ] ;
@@ -601,7 +744,7 @@ export class ScenarioExecutor {
601744 }
602745
603746 results . push ( {
604- step,
747+ step : originalStep ,
605748 success : stepSuccess ,
606749 durationSeconds : ( Date . now ( ) - stepStartTime ) / 1000 ,
607750 expectedSkills : step . expectedSkills ?? [ ] ,
0 commit comments