@@ -44,6 +44,7 @@ function parseArgs(argv) {
4444 dryRun : false ,
4545 hud : true ,
4646 inputOverrides : { } ,
47+ log : true ,
4748 recipe : '' ,
4849 singleStep : '' ,
4950 skipManual : false ,
@@ -64,6 +65,9 @@ function parseArgs(argv) {
6465 case '--no-hud' :
6566 options . hud = false ;
6667 break ;
68+ case '--no-log' :
69+ options . log = false ;
70+ break ;
6771 case '--skip-manual' :
6872 options . skipManual = true ;
6973 break ;
@@ -124,6 +128,7 @@ function printHelp() {
124128 [--testnet]
125129 [--artifacts-dir <path>]
126130 [--dry-run]
131+ [--no-log]
127132
128133The runner executes workflow files stored under:
129134 scripts/perps/agentic/teams/<team>/{flows,recipes}
@@ -133,7 +138,8 @@ Runtime features:
133138 - Scenarios use validate.workflow with explicit nodes, transitions, switch branches, and end nodes.
134139 - setup / teardown hooks live under validate.workflow.setup / validate.workflow.teardown.
135140 - Failures capture screenshots, route/state snapshots, eval refs, and recent logs.
136- - Successful runs emit workflow.json, workflow.mmd, trace.json, and summary.json artifacts.` ) ;
141+ - Successful runs emit workflow.json, workflow.mmd, trace.json, summary.json, and run.log artifacts.
142+ - Console output is teed to run.log by default. Use --no-log to disable.` ) ;
137143}
138144
139145function resolveRecipeInput ( appRoot , inputPath ) {
@@ -546,6 +552,7 @@ function ensureRunArtifacts(runOptions, recipePath) {
546552 workflowPath : path . join ( rootDir , 'workflow.json' ) ,
547553 workflowMermaidPath : path . join ( rootDir , 'workflow.mmd' ) ,
548554 summaryPath : path . join ( rootDir , 'summary.json' ) ,
555+ runLogPath : path . join ( rootDir , 'run.log' ) ,
549556 } ;
550557
551558 [ artifacts . rootDir , artifacts . screenshotsDir , artifacts . failuresDir , artifacts . logsDir ]
@@ -1361,6 +1368,25 @@ async function executeEndNode(node, context) {
13611368}
13621369
13631370async function executeWorkflowNode ( node , context , options = { } ) {
1371+ // Handle when-guard for switch/end nodes (runExecutableNode handles its own)
1372+ if ( ( node . action === 'switch' || node . action === 'end' ) && node . when && ! evaluateWorkflowCondition ( node . when , context ) ) {
1373+ const startedAt = new Date ( ) . toISOString ( ) ;
1374+ context . currentStepRef . current = node ;
1375+ context . stats . total += 1 ;
1376+ context . stats . skipped += 1 ;
1377+ console . log ( `[${ node . id || '?' } ] ${ describeStep ( node ) } ` ) ;
1378+ console . log ( ' [SKIPPED - when condition did not match]' ) ;
1379+ console . log ( '' ) ;
1380+ finalizeNodeRecord ( context , node , {
1381+ next : node . default || node . next || '' ,
1382+ note : 'when condition did not match' ,
1383+ startedAt,
1384+ status : 'skipped' ,
1385+ } ) ;
1386+ context . currentStepRef . current = null ;
1387+ return { next : node . default || node . next || '' } ;
1388+ }
1389+
13641390 if ( node . action === 'end' ) {
13651391 return executeEndNode ( node , context ) ;
13661392 }
@@ -1605,6 +1631,7 @@ async function runRecipe(recipePath, runOptions, flowParams = {}, depth = 0) {
16051631// ---------------------------------------------------------------------------
16061632
16071633async function main ( ) {
1634+ let teardownLog = null ;
16081635 try {
16091636 const options = parseArgs ( process . argv . slice ( 2 ) ) ;
16101637 const appRoot = getAppRoot ( ) ;
@@ -1621,10 +1648,34 @@ async function main() {
16211648 dryRun : options . dryRun ,
16221649 hud : options . hud ,
16231650 inputOverrides : options . inputOverrides ,
1651+ log : options . log ,
16241652 singleStep : options . singleStep ,
16251653 skipManual : options . skipManual ,
16261654 } ;
16271655
1656+ // Set up log capture — tee stdout/stderr to a log file
1657+ if ( options . log && ! options . dryRun ) {
1658+ const artifacts = ensureRunArtifacts ( runOptions , recipeInput . recipePath ) ;
1659+ if ( artifacts ) {
1660+ const logStream = fs . createWriteStream ( artifacts . runLogPath , { flags : 'w' } ) ;
1661+ const origLog = console . log . bind ( console ) ;
1662+ const origError = console . error . bind ( console ) ;
1663+ console . log = ( ...args ) => {
1664+ origLog ( ...args ) ;
1665+ logStream . write ( args . map ( String ) . join ( ' ' ) + '\n' ) ;
1666+ } ;
1667+ console . error = ( ...args ) => {
1668+ origError ( ...args ) ;
1669+ logStream . write ( args . map ( String ) . join ( ' ' ) + '\n' ) ;
1670+ } ;
1671+ teardownLog = ( ) => {
1672+ console . log = origLog ;
1673+ console . error = origError ;
1674+ logStream . end ( ) ;
1675+ } ;
1676+ }
1677+ }
1678+
16281679 // Apply CLI initial conditions before running the recipe
16291680 if ( ! options . dryRun ) {
16301681 if ( options . account ) {
@@ -1646,7 +1697,13 @@ async function main() {
16461697 }
16471698
16481699 await runRecipe ( recipeInput . recipePath , runOptions , runOptions . inputOverrides ) ;
1700+ if ( teardownLog ) {
1701+ teardownLog ( ) ;
1702+ }
16491703 } catch ( error ) {
1704+ if ( teardownLog ) {
1705+ teardownLog ( ) ;
1706+ }
16501707 console . error ( String ( error . message || error ) ) ;
16511708 process . exit ( 1 ) ;
16521709 }
0 commit comments