Skip to content

Commit bd2da4e

Browse files
committed
fix(perps): add when-guard for switch/end nodes + log capture
Switch and end nodes bypassed the when-guard check because executeWorkflowNode dispatched them directly to executeSwitchNode/ executeEndNode, skipping runExecutableNode which handles when. This caused a TypeError when test_restart=false (default) because the switch tried to read result from a skipped node. Also adds automatic run.log capture — console output is teed to a log file in the artifacts directory by default. Use --no-log to disable.
1 parent 2684700 commit bd2da4e

File tree

1 file changed

+58
-1
lines changed

1 file changed

+58
-1
lines changed

scripts/perps/agentic/validate-recipe.js

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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
128133
The 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

139145
function 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

13631370
async 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

16071633
async 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

Comments
 (0)