@agent-relay/sdk 4.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/agent-relay-broker-darwin-arm64 +0 -0
- package/bin/agent-relay-broker-darwin-x64 +0 -0
- package/bin/agent-relay-broker-linux-arm64 +0 -0
- package/bin/agent-relay-broker-linux-x64 +0 -0
- package/dist/broker-path.d.ts +3 -2
- package/dist/broker-path.d.ts.map +1 -1
- package/dist/broker-path.js +119 -32
- package/dist/broker-path.js.map +1 -1
- package/dist/client.d.ts +12 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +20 -1
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/relay.d.ts +2 -1
- package/dist/relay.d.ts.map +1 -1
- package/dist/relay.js +1 -1
- package/dist/relay.js.map +1 -1
- package/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
- package/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
- package/dist/workflows/__tests__/channel-messenger.test.js +117 -0
- package/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
- package/dist/workflows/__tests__/run-summary-table.test.js +4 -3
- package/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
- package/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
- package/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
- package/dist/workflows/__tests__/step-executor.test.js +378 -0
- package/dist/workflows/__tests__/step-executor.test.js.map +1 -0
- package/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
- package/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
- package/dist/workflows/__tests__/template-resolver.test.js +145 -0
- package/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
- package/dist/workflows/__tests__/verification.test.d.ts +2 -0
- package/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
- package/dist/workflows/__tests__/verification.test.js +170 -0
- package/dist/workflows/__tests__/verification.test.js.map +1 -0
- package/dist/workflows/builder.d.ts +3 -2
- package/dist/workflows/builder.d.ts.map +1 -1
- package/dist/workflows/builder.js +1 -3
- package/dist/workflows/builder.js.map +1 -1
- package/dist/workflows/channel-messenger.d.ts +28 -0
- package/dist/workflows/channel-messenger.d.ts.map +1 -0
- package/dist/workflows/channel-messenger.js +255 -0
- package/dist/workflows/channel-messenger.js.map +1 -0
- package/dist/workflows/index.d.ts +7 -0
- package/dist/workflows/index.d.ts.map +1 -1
- package/dist/workflows/index.js +7 -0
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/process-spawner.d.ts +35 -0
- package/dist/workflows/process-spawner.d.ts.map +1 -0
- package/dist/workflows/process-spawner.js +141 -0
- package/dist/workflows/process-spawner.js.map +1 -0
- package/dist/workflows/run.d.ts +2 -1
- package/dist/workflows/run.d.ts.map +1 -1
- package/dist/workflows/run.js.map +1 -1
- package/dist/workflows/runner.d.ts +6 -6
- package/dist/workflows/runner.d.ts.map +1 -1
- package/dist/workflows/runner.js +443 -719
- package/dist/workflows/runner.js.map +1 -1
- package/dist/workflows/step-executor.d.ts +95 -0
- package/dist/workflows/step-executor.d.ts.map +1 -0
- package/dist/workflows/step-executor.js +393 -0
- package/dist/workflows/step-executor.js.map +1 -0
- package/dist/workflows/template-resolver.d.ts +33 -0
- package/dist/workflows/template-resolver.d.ts.map +1 -0
- package/dist/workflows/template-resolver.js +144 -0
- package/dist/workflows/template-resolver.js.map +1 -0
- package/dist/workflows/verification.d.ts +33 -0
- package/dist/workflows/verification.d.ts.map +1 -0
- package/dist/workflows/verification.js +122 -0
- package/dist/workflows/verification.js.map +1 -0
- package/package.json +2 -2
package/dist/workflows/runner.js
CHANGED
|
@@ -18,13 +18,68 @@ import { resolveCliSync } from '../cli-resolver.js';
|
|
|
18
18
|
import { loadCustomSteps, resolveAllCustomSteps, validateCustomStepsUsage, CustomStepsParseError, CustomStepResolutionError, } from './custom-steps.js';
|
|
19
19
|
import { collectCliSession } from './cli-session-collector.js';
|
|
20
20
|
import { executeApiStep } from './api-executor.js';
|
|
21
|
+
import { ChannelMessenger } from './channel-messenger.js';
|
|
21
22
|
import { InMemoryWorkflowDb } from './memory-db.js';
|
|
23
|
+
import { buildCommand as buildProcessCommand, spawnProcess } from './process-spawner.js';
|
|
22
24
|
import { formatRunSummaryTable } from './run-summary-table.js';
|
|
25
|
+
import { StepExecutor as WorkflowStepLifecycleExecutor, } from './step-executor.js';
|
|
26
|
+
import { interpolateStepTask as interpolateStepTaskTemplate, resolveDotPath as resolveTemplateDotPath, resolveTemplate, TemplateResolver, } from './template-resolver.js';
|
|
23
27
|
import { WorkflowTrajectory } from './trajectory.js';
|
|
28
|
+
import { runVerification, stripInjectedTaskEcho, WorkflowCompletionError, } from './verification.js';
|
|
24
29
|
// ── AgentRelay SDK imports ──────────────────────────────────────────────────
|
|
25
30
|
// Import from sub-paths to avoid pulling in the full @relaycast/sdk dependency.
|
|
26
31
|
import { AgentRelay } from '../relay.js';
|
|
27
32
|
import { RelayCast, RelayError } from '@relaycast/sdk';
|
|
33
|
+
// ── Environment filtering ──────────────────────────────────────────────────
|
|
34
|
+
/** Keys explicitly allowed to propagate to spawned child processes. */
|
|
35
|
+
const ENV_ALLOWLIST = new Set([
|
|
36
|
+
'PATH',
|
|
37
|
+
'HOME',
|
|
38
|
+
'USER',
|
|
39
|
+
'SHELL',
|
|
40
|
+
'LANG',
|
|
41
|
+
'TERM',
|
|
42
|
+
'TMPDIR',
|
|
43
|
+
'TZ',
|
|
44
|
+
'NODE_ENV',
|
|
45
|
+
'NODE_PATH',
|
|
46
|
+
'NODE_OPTIONS',
|
|
47
|
+
'NODE_EXTRA_CA_CERTS',
|
|
48
|
+
'RUST_LOG',
|
|
49
|
+
'RUST_BACKTRACE',
|
|
50
|
+
'RELAY_API_KEY',
|
|
51
|
+
'RELAYCAST_BASE_URL',
|
|
52
|
+
'AGENT_RELAY_DASHBOARD_PORT',
|
|
53
|
+
'AGENT_RELAY_RUN_ID_FILE',
|
|
54
|
+
'EDITOR',
|
|
55
|
+
'VISUAL',
|
|
56
|
+
'GIT_AUTHOR_NAME',
|
|
57
|
+
'GIT_AUTHOR_EMAIL',
|
|
58
|
+
'GIT_COMMITTER_NAME',
|
|
59
|
+
'GIT_COMMITTER_EMAIL',
|
|
60
|
+
'HTTPS_PROXY',
|
|
61
|
+
'HTTP_PROXY',
|
|
62
|
+
'NO_PROXY',
|
|
63
|
+
'https_proxy',
|
|
64
|
+
'http_proxy',
|
|
65
|
+
'no_proxy',
|
|
66
|
+
'XDG_CONFIG_HOME',
|
|
67
|
+
'XDG_DATA_HOME',
|
|
68
|
+
'XDG_CACHE_HOME',
|
|
69
|
+
]);
|
|
70
|
+
/** Return a filtered copy of process.env containing only allowlisted keys. */
|
|
71
|
+
function filteredEnv(extra) {
|
|
72
|
+
const env = {};
|
|
73
|
+
for (const key of ENV_ALLOWLIST) {
|
|
74
|
+
if (process.env[key] !== undefined) {
|
|
75
|
+
env[key] = process.env[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (extra) {
|
|
79
|
+
Object.assign(env, extra);
|
|
80
|
+
}
|
|
81
|
+
return env;
|
|
82
|
+
}
|
|
28
83
|
/** Error carrying exit code/signal from a failed subprocess spawn. */
|
|
29
84
|
class SpawnExitError extends Error {
|
|
30
85
|
exitCode;
|
|
@@ -36,14 +91,6 @@ class SpawnExitError extends Error {
|
|
|
36
91
|
this.exitSignal = exitSignal ?? undefined;
|
|
37
92
|
}
|
|
38
93
|
}
|
|
39
|
-
class WorkflowCompletionError extends Error {
|
|
40
|
-
completionReason;
|
|
41
|
-
constructor(message, completionReason) {
|
|
42
|
-
super(message);
|
|
43
|
-
this.name = 'WorkflowCompletionError';
|
|
44
|
-
this.completionReason = completionReason;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
94
|
// ── CLI resolution ───────────────────────────────────────────────────────────
|
|
48
95
|
/**
|
|
49
96
|
* Resolve `cursor` to the concrete cursor agent binary available in PATH.
|
|
@@ -63,6 +110,8 @@ export class WorkflowRunner {
|
|
|
63
110
|
summaryDir;
|
|
64
111
|
executor;
|
|
65
112
|
envSecrets;
|
|
113
|
+
templateResolver;
|
|
114
|
+
channelMessenger;
|
|
66
115
|
/** @internal exposed for CLI signal-handler shutdown only */
|
|
67
116
|
relay;
|
|
68
117
|
relaycast;
|
|
@@ -125,6 +174,8 @@ export class WorkflowRunner {
|
|
|
125
174
|
this.workersPath = path.join(this.cwd, '.agent-relay', 'team', 'workers.json');
|
|
126
175
|
this.executor = options.executor;
|
|
127
176
|
this.envSecrets = options.envSecrets;
|
|
177
|
+
this.templateResolver = new TemplateResolver();
|
|
178
|
+
this.channelMessenger = new ChannelMessenger({ postFn: (text) => this.postToChannel(text) });
|
|
128
179
|
}
|
|
129
180
|
// ── Path resolution ─────────────────────────────────────────────────────
|
|
130
181
|
/** Expand environment variables like $HOME or $VAR in a path string. */
|
|
@@ -251,11 +302,10 @@ export class WorkflowRunner {
|
|
|
251
302
|
this.rememberStepSignalSender(stepName, 'worker', workerSender);
|
|
252
303
|
}
|
|
253
304
|
rememberStepSignalSender(stepName, participant, ...senders) {
|
|
254
|
-
const participants = this.stepSignalParticipants.get(stepName) ??
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
};
|
|
305
|
+
const participants = this.stepSignalParticipants.get(stepName) ?? {
|
|
306
|
+
ownerSenders: new Set(),
|
|
307
|
+
workerSenders: new Set(),
|
|
308
|
+
};
|
|
259
309
|
this.stepSignalParticipants.set(stepName, participants);
|
|
260
310
|
const target = participant === 'owner' ? participants.ownerSenders : participants.workerSenders;
|
|
261
311
|
for (const sender of senders) {
|
|
@@ -275,11 +325,7 @@ export class WorkflowRunner {
|
|
|
275
325
|
return undefined;
|
|
276
326
|
}
|
|
277
327
|
isSignalFromExpectedSender(stepName, signal) {
|
|
278
|
-
const expectedParticipant = signal.kind === 'worker_done'
|
|
279
|
-
? 'worker'
|
|
280
|
-
: signal.kind === 'lead_done'
|
|
281
|
-
? 'owner'
|
|
282
|
-
: undefined;
|
|
328
|
+
const expectedParticipant = signal.kind === 'worker_done' ? 'worker' : signal.kind === 'lead_done' ? 'owner' : undefined;
|
|
283
329
|
if (!expectedParticipant)
|
|
284
330
|
return true;
|
|
285
331
|
const participants = this.stepSignalParticipants.get(stepName);
|
|
@@ -499,7 +545,9 @@ export class WorkflowRunner {
|
|
|
499
545
|
return undefined;
|
|
500
546
|
}
|
|
501
547
|
uniqueEvidenceRoots(roots) {
|
|
502
|
-
return [
|
|
548
|
+
return [
|
|
549
|
+
...new Set(roots.filter((root) => Boolean(root)).map((root) => path.resolve(root))),
|
|
550
|
+
];
|
|
503
551
|
}
|
|
504
552
|
captureFileSnapshot(root) {
|
|
505
553
|
const snapshot = new Map();
|
|
@@ -611,9 +659,7 @@ export class WorkflowRunner {
|
|
|
611
659
|
const evidence = this.getStepCompletionEvidence(stepName);
|
|
612
660
|
if (!evidence)
|
|
613
661
|
return undefined;
|
|
614
|
-
const signals = evidence.coordinationSignals
|
|
615
|
-
.slice(-6)
|
|
616
|
-
.map((signal) => signal.value ?? signal.text);
|
|
662
|
+
const signals = evidence.coordinationSignals.slice(-6).map((signal) => signal.value ?? signal.text);
|
|
617
663
|
const channelPosts = evidence.channelPosts
|
|
618
664
|
.filter((post) => post.completionRelevant)
|
|
619
665
|
.slice(-3)
|
|
@@ -709,7 +755,7 @@ export class WorkflowRunner {
|
|
|
709
755
|
return this.relayOptions.env;
|
|
710
756
|
}
|
|
711
757
|
return {
|
|
712
|
-
...(this.relayOptions.env ??
|
|
758
|
+
...(this.relayOptions.env ?? filteredEnv()),
|
|
713
759
|
RELAY_API_KEY: this.relayApiKey,
|
|
714
760
|
};
|
|
715
761
|
}
|
|
@@ -1253,71 +1299,13 @@ export class WorkflowRunner {
|
|
|
1253
1299
|
// ── Template variable resolution ────────────────────────────────────────
|
|
1254
1300
|
/** Resolve {{variable}} placeholders in all task strings. */
|
|
1255
1301
|
resolveVariables(config, vars) {
|
|
1256
|
-
|
|
1257
|
-
for (const agent of resolved.agents) {
|
|
1258
|
-
if (agent.task) {
|
|
1259
|
-
agent.task = this.interpolate(agent.task, vars);
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
if (resolved.workflows) {
|
|
1263
|
-
for (const wf of resolved.workflows) {
|
|
1264
|
-
for (const step of wf.steps) {
|
|
1265
|
-
// Resolve variables in task (agent steps) and command (deterministic steps)
|
|
1266
|
-
if (step.task) {
|
|
1267
|
-
step.task = this.interpolate(step.task, vars);
|
|
1268
|
-
}
|
|
1269
|
-
if (step.command) {
|
|
1270
|
-
step.command = this.interpolate(step.command, vars);
|
|
1271
|
-
}
|
|
1272
|
-
// Resolve variables in integration step params
|
|
1273
|
-
if (step.params && typeof step.params === 'object') {
|
|
1274
|
-
for (const key of Object.keys(step.params)) {
|
|
1275
|
-
const val = step.params[key];
|
|
1276
|
-
if (typeof val === 'string') {
|
|
1277
|
-
step.params[key] = this.interpolate(val, vars);
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return resolved;
|
|
1302
|
+
return this.templateResolver.resolveVariables(config, vars);
|
|
1285
1303
|
}
|
|
1286
1304
|
interpolate(template, vars) {
|
|
1287
|
-
return template
|
|
1288
|
-
// Skip step-output placeholders — they are resolved at execution time by interpolateStepTask()
|
|
1289
|
-
if (key.startsWith('steps.')) {
|
|
1290
|
-
return _match;
|
|
1291
|
-
}
|
|
1292
|
-
// Resolve dot-path variables like steps.plan.output
|
|
1293
|
-
const value = this.resolveDotPath(key, vars);
|
|
1294
|
-
if (value === undefined) {
|
|
1295
|
-
throw new Error(`Unresolved variable: {{${key}}}`);
|
|
1296
|
-
}
|
|
1297
|
-
return String(value);
|
|
1298
|
-
});
|
|
1305
|
+
return resolveTemplate(template, vars);
|
|
1299
1306
|
}
|
|
1300
1307
|
resolveDotPath(key, vars) {
|
|
1301
|
-
|
|
1302
|
-
if (!key.includes('.')) {
|
|
1303
|
-
return vars[key];
|
|
1304
|
-
}
|
|
1305
|
-
// Dot-path — walk into nested context
|
|
1306
|
-
const parts = key.split('.');
|
|
1307
|
-
let current = vars;
|
|
1308
|
-
for (const part of parts) {
|
|
1309
|
-
if (current === null || current === undefined || typeof current !== 'object') {
|
|
1310
|
-
return undefined;
|
|
1311
|
-
}
|
|
1312
|
-
current = current[part];
|
|
1313
|
-
}
|
|
1314
|
-
if (current === undefined || current === null) {
|
|
1315
|
-
return undefined;
|
|
1316
|
-
}
|
|
1317
|
-
if (typeof current === 'string' || typeof current === 'number' || typeof current === 'boolean') {
|
|
1318
|
-
return current;
|
|
1319
|
-
}
|
|
1320
|
-
return String(current);
|
|
1308
|
+
return resolveTemplateDotPath(key, vars);
|
|
1321
1309
|
}
|
|
1322
1310
|
/** Build a nested context from completed step outputs for {{steps.X.output}} resolution. */
|
|
1323
1311
|
buildStepOutputContext(stepStates, runId) {
|
|
@@ -1339,14 +1327,84 @@ export class WorkflowRunner {
|
|
|
1339
1327
|
}
|
|
1340
1328
|
/** Interpolate step-output variables, silently skipping unresolved ones (they may be user vars). */
|
|
1341
1329
|
interpolateStepTask(template, context) {
|
|
1342
|
-
return template
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1330
|
+
return interpolateStepTaskTemplate(template, context);
|
|
1331
|
+
}
|
|
1332
|
+
createStepLifecycleExecutor(workflow, stepStates, agentMap, errorHandling, runId) {
|
|
1333
|
+
// eslint-disable-next-line prefer-const -- circular: deps closure captures lifecycle before assignment
|
|
1334
|
+
let lifecycle;
|
|
1335
|
+
const deps = {
|
|
1336
|
+
cwd: this.cwd,
|
|
1337
|
+
runId,
|
|
1338
|
+
templateResolver: this.templateResolver,
|
|
1339
|
+
channelMessenger: this.channelMessenger,
|
|
1340
|
+
verificationRunner: (check, output, stepName, injectedTaskText, options) => this.runVerification(check, output, stepName, injectedTaskText, options),
|
|
1341
|
+
postToChannel: (text) => this.postToChannel(text),
|
|
1342
|
+
persistStepRow: async (stepId, patch) => this.db.updateStep(stepId, patch),
|
|
1343
|
+
persistStepOutput: async (lifecycleRunId, stepName, output) => this.persistStepOutput(lifecycleRunId, stepName, output),
|
|
1344
|
+
loadStepOutput: (lifecycleRunId, stepName) => this.loadStepOutput(lifecycleRunId, stepName),
|
|
1345
|
+
checkAborted: () => this.checkAborted(),
|
|
1346
|
+
waitIfPaused: () => this.waitIfPaused(),
|
|
1347
|
+
log: (message) => this.log(message),
|
|
1348
|
+
onStepStarted: async (step) => {
|
|
1349
|
+
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
1350
|
+
},
|
|
1351
|
+
onStepCompleted: async (step, state, result) => {
|
|
1352
|
+
this.emit({
|
|
1353
|
+
type: 'step:completed',
|
|
1354
|
+
runId,
|
|
1355
|
+
stepName: step.name,
|
|
1356
|
+
output: result.output,
|
|
1357
|
+
exitCode: result.exitCode,
|
|
1358
|
+
exitSignal: result.exitSignal,
|
|
1359
|
+
});
|
|
1360
|
+
this.finalizeStepEvidence(step.name, result.status, state.row.completedAt, result.completionReason);
|
|
1361
|
+
},
|
|
1362
|
+
onStepFailed: async (step, state, result) => {
|
|
1363
|
+
this.captureStepTerminalEvidence(step.name, {}, {
|
|
1364
|
+
exitCode: result.exitCode,
|
|
1365
|
+
exitSignal: result.exitSignal,
|
|
1366
|
+
});
|
|
1367
|
+
this.emit({
|
|
1368
|
+
type: 'step:failed',
|
|
1369
|
+
runId,
|
|
1370
|
+
stepName: step.name,
|
|
1371
|
+
error: result.error ?? 'Unknown error',
|
|
1372
|
+
exitCode: result.exitCode,
|
|
1373
|
+
exitSignal: result.exitSignal,
|
|
1374
|
+
});
|
|
1375
|
+
this.finalizeStepEvidence(step.name, 'failed', state.row.completedAt, result.completionReason);
|
|
1376
|
+
},
|
|
1377
|
+
executeStep: async (step, state) => {
|
|
1378
|
+
await this.executeStep(step, state, stepStates, agentMap, errorHandling, runId, lifecycle);
|
|
1379
|
+
return {
|
|
1380
|
+
status: state.row.status,
|
|
1381
|
+
output: state.row.output ?? '',
|
|
1382
|
+
completionReason: state.row.completionReason,
|
|
1383
|
+
retries: state.row.retryCount,
|
|
1384
|
+
error: state.row.error,
|
|
1385
|
+
};
|
|
1386
|
+
},
|
|
1387
|
+
onBeginTrack: async (steps) => {
|
|
1388
|
+
if (steps.length > 1 && this.trajectory) {
|
|
1389
|
+
await this.trajectory.beginTrack(steps.map((step) => step.name).join(', '));
|
|
1390
|
+
}
|
|
1391
|
+
},
|
|
1392
|
+
onConverge: async (readySteps, batchOutcomes) => {
|
|
1393
|
+
if (readySteps.length <= 1 || !this.trajectory?.shouldReflectOnConverge()) {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const completedNames = new Set(batchOutcomes.filter((outcome) => outcome.status === 'completed').map((outcome) => outcome.name));
|
|
1397
|
+
const unblocked = workflow.steps
|
|
1398
|
+
.filter((step) => step.dependsOn?.some((dependency) => completedNames.has(dependency)))
|
|
1399
|
+
.filter((step) => stepStates.get(step.name)?.row.status === 'pending')
|
|
1400
|
+
.map((step) => step.name);
|
|
1401
|
+
await this.trajectory.synthesizeAndReflect(readySteps.map((step) => step.name).join(' + '), batchOutcomes, unblocked.length > 0 ? unblocked : undefined);
|
|
1402
|
+
},
|
|
1403
|
+
markDownstreamSkipped: async (failedStepName) => this.markDownstreamSkipped(failedStepName, workflow.steps, stepStates, runId),
|
|
1404
|
+
buildCompletionMode: (stepName, completionReason) => completionReason ? this.buildStepCompletionDecision(stepName, completionReason)?.mode : undefined,
|
|
1405
|
+
};
|
|
1406
|
+
lifecycle = new WorkflowStepLifecycleExecutor(deps);
|
|
1407
|
+
return lifecycle;
|
|
1350
1408
|
}
|
|
1351
1409
|
// ── Execution ───────────────────────────────────────────────────────────
|
|
1352
1410
|
/** Execute a named workflow from a validated config. */
|
|
@@ -1409,7 +1467,7 @@ export class WorkflowRunner {
|
|
|
1409
1467
|
: step.type === 'worktree'
|
|
1410
1468
|
? (step.branch ?? '')
|
|
1411
1469
|
: step.type === 'integration'
|
|
1412
|
-
?
|
|
1470
|
+
? `${step.integration}.${step.action}`
|
|
1413
1471
|
: (step.task ?? ''),
|
|
1414
1472
|
dependsOn: step.dependsOn ?? [],
|
|
1415
1473
|
retryCount: 0,
|
|
@@ -1429,8 +1487,7 @@ export class WorkflowRunner {
|
|
|
1429
1487
|
const transitiveDeps = this.collectTransitiveDeps(startFromName, resolvedWorkflow.steps);
|
|
1430
1488
|
const skippedCount = transitiveDeps.size;
|
|
1431
1489
|
// Determine which run ID to load cached outputs from
|
|
1432
|
-
const cacheRunId = executeOptions.previousRunId
|
|
1433
|
-
?? this.findMostRecentRunWithSteps(transitiveDeps);
|
|
1490
|
+
const cacheRunId = executeOptions.previousRunId ?? this.findMostRecentRunWithSteps(transitiveDeps);
|
|
1434
1491
|
for (const depName of transitiveDeps) {
|
|
1435
1492
|
const state = stepStates.get(depName);
|
|
1436
1493
|
if (!state)
|
|
@@ -1920,8 +1977,6 @@ export class WorkflowRunner {
|
|
|
1920
1977
|
// ── Step execution engine ─────────────────────────────────────────────
|
|
1921
1978
|
async executeSteps(workflow, stepStates, agentMap, errorHandling, runId) {
|
|
1922
1979
|
const rawStrategy = errorHandling?.strategy ?? workflow.onError ?? 'fail-fast';
|
|
1923
|
-
// Map shorthand onError values to canonical strategy names.
|
|
1924
|
-
// 'retry' maps to 'fail-fast' so downstream steps are properly skipped after retries exhaust.
|
|
1925
1980
|
const strategy = rawStrategy === 'fail'
|
|
1926
1981
|
? 'fail-fast'
|
|
1927
1982
|
: rawStrategy === 'skip'
|
|
@@ -1929,89 +1984,11 @@ export class WorkflowRunner {
|
|
|
1929
1984
|
: rawStrategy === 'retry'
|
|
1930
1985
|
? 'fail-fast'
|
|
1931
1986
|
: rawStrategy;
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
if (readySteps.length === 0) {
|
|
1938
|
-
// No steps ready — either all done or blocked
|
|
1939
|
-
break;
|
|
1940
|
-
}
|
|
1941
|
-
// Begin a track chapter if multiple parallel steps are starting
|
|
1942
|
-
if (readySteps.length > 1 && this.trajectory) {
|
|
1943
|
-
const trackNames = readySteps.map((s) => s.name).join(', ');
|
|
1944
|
-
await this.trajectory.beginTrack(trackNames);
|
|
1945
|
-
}
|
|
1946
|
-
// Stagger spawns when many steps are ready simultaneously.
|
|
1947
|
-
// All agents still run concurrently once spawned — this only delays when
|
|
1948
|
-
// each step's executeStep() begins, preventing Relaycast from receiving
|
|
1949
|
-
// N simultaneous registration requests which causes spawn timeouts.
|
|
1950
|
-
const STAGGER_THRESHOLD = 3;
|
|
1951
|
-
const STAGGER_DELAY_MS = 2_000;
|
|
1952
|
-
const results = await Promise.allSettled(readySteps.map((step, i) => {
|
|
1953
|
-
const delay = readySteps.length > STAGGER_THRESHOLD ? i * STAGGER_DELAY_MS : 0;
|
|
1954
|
-
if (delay === 0) {
|
|
1955
|
-
return this.executeStep(step, stepStates, agentMap, errorHandling, runId);
|
|
1956
|
-
}
|
|
1957
|
-
return new Promise((resolve) => setTimeout(resolve, delay)).then(() => this.executeStep(step, stepStates, agentMap, errorHandling, runId));
|
|
1958
|
-
}));
|
|
1959
|
-
// Collect outcomes from this batch for convergence reflection
|
|
1960
|
-
const batchOutcomes = [];
|
|
1961
|
-
for (let i = 0; i < results.length; i++) {
|
|
1962
|
-
const result = results[i];
|
|
1963
|
-
const step = readySteps[i];
|
|
1964
|
-
const state = stepStates.get(step.name);
|
|
1965
|
-
if (result.status === 'rejected') {
|
|
1966
|
-
const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
1967
|
-
if (state && state.row.status !== 'failed') {
|
|
1968
|
-
await this.markStepFailed(state, error, runId);
|
|
1969
|
-
}
|
|
1970
|
-
batchOutcomes.push({
|
|
1971
|
-
name: step.name,
|
|
1972
|
-
agent: step.agent ?? 'deterministic',
|
|
1973
|
-
status: 'failed',
|
|
1974
|
-
attempts: (state?.row.retryCount ?? 0) + 1,
|
|
1975
|
-
error,
|
|
1976
|
-
});
|
|
1977
|
-
if (strategy === 'fail-fast') {
|
|
1978
|
-
// Mark all pending downstream steps as skipped
|
|
1979
|
-
await this.markDownstreamSkipped(step.name, workflow.steps, stepStates, runId);
|
|
1980
|
-
throw new Error(`Step "${step.name}" failed: ${error}`);
|
|
1981
|
-
}
|
|
1982
|
-
if (strategy === 'continue') {
|
|
1983
|
-
await this.markDownstreamSkipped(step.name, workflow.steps, stepStates, runId);
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
else {
|
|
1987
|
-
batchOutcomes.push({
|
|
1988
|
-
name: step.name,
|
|
1989
|
-
agent: step.agent ?? 'deterministic',
|
|
1990
|
-
status: state?.row.status === 'completed' ? 'completed' : 'failed',
|
|
1991
|
-
attempts: (state?.row.retryCount ?? 0) + 1,
|
|
1992
|
-
output: state?.row.output,
|
|
1993
|
-
verificationPassed: state?.row.status === 'completed' && step.verification !== undefined,
|
|
1994
|
-
completionMode: state?.row.completionReason
|
|
1995
|
-
? this.buildStepCompletionDecision(step.name, state.row.completionReason)?.mode
|
|
1996
|
-
: undefined,
|
|
1997
|
-
});
|
|
1998
|
-
}
|
|
1999
|
-
}
|
|
2000
|
-
// Reflect at convergence when a parallel batch completes
|
|
2001
|
-
if (readySteps.length > 1 && this.trajectory?.shouldReflectOnConverge()) {
|
|
2002
|
-
const label = readySteps.map((s) => s.name).join(' + ');
|
|
2003
|
-
// Find steps that this batch unblocks
|
|
2004
|
-
const completedNames = new Set(batchOutcomes.filter((o) => o.status === 'completed').map((o) => o.name));
|
|
2005
|
-
const unblocked = workflow.steps
|
|
2006
|
-
.filter((s) => s.dependsOn?.some((dep) => completedNames.has(dep)))
|
|
2007
|
-
.filter((s) => {
|
|
2008
|
-
const st = stepStates.get(s.name);
|
|
2009
|
-
return st && st.row.status === 'pending';
|
|
2010
|
-
})
|
|
2011
|
-
.map((s) => s.name);
|
|
2012
|
-
await this.trajectory.synthesizeAndReflect(label, batchOutcomes, unblocked.length > 0 ? unblocked : undefined);
|
|
2013
|
-
}
|
|
2014
|
-
}
|
|
1987
|
+
const lifecycle = this.createStepLifecycleExecutor(workflow, stepStates, agentMap, errorHandling, runId);
|
|
1988
|
+
await lifecycle.executeAll(workflow.steps, agentMap, {
|
|
1989
|
+
...(errorHandling ?? { strategy: 'fail-fast' }),
|
|
1990
|
+
strategy,
|
|
1991
|
+
}, stepStates);
|
|
2015
1992
|
}
|
|
2016
1993
|
findReadySteps(steps, stepStates) {
|
|
2017
1994
|
return steps.filter((step) => {
|
|
@@ -2040,7 +2017,7 @@ export class WorkflowRunner {
|
|
|
2040
2017
|
const child = cpSpawn('sh', ['-c', check.command], {
|
|
2041
2018
|
stdio: 'pipe',
|
|
2042
2019
|
cwd: this.cwd,
|
|
2043
|
-
env:
|
|
2020
|
+
env: filteredEnv(),
|
|
2044
2021
|
});
|
|
2045
2022
|
const stdoutChunks = [];
|
|
2046
2023
|
const stderrChunks = [];
|
|
@@ -2135,18 +2112,18 @@ export class WorkflowRunner {
|
|
|
2135
2112
|
isIntegrationStep(step) {
|
|
2136
2113
|
return step.type === 'integration';
|
|
2137
2114
|
}
|
|
2138
|
-
async executeStep(step, stepStates, agentMap, errorHandling, runId) {
|
|
2115
|
+
async executeStep(step, state, stepStates, agentMap, errorHandling, runId, lifecycle) {
|
|
2139
2116
|
// Branch: deterministic steps execute shell commands
|
|
2140
2117
|
if (this.isDeterministicStep(step)) {
|
|
2141
|
-
return this.executeDeterministicStep(step, stepStates, runId, errorHandling);
|
|
2118
|
+
return this.executeDeterministicStep(step, state, stepStates, runId, errorHandling, lifecycle);
|
|
2142
2119
|
}
|
|
2143
2120
|
// Branch: worktree steps set up git worktrees
|
|
2144
2121
|
if (this.isWorktreeStep(step)) {
|
|
2145
|
-
return this.executeWorktreeStep(step, stepStates, runId);
|
|
2122
|
+
return this.executeWorktreeStep(step, state, stepStates, runId, lifecycle);
|
|
2146
2123
|
}
|
|
2147
2124
|
// Branch: integration steps interact with external services
|
|
2148
2125
|
if (this.isIntegrationStep(step)) {
|
|
2149
|
-
return this.executeIntegrationStep(step, stepStates, runId);
|
|
2126
|
+
return this.executeIntegrationStep(step, state, stepStates, runId, lifecycle);
|
|
2150
2127
|
}
|
|
2151
2128
|
// Agent step execution
|
|
2152
2129
|
return this.executeAgentStep(step, stepStates, agentMap, errorHandling, runId);
|
|
@@ -2155,92 +2132,56 @@ export class WorkflowRunner {
|
|
|
2155
2132
|
* Execute a deterministic step (shell command).
|
|
2156
2133
|
* Fast, reliable, $0 LLM cost.
|
|
2157
2134
|
*/
|
|
2158
|
-
async executeDeterministicStep(step, stepStates, runId, errorHandling) {
|
|
2159
|
-
const state = stepStates.get(step.name);
|
|
2160
|
-
if (!state)
|
|
2161
|
-
throw new Error(`Step state not found: ${step.name}`);
|
|
2135
|
+
async executeDeterministicStep(step, state, stepStates, runId, errorHandling, lifecycle) {
|
|
2162
2136
|
const maxRetries = step.retries ?? errorHandling?.maxRetries ?? 0;
|
|
2163
2137
|
const retryDelay = errorHandling?.retryDelayMs ?? 1000;
|
|
2164
|
-
let lastError;
|
|
2138
|
+
let lastError = 'Unknown error';
|
|
2165
2139
|
let lastCompletionReason;
|
|
2166
2140
|
let lastExitCode;
|
|
2167
2141
|
let lastExitSignal;
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2142
|
+
const result = await lifecycle.monitorStep(step, state, {
|
|
2143
|
+
maxRetries,
|
|
2144
|
+
retryDelayMs: retryDelay,
|
|
2145
|
+
startMessage: `**[${step.name}]** Started (deterministic)`,
|
|
2146
|
+
onRetry: async (attempt, total) => {
|
|
2173
2147
|
this.emit({ type: 'step:retrying', runId, stepName: step.name, attempt });
|
|
2174
|
-
this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${
|
|
2148
|
+
this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${total + 1})`);
|
|
2175
2149
|
this.recordStepToolSideEffect(step.name, {
|
|
2176
2150
|
type: 'retry',
|
|
2177
|
-
detail: `Retrying attempt ${attempt + 1}/${
|
|
2178
|
-
raw: { attempt, maxRetries },
|
|
2151
|
+
detail: `Retrying attempt ${attempt + 1}/${total + 1}`,
|
|
2152
|
+
raw: { attempt, maxRetries: total },
|
|
2179
2153
|
});
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2154
|
+
},
|
|
2155
|
+
execute: async () => {
|
|
2156
|
+
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2157
|
+
let resolvedCommand = this.interpolateStepTask(step.command ?? '', stepOutputContext);
|
|
2158
|
+
resolvedCommand = resolvedCommand.replace(/\{\{([\w][\w.\-]*)\}\}/g, (_match, key) => {
|
|
2159
|
+
if (key.startsWith('steps.'))
|
|
2160
|
+
return _match;
|
|
2161
|
+
const value = this.resolveDotPath(key, stepOutputContext);
|
|
2162
|
+
return value !== undefined ? String(value) : _match;
|
|
2184
2163
|
});
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
// Mark step as running
|
|
2188
|
-
state.row.status = 'running';
|
|
2189
|
-
state.row.error = undefined;
|
|
2190
|
-
state.row.completionReason = undefined;
|
|
2191
|
-
state.row.startedAt = new Date().toISOString();
|
|
2192
|
-
await this.db.updateStep(state.row.id, {
|
|
2193
|
-
status: 'running',
|
|
2194
|
-
error: undefined,
|
|
2195
|
-
completionReason: undefined,
|
|
2196
|
-
startedAt: state.row.startedAt,
|
|
2197
|
-
updatedAt: new Date().toISOString(),
|
|
2198
|
-
});
|
|
2199
|
-
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
2200
|
-
this.postToChannel(`**[${step.name}]** Started (deterministic)`);
|
|
2201
|
-
// Resolve variables in the command (e.g., {{steps.plan.output}}, {{branch-name}})
|
|
2202
|
-
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2203
|
-
let resolvedCommand = this.interpolateStepTask(step.command ?? '', stepOutputContext);
|
|
2204
|
-
// Also resolve simple {{variable}} placeholders (already resolved in top-level config but safe to re-run)
|
|
2205
|
-
resolvedCommand = resolvedCommand.replace(/\{\{([\w][\w.\-]*)\}\}/g, (_match, key) => {
|
|
2206
|
-
if (key.startsWith('steps.'))
|
|
2207
|
-
return _match; // Already handled above
|
|
2208
|
-
const value = this.resolveDotPath(key, stepOutputContext);
|
|
2209
|
-
return value !== undefined ? String(value) : _match;
|
|
2210
|
-
});
|
|
2211
|
-
// Resolve step workdir (named path reference) for deterministic steps
|
|
2212
|
-
const stepCwd = this.resolveEffectiveCwd(step);
|
|
2213
|
-
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2214
|
-
try {
|
|
2215
|
-
// Delegate to executor if present
|
|
2164
|
+
const stepCwd = this.resolveEffectiveCwd(step);
|
|
2165
|
+
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2216
2166
|
if (this.executor?.executeDeterministicStep) {
|
|
2217
|
-
const
|
|
2218
|
-
lastExitCode =
|
|
2167
|
+
const executorResult = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
|
|
2168
|
+
lastExitCode = executorResult.exitCode;
|
|
2169
|
+
lastExitSignal = undefined;
|
|
2219
2170
|
const failOnError = step.failOnError !== false;
|
|
2220
|
-
if (failOnError &&
|
|
2221
|
-
throw new Error(`Command failed with exit code ${
|
|
2171
|
+
if (failOnError && executorResult.exitCode !== 0) {
|
|
2172
|
+
throw new Error(`Command failed with exit code ${executorResult.exitCode}: ${executorResult.output.slice(0, 500)}`);
|
|
2222
2173
|
}
|
|
2223
|
-
const output = step.captureOutput !== false
|
|
2224
|
-
|
|
2174
|
+
const output = step.captureOutput !== false
|
|
2175
|
+
? executorResult.output
|
|
2176
|
+
: `Command completed (exit code ${executorResult.exitCode})`;
|
|
2177
|
+
this.captureStepTerminalEvidence(step.name, { stdout: executorResult.output, combined: executorResult.output }, { exitCode: executorResult.exitCode });
|
|
2225
2178
|
const verificationResult = step.verification
|
|
2226
2179
|
? this.runVerification(step.verification, output, step.name)
|
|
2227
2180
|
: undefined;
|
|
2228
|
-
|
|
2229
|
-
state.row.status = 'completed';
|
|
2230
|
-
state.row.output = output;
|
|
2231
|
-
state.row.completionReason = verificationResult?.completionReason;
|
|
2232
|
-
state.row.completedAt = new Date().toISOString();
|
|
2233
|
-
await this.db.updateStep(state.row.id, {
|
|
2234
|
-
status: 'completed',
|
|
2181
|
+
return {
|
|
2235
2182
|
output,
|
|
2236
2183
|
completionReason: verificationResult?.completionReason,
|
|
2237
|
-
|
|
2238
|
-
updatedAt: new Date().toISOString(),
|
|
2239
|
-
});
|
|
2240
|
-
await this.persistStepOutput(runId, step.name, output);
|
|
2241
|
-
this.emit({ type: 'step:completed', runId, stepName: step.name, output });
|
|
2242
|
-
this.finalizeStepEvidence(step.name, 'completed', state.row.completedAt, verificationResult?.completionReason);
|
|
2243
|
-
return;
|
|
2184
|
+
};
|
|
2244
2185
|
}
|
|
2245
2186
|
let commandStdout = '';
|
|
2246
2187
|
let commandStderr = '';
|
|
@@ -2248,11 +2189,10 @@ export class WorkflowRunner {
|
|
|
2248
2189
|
const child = cpSpawn('sh', ['-c', resolvedCommand], {
|
|
2249
2190
|
stdio: 'pipe',
|
|
2250
2191
|
cwd: stepCwd,
|
|
2251
|
-
env:
|
|
2192
|
+
env: filteredEnv(),
|
|
2252
2193
|
});
|
|
2253
2194
|
const stdoutChunks = [];
|
|
2254
2195
|
const stderrChunks = [];
|
|
2255
|
-
// Wire abort signal
|
|
2256
2196
|
const abortSignal = this.abortController?.signal;
|
|
2257
2197
|
let abortHandler;
|
|
2258
2198
|
if (abortSignal && !abortSignal.aborted) {
|
|
@@ -2262,7 +2202,6 @@ export class WorkflowRunner {
|
|
|
2262
2202
|
};
|
|
2263
2203
|
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
2264
2204
|
}
|
|
2265
|
-
// Handle timeout
|
|
2266
2205
|
let timedOut = false;
|
|
2267
2206
|
let timer;
|
|
2268
2207
|
if (step.timeoutMs) {
|
|
@@ -2298,7 +2237,6 @@ export class WorkflowRunner {
|
|
|
2298
2237
|
commandStderr = stderr;
|
|
2299
2238
|
lastExitCode = code ?? undefined;
|
|
2300
2239
|
lastExitSignal = signal ?? undefined;
|
|
2301
|
-
// Check exit code unless failOnError is explicitly false
|
|
2302
2240
|
const failOnError = step.failOnError !== false;
|
|
2303
2241
|
if (failOnError && code !== 0 && code !== null) {
|
|
2304
2242
|
reject(new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ''}`));
|
|
@@ -2323,282 +2261,233 @@ export class WorkflowRunner {
|
|
|
2323
2261
|
const verificationResult = step.verification
|
|
2324
2262
|
? this.runVerification(step.verification, output, step.name)
|
|
2325
2263
|
: undefined;
|
|
2326
|
-
|
|
2327
|
-
state.row.status = 'completed';
|
|
2328
|
-
state.row.output = output;
|
|
2329
|
-
state.row.completionReason = verificationResult?.completionReason;
|
|
2330
|
-
state.row.completedAt = new Date().toISOString();
|
|
2331
|
-
await this.db.updateStep(state.row.id, {
|
|
2332
|
-
status: 'completed',
|
|
2264
|
+
return {
|
|
2333
2265
|
output,
|
|
2334
2266
|
completionReason: verificationResult?.completionReason,
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
}
|
|
2267
|
+
};
|
|
2268
|
+
},
|
|
2269
|
+
toCompletionResult: ({ output, completionReason }, attempt) => ({
|
|
2270
|
+
status: 'completed',
|
|
2271
|
+
output,
|
|
2272
|
+
completionReason,
|
|
2273
|
+
retries: attempt,
|
|
2274
|
+
exitCode: lastExitCode,
|
|
2275
|
+
exitSignal: lastExitSignal,
|
|
2276
|
+
}),
|
|
2277
|
+
onAttemptFailed: async (error) => {
|
|
2278
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
2279
|
+
lastCompletionReason = error instanceof WorkflowCompletionError ? error.completionReason : undefined;
|
|
2280
|
+
},
|
|
2281
|
+
getFailureResult: () => ({
|
|
2282
|
+
status: 'failed',
|
|
2283
|
+
output: '',
|
|
2284
|
+
error: lastError,
|
|
2285
|
+
retries: state.row.retryCount,
|
|
2286
|
+
exitCode: lastExitCode,
|
|
2287
|
+
exitSignal: lastExitSignal,
|
|
2288
|
+
completionReason: lastCompletionReason,
|
|
2289
|
+
}),
|
|
2290
|
+
});
|
|
2291
|
+
if (result.status === 'failed') {
|
|
2292
|
+
this.postToChannel(`**[${step.name}]** Failed: ${result.error ?? 'Unknown error'}`);
|
|
2293
|
+
throw new Error(`Step "${step.name}" failed: ${result.error ?? 'Unknown error'}`);
|
|
2349
2294
|
}
|
|
2350
|
-
const errorMsg = lastError ?? 'Unknown error';
|
|
2351
|
-
this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
|
|
2352
|
-
await this.markStepFailed(state, errorMsg, runId, { exitCode: lastExitCode, exitSignal: lastExitSignal }, lastCompletionReason);
|
|
2353
|
-
throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
|
|
2354
2295
|
}
|
|
2355
2296
|
/**
|
|
2356
2297
|
* Execute a worktree step (git worktree setup).
|
|
2357
2298
|
* Fast, reliable, $0 LLM cost.
|
|
2358
2299
|
* Outputs the worktree path for downstream steps to use.
|
|
2359
2300
|
*/
|
|
2360
|
-
async executeWorktreeStep(step, stepStates, runId) {
|
|
2361
|
-
const state = stepStates.get(step.name);
|
|
2362
|
-
if (!state)
|
|
2363
|
-
throw new Error(`Step state not found: ${step.name}`);
|
|
2301
|
+
async executeWorktreeStep(step, state, stepStates, runId, lifecycle) {
|
|
2364
2302
|
let lastExitCode;
|
|
2365
2303
|
let lastExitSignal;
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
}
|
|
2399
|
-
try {
|
|
2400
|
-
// Build the git worktree command
|
|
2401
|
-
// If createBranch is true and branch doesn't exist, use -b flag
|
|
2402
|
-
const absoluteWorktreePath = path.resolve(stepCwd, worktreePath);
|
|
2403
|
-
// First, check if the branch already exists
|
|
2404
|
-
const checkBranchCmd = `git rev-parse --verify --quiet ${branch} 2>/dev/null`;
|
|
2405
|
-
let branchExists = false;
|
|
2406
|
-
await new Promise((resolve) => {
|
|
2407
|
-
const checkChild = cpSpawn('sh', ['-c', checkBranchCmd], {
|
|
2408
|
-
stdio: 'pipe',
|
|
2409
|
-
cwd: stepCwd,
|
|
2410
|
-
env: { ...process.env },
|
|
2411
|
-
});
|
|
2412
|
-
checkChild.on('close', (code) => {
|
|
2413
|
-
branchExists = code === 0;
|
|
2414
|
-
resolve();
|
|
2415
|
-
});
|
|
2416
|
-
checkChild.on('error', () => resolve());
|
|
2417
|
-
});
|
|
2418
|
-
// Build appropriate worktree add command
|
|
2419
|
-
let worktreeCmd;
|
|
2420
|
-
if (branchExists) {
|
|
2421
|
-
// Branch exists, just checkout into worktree
|
|
2422
|
-
worktreeCmd = `git worktree add "${absoluteWorktreePath}" ${branch}`;
|
|
2423
|
-
}
|
|
2424
|
-
else if (createBranch) {
|
|
2425
|
-
// Create new branch from baseBranch
|
|
2426
|
-
worktreeCmd = `git worktree add -b ${branch} "${absoluteWorktreePath}" ${baseBranch}`;
|
|
2427
|
-
}
|
|
2428
|
-
else {
|
|
2429
|
-
// Branch doesn't exist and we're not creating it
|
|
2430
|
-
const errorMsg = `Branch "${branch}" does not exist and createBranch is false`;
|
|
2431
|
-
await this.markStepFailed(state, errorMsg, runId);
|
|
2432
|
-
throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
|
|
2433
|
-
}
|
|
2434
|
-
let commandStdout = '';
|
|
2435
|
-
let commandStderr = '';
|
|
2436
|
-
let commandExitCode;
|
|
2437
|
-
let commandExitSignal;
|
|
2438
|
-
const output = await new Promise((resolve, reject) => {
|
|
2439
|
-
const child = cpSpawn('sh', ['-c', worktreeCmd], {
|
|
2440
|
-
stdio: 'pipe',
|
|
2441
|
-
cwd: stepCwd,
|
|
2442
|
-
env: { ...process.env },
|
|
2304
|
+
let worktreeBranch = '';
|
|
2305
|
+
let createdBranch = false;
|
|
2306
|
+
const result = await lifecycle.monitorStep(step, state, {
|
|
2307
|
+
startMessage: `**[${step.name}]** Started (worktree setup)`,
|
|
2308
|
+
execute: async () => {
|
|
2309
|
+
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2310
|
+
const branch = this.interpolateStepTask(step.branch ?? '', stepOutputContext);
|
|
2311
|
+
const baseBranch = step.baseBranch
|
|
2312
|
+
? this.interpolateStepTask(step.baseBranch, stepOutputContext)
|
|
2313
|
+
: 'HEAD';
|
|
2314
|
+
const worktreePath = step.path
|
|
2315
|
+
? this.interpolateStepTask(step.path, stepOutputContext)
|
|
2316
|
+
: path.join('.worktrees', step.name);
|
|
2317
|
+
const createBranch = step.createBranch !== false;
|
|
2318
|
+
const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
|
|
2319
|
+
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2320
|
+
if (!branch) {
|
|
2321
|
+
throw new Error('Worktree step missing required "branch" field');
|
|
2322
|
+
}
|
|
2323
|
+
const absoluteWorktreePath = path.resolve(stepCwd, worktreePath);
|
|
2324
|
+
let branchExists = false;
|
|
2325
|
+
await new Promise((resolve) => {
|
|
2326
|
+
const checkChild = cpSpawn('git', ['rev-parse', '--verify', '--quiet', branch], {
|
|
2327
|
+
stdio: 'pipe',
|
|
2328
|
+
cwd: stepCwd,
|
|
2329
|
+
env: filteredEnv(),
|
|
2330
|
+
});
|
|
2331
|
+
checkChild.on('close', (code) => {
|
|
2332
|
+
branchExists = code === 0;
|
|
2333
|
+
resolve();
|
|
2334
|
+
});
|
|
2335
|
+
checkChild.on('error', () => resolve());
|
|
2443
2336
|
});
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
const abortSignal = this.abortController?.signal;
|
|
2448
|
-
let abortHandler;
|
|
2449
|
-
if (abortSignal && !abortSignal.aborted) {
|
|
2450
|
-
abortHandler = () => {
|
|
2451
|
-
child.kill('SIGTERM');
|
|
2452
|
-
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
2453
|
-
};
|
|
2454
|
-
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
2337
|
+
let worktreeArgs;
|
|
2338
|
+
if (branchExists) {
|
|
2339
|
+
worktreeArgs = ['worktree', 'add', absoluteWorktreePath, branch];
|
|
2455
2340
|
}
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
let timer;
|
|
2459
|
-
if (step.timeoutMs) {
|
|
2460
|
-
timer = setTimeout(() => {
|
|
2461
|
-
timedOut = true;
|
|
2462
|
-
child.kill('SIGTERM');
|
|
2463
|
-
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
2464
|
-
}, step.timeoutMs);
|
|
2341
|
+
else if (createBranch) {
|
|
2342
|
+
worktreeArgs = ['worktree', 'add', '-b', branch, absoluteWorktreePath, baseBranch];
|
|
2465
2343
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
}
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
const stderr = stderrChunks.join('');
|
|
2488
|
-
commandStderr = stderr;
|
|
2489
|
-
commandExitCode = code ?? undefined;
|
|
2490
|
-
commandExitSignal = signal ?? undefined;
|
|
2491
|
-
lastExitCode = commandExitCode;
|
|
2492
|
-
lastExitSignal = commandExitSignal;
|
|
2493
|
-
if (code !== 0 && code !== null) {
|
|
2494
|
-
reject(new Error(`git worktree add failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ''}`));
|
|
2495
|
-
return;
|
|
2344
|
+
else {
|
|
2345
|
+
throw new Error(`Branch "${branch}" does not exist and createBranch is false`);
|
|
2346
|
+
}
|
|
2347
|
+
let commandStdout = '';
|
|
2348
|
+
let commandStderr = '';
|
|
2349
|
+
const output = await new Promise((resolve, reject) => {
|
|
2350
|
+
const child = cpSpawn('git', worktreeArgs, {
|
|
2351
|
+
stdio: 'pipe',
|
|
2352
|
+
cwd: stepCwd,
|
|
2353
|
+
env: filteredEnv(),
|
|
2354
|
+
});
|
|
2355
|
+
const stdoutChunks = [];
|
|
2356
|
+
const stderrChunks = [];
|
|
2357
|
+
const abortSignal = this.abortController?.signal;
|
|
2358
|
+
let abortHandler;
|
|
2359
|
+
if (abortSignal && !abortSignal.aborted) {
|
|
2360
|
+
abortHandler = () => {
|
|
2361
|
+
child.kill('SIGTERM');
|
|
2362
|
+
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
2363
|
+
};
|
|
2364
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
2496
2365
|
}
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2366
|
+
let timedOut = false;
|
|
2367
|
+
let timer;
|
|
2368
|
+
if (step.timeoutMs) {
|
|
2369
|
+
timer = setTimeout(() => {
|
|
2370
|
+
timedOut = true;
|
|
2371
|
+
child.kill('SIGTERM');
|
|
2372
|
+
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
2373
|
+
}, step.timeoutMs);
|
|
2505
2374
|
}
|
|
2506
|
-
|
|
2375
|
+
child.stdout?.on('data', (chunk) => {
|
|
2376
|
+
stdoutChunks.push(chunk.toString());
|
|
2377
|
+
});
|
|
2378
|
+
child.stderr?.on('data', (chunk) => {
|
|
2379
|
+
stderrChunks.push(chunk.toString());
|
|
2380
|
+
});
|
|
2381
|
+
child.on('close', (code, signal) => {
|
|
2382
|
+
if (timer)
|
|
2383
|
+
clearTimeout(timer);
|
|
2384
|
+
if (abortHandler && abortSignal) {
|
|
2385
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
2386
|
+
}
|
|
2387
|
+
if (abortSignal?.aborted) {
|
|
2388
|
+
reject(new Error(`Step "${step.name}" aborted`));
|
|
2389
|
+
return;
|
|
2390
|
+
}
|
|
2391
|
+
if (timedOut) {
|
|
2392
|
+
reject(new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`));
|
|
2393
|
+
return;
|
|
2394
|
+
}
|
|
2395
|
+
commandStdout = stdoutChunks.join('');
|
|
2396
|
+
commandStderr = stderrChunks.join('');
|
|
2397
|
+
lastExitCode = code ?? undefined;
|
|
2398
|
+
lastExitSignal = signal ?? undefined;
|
|
2399
|
+
if (code !== 0 && code !== null) {
|
|
2400
|
+
reject(new Error(`git worktree add failed with exit code ${code}${commandStderr ? `: ${commandStderr.slice(0, 500)}` : ''}`));
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
resolve(absoluteWorktreePath);
|
|
2404
|
+
});
|
|
2405
|
+
child.on('error', (err) => {
|
|
2406
|
+
if (timer)
|
|
2407
|
+
clearTimeout(timer);
|
|
2408
|
+
if (abortHandler && abortSignal) {
|
|
2409
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
2410
|
+
}
|
|
2411
|
+
reject(new Error(`Failed to execute git worktree command: ${err.message}`));
|
|
2412
|
+
});
|
|
2507
2413
|
});
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
await this.db.updateStep(state.row.id, {
|
|
2414
|
+
this.captureStepTerminalEvidence(step.name, {
|
|
2415
|
+
stdout: commandStdout || output,
|
|
2416
|
+
stderr: commandStderr,
|
|
2417
|
+
combined: [commandStdout || output, commandStderr].filter(Boolean).join('\n'),
|
|
2418
|
+
}, { exitCode: lastExitCode, exitSignal: lastExitSignal });
|
|
2419
|
+
worktreeBranch = branch;
|
|
2420
|
+
createdBranch = !branchExists && createBranch;
|
|
2421
|
+
return { output };
|
|
2422
|
+
},
|
|
2423
|
+
toCompletionResult: ({ output }, attempt) => ({
|
|
2519
2424
|
status: 'completed',
|
|
2520
2425
|
output,
|
|
2521
|
-
|
|
2522
|
-
updatedAt: new Date().toISOString(),
|
|
2523
|
-
});
|
|
2524
|
-
// Persist step output
|
|
2525
|
-
await this.persistStepOutput(runId, step.name, output);
|
|
2526
|
-
this.emit({ type: 'step:completed', runId, stepName: step.name, output });
|
|
2527
|
-
this.postToChannel(`**[${step.name}]** Worktree created at: ${output}\n Branch: ${branch}${!branchExists && createBranch ? ' (created)' : ''}`);
|
|
2528
|
-
this.recordStepToolSideEffect(step.name, {
|
|
2529
|
-
type: 'worktree_created',
|
|
2530
|
-
detail: `Worktree created at ${output}`,
|
|
2531
|
-
raw: { branch, createdBranch: !branchExists && createBranch },
|
|
2532
|
-
});
|
|
2533
|
-
this.finalizeStepEvidence(step.name, 'completed', state.row.completedAt);
|
|
2534
|
-
}
|
|
2535
|
-
catch (err) {
|
|
2536
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
2537
|
-
this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
|
|
2538
|
-
await this.markStepFailed(state, errorMsg, runId, {
|
|
2426
|
+
retries: attempt,
|
|
2539
2427
|
exitCode: lastExitCode,
|
|
2540
2428
|
exitSignal: lastExitSignal,
|
|
2541
|
-
})
|
|
2542
|
-
|
|
2429
|
+
}),
|
|
2430
|
+
getFailureResult: (error) => ({
|
|
2431
|
+
status: 'failed',
|
|
2432
|
+
output: '',
|
|
2433
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2434
|
+
retries: state.row.retryCount,
|
|
2435
|
+
exitCode: lastExitCode,
|
|
2436
|
+
exitSignal: lastExitSignal,
|
|
2437
|
+
}),
|
|
2438
|
+
});
|
|
2439
|
+
if (result.status === 'failed') {
|
|
2440
|
+
this.postToChannel(`**[${step.name}]** Failed: ${result.error ?? 'Unknown error'}`);
|
|
2441
|
+
throw new Error(`Step "${step.name}" failed: ${result.error ?? 'Unknown error'}`);
|
|
2543
2442
|
}
|
|
2443
|
+
this.postToChannel(`**[${step.name}]** Worktree created at: ${result.output}\n Branch: ${worktreeBranch}${createdBranch ? ' (created)' : ''}`);
|
|
2444
|
+
this.recordStepToolSideEffect(step.name, {
|
|
2445
|
+
type: 'worktree_created',
|
|
2446
|
+
detail: `Worktree created at ${result.output}`,
|
|
2447
|
+
raw: { branch: worktreeBranch, createdBranch },
|
|
2448
|
+
});
|
|
2544
2449
|
}
|
|
2545
2450
|
/**
|
|
2546
2451
|
* Execute an integration step (external service interaction via executor).
|
|
2547
2452
|
*/
|
|
2548
|
-
async executeIntegrationStep(step, stepStates, runId) {
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
for (const [key, value] of Object.entries(step.params ?? {})) {
|
|
2571
|
-
resolvedParams[key] = this.interpolateStepTask(value, stepOutputContext);
|
|
2572
|
-
}
|
|
2573
|
-
try {
|
|
2574
|
-
if (!this.executor?.executeIntegrationStep) {
|
|
2575
|
-
throw new Error(`Integration steps require a cloud executor. Step "${step.name}" cannot run locally. ` +
|
|
2576
|
-
`Use "cloud run" to execute workflows with integration steps.`);
|
|
2577
|
-
}
|
|
2578
|
-
const result = await this.executor.executeIntegrationStep(step, resolvedParams, { workspaceId: this.workspaceId });
|
|
2579
|
-
if (!result.success) {
|
|
2580
|
-
throw new Error(`Integration step "${step.name}" failed: ${result.output}`);
|
|
2581
|
-
}
|
|
2582
|
-
// Mark completed
|
|
2583
|
-
state.row.status = 'completed';
|
|
2584
|
-
state.row.output = result.output;
|
|
2585
|
-
state.row.completedAt = new Date().toISOString();
|
|
2586
|
-
await this.db.updateStep(state.row.id, {
|
|
2453
|
+
async executeIntegrationStep(step, state, stepStates, runId, lifecycle) {
|
|
2454
|
+
const result = await lifecycle.monitorStep(step, state, {
|
|
2455
|
+
startMessage: `**[${step.name}]** Started (integration: ${step.integration}.${step.action})`,
|
|
2456
|
+
execute: async () => {
|
|
2457
|
+
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2458
|
+
const resolvedParams = {};
|
|
2459
|
+
for (const [key, value] of Object.entries(step.params ?? {})) {
|
|
2460
|
+
resolvedParams[key] = this.interpolateStepTask(value, stepOutputContext);
|
|
2461
|
+
}
|
|
2462
|
+
if (!this.executor?.executeIntegrationStep) {
|
|
2463
|
+
throw new Error(`Integration steps require a cloud executor. Step "${step.name}" cannot run locally. ` +
|
|
2464
|
+
`Use "cloud run" to execute workflows with integration steps.`);
|
|
2465
|
+
}
|
|
2466
|
+
const integrationResult = await this.executor.executeIntegrationStep(step, resolvedParams, {
|
|
2467
|
+
workspaceId: this.workspaceId,
|
|
2468
|
+
});
|
|
2469
|
+
if (!integrationResult.success) {
|
|
2470
|
+
throw new Error(`Integration step "${step.name}" failed: ${integrationResult.output}`);
|
|
2471
|
+
}
|
|
2472
|
+
return { output: integrationResult.output };
|
|
2473
|
+
},
|
|
2474
|
+
toCompletionResult: ({ output }, attempt) => ({
|
|
2587
2475
|
status: 'completed',
|
|
2588
|
-
output
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
throw new Error(`Step "${step.name}" failed: ${
|
|
2476
|
+
output,
|
|
2477
|
+
retries: attempt,
|
|
2478
|
+
}),
|
|
2479
|
+
getFailureResult: (error) => ({
|
|
2480
|
+
status: 'failed',
|
|
2481
|
+
output: '',
|
|
2482
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2483
|
+
retries: state.row.retryCount,
|
|
2484
|
+
}),
|
|
2485
|
+
});
|
|
2486
|
+
if (result.status === 'failed') {
|
|
2487
|
+
this.postToChannel(`**[${step.name}]** Failed: ${result.error ?? 'Unknown error'}`);
|
|
2488
|
+
throw new Error(`Step "${step.name}" failed: ${result.error ?? 'Unknown error'}`);
|
|
2601
2489
|
}
|
|
2490
|
+
this.postToChannel(`**[${step.name}]** Completed (integration: ${step.integration}.${step.action})`);
|
|
2602
2491
|
}
|
|
2603
2492
|
/**
|
|
2604
2493
|
* Execute an agent step (LLM-powered).
|
|
@@ -2630,7 +2519,11 @@ export class WorkflowRunner {
|
|
|
2630
2519
|
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
2631
2520
|
this.postToChannel(`**[${step.name}]** Started (api)`);
|
|
2632
2521
|
try {
|
|
2633
|
-
const output = await executeApiStep(specialistDef.constraints?.model ?? 'claude-sonnet-4-20250514', resolvedTask, {
|
|
2522
|
+
const output = await executeApiStep(specialistDef.constraints?.model ?? 'claude-sonnet-4-20250514', resolvedTask, {
|
|
2523
|
+
envSecrets: this.envSecrets,
|
|
2524
|
+
skills: specialistDef.skills,
|
|
2525
|
+
defaultMaxTokens: specialistDef.constraints?.maxTokens,
|
|
2526
|
+
});
|
|
2634
2527
|
state.row.status = 'completed';
|
|
2635
2528
|
state.row.output = output;
|
|
2636
2529
|
state.row.completedAt = new Date().toISOString();
|
|
@@ -2819,7 +2712,7 @@ export class WorkflowRunner {
|
|
|
2819
2712
|
: await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs, {
|
|
2820
2713
|
evidenceStepName: step.name,
|
|
2821
2714
|
evidenceRole: usesOwnerFlow ? 'owner' : 'specialist',
|
|
2822
|
-
preserveOnIdle:
|
|
2715
|
+
preserveOnIdle: !isHubPattern || !this.isLeadLikeAgent(effectiveOwner) ? false : undefined,
|
|
2823
2716
|
logicalName: effectiveOwner.name,
|
|
2824
2717
|
onSpawned: explicitInteractiveWorker
|
|
2825
2718
|
? ({ agent }) => {
|
|
@@ -2843,7 +2736,7 @@ export class WorkflowRunner {
|
|
|
2843
2736
|
? effectiveOwner.interactive === false
|
|
2844
2737
|
? undefined
|
|
2845
2738
|
: ownerTask
|
|
2846
|
-
: spawnResult.promptTaskText ?? ownerTask;
|
|
2739
|
+
: (spawnResult.promptTaskText ?? ownerTask);
|
|
2847
2740
|
lastExitCode = typeof spawnResult === 'string' ? undefined : spawnResult.exitCode;
|
|
2848
2741
|
lastExitSignal = typeof spawnResult === 'string' ? undefined : spawnResult.exitSignal;
|
|
2849
2742
|
ownerElapsed = Date.now() - ownerStartTime;
|
|
@@ -2931,15 +2824,21 @@ export class WorkflowRunner {
|
|
|
2931
2824
|
});
|
|
2932
2825
|
// Persist step output to disk so it survives restarts and is inspectable
|
|
2933
2826
|
await this.persistStepOutput(runId, step.name, combinedOutput);
|
|
2934
|
-
this.emit({
|
|
2827
|
+
this.emit({
|
|
2828
|
+
type: 'step:completed',
|
|
2829
|
+
runId,
|
|
2830
|
+
stepName: step.name,
|
|
2831
|
+
output: combinedOutput,
|
|
2832
|
+
exitCode: lastExitCode,
|
|
2833
|
+
exitSignal: lastExitSignal,
|
|
2834
|
+
});
|
|
2935
2835
|
this.finalizeStepEvidence(step.name, 'completed', state.row.completedAt, completionReason);
|
|
2936
2836
|
await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
|
|
2937
2837
|
return;
|
|
2938
2838
|
}
|
|
2939
2839
|
catch (err) {
|
|
2940
2840
|
lastError = err instanceof Error ? err.message : String(err);
|
|
2941
|
-
lastCompletionReason =
|
|
2942
|
-
err instanceof WorkflowCompletionError ? err.completionReason : undefined;
|
|
2841
|
+
lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : undefined;
|
|
2943
2842
|
if (lastCompletionReason === 'retry_requested_by_owner' && attempt >= maxRetries) {
|
|
2944
2843
|
lastError = this.buildOwnerRetryBudgetExceededMessage(step.name, maxRetries, lastError);
|
|
2945
2844
|
}
|
|
@@ -2979,9 +2878,7 @@ export class WorkflowRunner {
|
|
|
2979
2878
|
const normalizedDecision = ownerDecisionError?.startsWith(prefix)
|
|
2980
2879
|
? ownerDecisionError.slice(prefix.length).trim()
|
|
2981
2880
|
: ownerDecisionError?.trim();
|
|
2982
|
-
const decisionSuffix = normalizedDecision
|
|
2983
|
-
? ` Latest owner decision: ${normalizedDecision}`
|
|
2984
|
-
: '';
|
|
2881
|
+
const decisionSuffix = normalizedDecision ? ` Latest owner decision: ${normalizedDecision}` : '';
|
|
2985
2882
|
if (maxRetries === 0) {
|
|
2986
2883
|
return (`Step "${stepName}" owner requested another attempt, but no retries are configured ` +
|
|
2987
2884
|
`(maxRetries=0). Configure retries > 0 to allow OWNER_DECISION: INCOMPLETE_RETRY.` +
|
|
@@ -3402,7 +3299,7 @@ export class WorkflowRunner {
|
|
|
3402
3299
|
}
|
|
3403
3300
|
hasOwnerCompletionMarker(step, output, injectedTaskText) {
|
|
3404
3301
|
const marker = `STEP_COMPLETE:${step.name}`;
|
|
3405
|
-
const strippedOutput =
|
|
3302
|
+
const strippedOutput = stripInjectedTaskEcho(output, injectedTaskText);
|
|
3406
3303
|
if (strippedOutput.includes(marker)) {
|
|
3407
3304
|
return true;
|
|
3408
3305
|
}
|
|
@@ -3478,28 +3375,11 @@ export class WorkflowRunner {
|
|
|
3478
3375
|
.filter((line) => patterns.every((pattern) => !pattern.test(line)))
|
|
3479
3376
|
.join('\n');
|
|
3480
3377
|
}
|
|
3481
|
-
stripInjectedTaskEcho(output, injectedTaskText) {
|
|
3482
|
-
if (!injectedTaskText) {
|
|
3483
|
-
return output;
|
|
3484
|
-
}
|
|
3485
|
-
const candidates = [
|
|
3486
|
-
injectedTaskText,
|
|
3487
|
-
injectedTaskText.replace(/\r\n/g, '\n'),
|
|
3488
|
-
injectedTaskText.replace(/\n/g, '\r\n'),
|
|
3489
|
-
].filter((candidate, index, all) => candidate.length > 0 && all.indexOf(candidate) === index);
|
|
3490
|
-
for (const candidate of candidates) {
|
|
3491
|
-
const start = output.indexOf(candidate);
|
|
3492
|
-
if (start !== -1) {
|
|
3493
|
-
return output.slice(0, start) + output.slice(start + candidate.length);
|
|
3494
|
-
}
|
|
3495
|
-
}
|
|
3496
|
-
return output;
|
|
3497
|
-
}
|
|
3498
3378
|
outputContainsVerificationToken(output, token, injectedTaskText) {
|
|
3499
3379
|
if (!token) {
|
|
3500
3380
|
return false;
|
|
3501
3381
|
}
|
|
3502
|
-
return
|
|
3382
|
+
return stripInjectedTaskEcho(output, injectedTaskText).includes(token);
|
|
3503
3383
|
}
|
|
3504
3384
|
prepareInteractiveSpawnTask(agentName, taskText) {
|
|
3505
3385
|
if (Buffer.byteLength(taskText, 'utf8') <= WorkflowRunner.PTY_TASK_ARG_SIZE_LIMIT) {
|
|
@@ -3572,7 +3452,8 @@ export class WorkflowRunner {
|
|
|
3572
3452
|
if (gracePeriodMs === 0)
|
|
3573
3453
|
return null;
|
|
3574
3454
|
// Never infer completion when the owner explicitly requested retry/fail/clarification.
|
|
3575
|
-
if (ownerOutput &&
|
|
3455
|
+
if (ownerOutput &&
|
|
3456
|
+
/OWNER_DECISION:\s*(?:INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/i.test(ownerOutput)) {
|
|
3576
3457
|
return null;
|
|
3577
3458
|
}
|
|
3578
3459
|
const evidence = this.getStepCompletionEvidence(step.name);
|
|
@@ -3860,17 +3741,10 @@ export class WorkflowRunner {
|
|
|
3860
3741
|
* Delegates to the consolidated CLI registry for per-CLI arg formats.
|
|
3861
3742
|
*/
|
|
3862
3743
|
static buildNonInteractiveCommand(cli, task, extraArgs = []) {
|
|
3863
|
-
|
|
3864
|
-
throw new Error('cli "api" uses direct API calls, not a subprocess command');
|
|
3865
|
-
}
|
|
3866
|
-
const resolvedCli = cli === 'cursor' ? resolveCursorCli() : cli;
|
|
3867
|
-
const def = getCliDefinition(resolvedCli);
|
|
3868
|
-
if (!def || def.binaries.length === 0) {
|
|
3869
|
-
throw new Error(`Unknown or non-executable CLI: ${resolvedCli}`);
|
|
3870
|
-
}
|
|
3744
|
+
const [cmd, ...args] = buildProcessCommand(cli, extraArgs, task);
|
|
3871
3745
|
return {
|
|
3872
|
-
cmd
|
|
3873
|
-
args
|
|
3746
|
+
cmd,
|
|
3747
|
+
args,
|
|
3874
3748
|
};
|
|
3875
3749
|
}
|
|
3876
3750
|
/**
|
|
@@ -3966,11 +3840,11 @@ export class WorkflowRunner {
|
|
|
3966
3840
|
const stdoutChunks = [];
|
|
3967
3841
|
const stderrChunks = [];
|
|
3968
3842
|
try {
|
|
3969
|
-
const { stdout: output, exitCode, exitSignal } = await new Promise((resolve, reject) => {
|
|
3970
|
-
const child =
|
|
3843
|
+
const { stdout: output, exitCode, exitSignal, } = await new Promise((resolve, reject) => {
|
|
3844
|
+
const child = spawnProcess([cmd, ...args], {
|
|
3971
3845
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3972
3846
|
cwd: this.resolveEffectiveCwd(step, agentDef),
|
|
3973
|
-
env: this.getRelayEnv() ??
|
|
3847
|
+
env: this.getRelayEnv() ?? filteredEnv(),
|
|
3974
3848
|
});
|
|
3975
3849
|
// Update workers.json with PID now that we have it
|
|
3976
3850
|
this.registerWorker(agentName, agentDef.cli, step.task ?? '', child.pid, false);
|
|
@@ -4244,12 +4118,12 @@ export class WorkflowRunner {
|
|
|
4244
4118
|
if (verificationResult.passed) {
|
|
4245
4119
|
this.log(`[${step.name}] Agent timed out but verification passed — treating as complete`);
|
|
4246
4120
|
this.postToChannel(`**[${step.name}]** Agent idle after completing work — verification passed, releasing`);
|
|
4247
|
-
await agent.release();
|
|
4121
|
+
await agent.release().catch(() => undefined);
|
|
4248
4122
|
timeoutRecovered = true;
|
|
4249
4123
|
}
|
|
4250
4124
|
}
|
|
4251
4125
|
if (!timeoutRecovered) {
|
|
4252
|
-
await agent.release();
|
|
4126
|
+
await agent.release().catch(() => undefined);
|
|
4253
4127
|
throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? 'unknown'}ms`);
|
|
4254
4128
|
}
|
|
4255
4129
|
}
|
|
@@ -4346,8 +4220,7 @@ export class WorkflowRunner {
|
|
|
4346
4220
|
return true;
|
|
4347
4221
|
const role = (roleOverride ?? agentDef.role ?? '').toLowerCase();
|
|
4348
4222
|
const nameLC = agentDef.name.toLowerCase();
|
|
4349
|
-
return [...WorkflowRunner.HUB_ROLES].some((hubRole) => new RegExp(`\\b${hubRole}\\b`, 'i').test(nameLC) ||
|
|
4350
|
-
new RegExp(`\\b${hubRole}\\b`, 'i').test(role));
|
|
4223
|
+
return [...WorkflowRunner.HUB_ROLES].some((hubRole) => new RegExp(`\\b${hubRole}\\b`, 'i').test(nameLC) || new RegExp(`\\b${hubRole}\\b`, 'i').test(role));
|
|
4351
4224
|
}
|
|
4352
4225
|
shouldPreserveIdleSupervisor(agentDef, step, evidenceRole) {
|
|
4353
4226
|
if (evidenceRole && /\bowner\b/i.test(evidenceRole)) {
|
|
@@ -4411,13 +4284,13 @@ export class WorkflowRunner {
|
|
|
4411
4284
|
// Release and let upstream executeAgentStep handle verification.
|
|
4412
4285
|
this.log(`[${step.name}] Agent "${agent.name}" still idle after ${idleGraceSecs}s grace — releasing`);
|
|
4413
4286
|
this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle — releasing (verification pending)`);
|
|
4414
|
-
await agent.release();
|
|
4287
|
+
await agent.release().catch(() => undefined);
|
|
4415
4288
|
return 'released';
|
|
4416
4289
|
}
|
|
4417
4290
|
}
|
|
4418
4291
|
this.log(`[${step.name}] Agent "${agent.name}" went idle — treating as complete`);
|
|
4419
4292
|
this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle — treating as complete`);
|
|
4420
|
-
await agent.release();
|
|
4293
|
+
await agent.release().catch(() => undefined);
|
|
4421
4294
|
return 'released';
|
|
4422
4295
|
}
|
|
4423
4296
|
// Exit won the race, or idle returned 'exited'/'timeout' — pass through.
|
|
@@ -4474,7 +4347,7 @@ export class WorkflowRunner {
|
|
|
4474
4347
|
// Exhausted nudges — force-release
|
|
4475
4348
|
this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` still idle after ${nudgeCount} nudge(s) — force-releasing`);
|
|
4476
4349
|
this.emit({ type: 'step:force-released', runId: this.currentRunId ?? '', stepName: step.name });
|
|
4477
|
-
await agent.release();
|
|
4350
|
+
await agent.release().catch(() => undefined);
|
|
4478
4351
|
return 'force-released';
|
|
4479
4352
|
}
|
|
4480
4353
|
}
|
|
@@ -4541,74 +4414,11 @@ export class WorkflowRunner {
|
|
|
4541
4414
|
}
|
|
4542
4415
|
// ── Verification ────────────────────────────────────────────────────────
|
|
4543
4416
|
runVerification(check, output, stepName, injectedTaskText, options) {
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
this.
|
|
4547
|
-
|
|
4548
|
-
detail: message,
|
|
4549
|
-
observedAt,
|
|
4550
|
-
raw: { passed: false, type: check.type, value: check.value },
|
|
4551
|
-
});
|
|
4552
|
-
this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
|
|
4553
|
-
kind: 'verification_failed',
|
|
4554
|
-
source: 'verification',
|
|
4555
|
-
text: message,
|
|
4556
|
-
observedAt,
|
|
4557
|
-
value: check.value,
|
|
4558
|
-
});
|
|
4559
|
-
if (options?.allowFailure) {
|
|
4560
|
-
return {
|
|
4561
|
-
passed: false,
|
|
4562
|
-
completionReason: 'failed_verification',
|
|
4563
|
-
error: message,
|
|
4564
|
-
};
|
|
4565
|
-
}
|
|
4566
|
-
throw new WorkflowCompletionError(message, 'failed_verification');
|
|
4567
|
-
};
|
|
4568
|
-
switch (check.type) {
|
|
4569
|
-
case 'output_contains': {
|
|
4570
|
-
const token = check.value;
|
|
4571
|
-
if (!this.outputContainsVerificationToken(output, token, injectedTaskText)) {
|
|
4572
|
-
return fail(`Verification failed for "${stepName}": output does not contain "${token}"`);
|
|
4573
|
-
}
|
|
4574
|
-
break;
|
|
4575
|
-
}
|
|
4576
|
-
case 'exit_code':
|
|
4577
|
-
// exit_code verification is implicitly satisfied if the agent exited successfully
|
|
4578
|
-
break;
|
|
4579
|
-
case 'file_exists':
|
|
4580
|
-
if (!existsSync(path.resolve(this.cwd, check.value))) {
|
|
4581
|
-
return fail(`Verification failed for "${stepName}": file "${check.value}" does not exist`);
|
|
4582
|
-
}
|
|
4583
|
-
break;
|
|
4584
|
-
case 'custom':
|
|
4585
|
-
// Custom verifications are evaluated by callers; no-op here
|
|
4586
|
-
return { passed: false };
|
|
4587
|
-
}
|
|
4588
|
-
if (options?.completionMarkerFound === false) {
|
|
4589
|
-
this.log(`[${stepName}] Verification passed without legacy STEP_COMPLETE marker; allowing completion`);
|
|
4590
|
-
}
|
|
4591
|
-
const successMessage = options?.completionMarkerFound === false
|
|
4592
|
-
? `Verification passed without legacy STEP_COMPLETE marker`
|
|
4593
|
-
: `Verification passed`;
|
|
4594
|
-
const observedAt = new Date().toISOString();
|
|
4595
|
-
this.recordStepToolSideEffect(stepName, {
|
|
4596
|
-
type: 'verification_observed',
|
|
4597
|
-
detail: successMessage,
|
|
4598
|
-
observedAt,
|
|
4599
|
-
raw: { passed: true, type: check.type, value: check.value },
|
|
4600
|
-
});
|
|
4601
|
-
this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
|
|
4602
|
-
kind: 'verification_passed',
|
|
4603
|
-
source: 'verification',
|
|
4604
|
-
text: successMessage,
|
|
4605
|
-
observedAt,
|
|
4606
|
-
value: check.value,
|
|
4417
|
+
return runVerification(check, output, stepName, injectedTaskText, { ...options, cwd: this.cwd }, {
|
|
4418
|
+
recordStepToolSideEffect: (name, effect) => this.recordStepToolSideEffect(name, effect),
|
|
4419
|
+
getOrCreateStepEvidenceRecord: (name) => this.getOrCreateStepEvidenceRecord(name),
|
|
4420
|
+
log: (message) => this.log(message),
|
|
4607
4421
|
});
|
|
4608
|
-
return {
|
|
4609
|
-
passed: true,
|
|
4610
|
-
completionReason: 'completed_verified',
|
|
4611
|
-
};
|
|
4612
4422
|
}
|
|
4613
4423
|
// ── State helpers ─────────────────────────────────────────────────────
|
|
4614
4424
|
async updateRunStatus(runId, status, error) {
|
|
@@ -4743,29 +4553,7 @@ export class WorkflowRunner {
|
|
|
4743
4553
|
* Returns undefined if there are no non-interactive agents.
|
|
4744
4554
|
*/
|
|
4745
4555
|
buildNonInteractiveAwareness(agentMap, stepStates) {
|
|
4746
|
-
|
|
4747
|
-
if (nonInteractive.length === 0)
|
|
4748
|
-
return undefined;
|
|
4749
|
-
// Map agent names to their step names so the lead knows exact {{steps.X.output}} references
|
|
4750
|
-
const agentToSteps = new Map();
|
|
4751
|
-
for (const [stepName, state] of stepStates) {
|
|
4752
|
-
const agentName = state.row.agentName;
|
|
4753
|
-
if (!agentName)
|
|
4754
|
-
continue; // Skip deterministic steps
|
|
4755
|
-
if (!agentToSteps.has(agentName))
|
|
4756
|
-
agentToSteps.set(agentName, []);
|
|
4757
|
-
agentToSteps.get(agentName).push(stepName);
|
|
4758
|
-
}
|
|
4759
|
-
const lines = nonInteractive.map((a) => {
|
|
4760
|
-
const steps = agentToSteps.get(a.name) ?? [];
|
|
4761
|
-
const stepRefs = steps.map((s) => `{{steps.${s}.output}}`).join(', ');
|
|
4762
|
-
return `- ${a.name} (${a.cli}) — will return output when complete${stepRefs ? `. Access via: ${stepRefs}` : ''}`;
|
|
4763
|
-
});
|
|
4764
|
-
return ('\n\n---\n' +
|
|
4765
|
-
'Note: The following agents are non-interactive workers and cannot receive messages:\n' +
|
|
4766
|
-
lines.join('\n') +
|
|
4767
|
-
'\n' +
|
|
4768
|
-
'Do NOT attempt to message these agents. Use the {{steps.<name>.output}} references above to access their results.');
|
|
4556
|
+
return this.channelMessenger.buildNonInteractiveAwareness(agentMap, stepStates);
|
|
4769
4557
|
}
|
|
4770
4558
|
/**
|
|
4771
4559
|
* Build guidance that encourages agents to autonomously delegate subtasks
|
|
@@ -4780,46 +4568,10 @@ export class WorkflowRunner {
|
|
|
4780
4568
|
* key, but they won't call `register` unless explicitly told to.
|
|
4781
4569
|
*/
|
|
4782
4570
|
buildRelayRegistrationNote(cli, agentName) {
|
|
4783
|
-
|
|
4784
|
-
return '';
|
|
4785
|
-
return ('---\n' +
|
|
4786
|
-
'RELAY SETUP — do this FIRST before any other relay tool:\n' +
|
|
4787
|
-
`1. Call: register(name="${agentName}")\n` +
|
|
4788
|
-
' This authenticates you in the Relaycast workspace.\n' +
|
|
4789
|
-
' ALL relay tools (mcp__relaycast__message_dm_send, mcp__relaycast__message_inbox_check, mcp__relaycast__message_post, etc.) require\n' +
|
|
4790
|
-
' registration first — they will fail with "Not registered" otherwise.\n' +
|
|
4791
|
-
`2. Your agent name is "${agentName}" — use this exact name when registering.`);
|
|
4571
|
+
return this.channelMessenger.buildRelayRegistrationNote(cli, agentName);
|
|
4792
4572
|
}
|
|
4793
4573
|
buildDelegationGuidance(cli, timeoutMs) {
|
|
4794
|
-
|
|
4795
|
-
? `You have approximately ${Math.round(timeoutMs / 60000)} minutes before this step times out. ` +
|
|
4796
|
-
'Plan accordingly — delegate early if the work is substantial.\n\n'
|
|
4797
|
-
: '';
|
|
4798
|
-
// Option 2 (sub-agents via Task tool) is only available in Claude
|
|
4799
|
-
const subAgentOption = cli === 'claude'
|
|
4800
|
-
? 'Option 2 — Use built-in sub-agents (Task tool) for research or scoped work:\n' +
|
|
4801
|
-
' - Good for exploring code, reading files, or making targeted changes\n' +
|
|
4802
|
-
' - Can run multiple sub-agents in parallel\n\n'
|
|
4803
|
-
: '';
|
|
4804
|
-
return ('---\n' +
|
|
4805
|
-
'AUTONOMOUS DELEGATION — READ THIS BEFORE STARTING:\n' +
|
|
4806
|
-
timeoutNote +
|
|
4807
|
-
'Before diving in, assess whether this task is too large or complex for a single agent. ' +
|
|
4808
|
-
'If it involves multiple independent subtasks, touches many files, or could take a long time, ' +
|
|
4809
|
-
'you should break it down and delegate to helper agents to avoid timeouts.\n\n' +
|
|
4810
|
-
'Option 1 — Spawn relay agents (for real parallel coding work):\n' +
|
|
4811
|
-
' - mcp__relaycast__agent_add(name="helper-1", cli="claude", task="Specific subtask description")\n' +
|
|
4812
|
-
' - Coordinate via mcp__relaycast__message_dm_send(to="helper-1", text="...")\n' +
|
|
4813
|
-
' - Check on them with mcp__relaycast__message_inbox_check()\n' +
|
|
4814
|
-
' - Clean up when done: mcp__relaycast__agent_remove(name="helper-1")\n\n' +
|
|
4815
|
-
subAgentOption +
|
|
4816
|
-
'Guidelines:\n' +
|
|
4817
|
-
'- You are the lead — delegate but stay in control, track progress, integrate results\n' +
|
|
4818
|
-
'- Give each helper a clear, self-contained task with enough context to work independently\n' +
|
|
4819
|
-
"- For simple or quick work, just do it yourself — don't over-delegate\n" +
|
|
4820
|
-
'- Always release spawned relay agents when their work is complete\n' +
|
|
4821
|
-
'- When spawning non-claude agents (codex, gemini, etc.), prepend to their task:\n' +
|
|
4822
|
-
' "RELAY SETUP: First call register(name=\'<exact-agent-name>\') before any other relay tool."');
|
|
4574
|
+
return this.channelMessenger.buildDelegationGuidance(cli, timeoutMs);
|
|
4823
4575
|
}
|
|
4824
4576
|
/** Post a message to the workflow channel. Fire-and-forget — never throws or blocks. */
|
|
4825
4577
|
postToChannel(text, options = {}) {
|
|
@@ -4847,43 +4599,11 @@ export class WorkflowRunner {
|
|
|
4847
4599
|
}
|
|
4848
4600
|
/** Post a rich completion report to the channel. */
|
|
4849
4601
|
postCompletionReport(workflowName, outcomes, summary, confidence) {
|
|
4850
|
-
|
|
4851
|
-
const skipped = outcomes.filter((o) => o.status === 'skipped');
|
|
4852
|
-
const retried = outcomes.filter((o) => o.attempts > 1);
|
|
4853
|
-
const lines = [
|
|
4854
|
-
`## Workflow **${workflowName}** — Complete`,
|
|
4855
|
-
'',
|
|
4856
|
-
summary,
|
|
4857
|
-
`Confidence: ${Math.round(confidence * 100)}%`,
|
|
4858
|
-
'',
|
|
4859
|
-
'### Steps',
|
|
4860
|
-
...completed.map((o) => `- **${o.name}** (${o.agent}) — passed${o.verificationPassed ? ' (verified)' : ''}${o.attempts > 1 ? ` after ${o.attempts} attempts` : ''}`),
|
|
4861
|
-
...skipped.map((o) => `- **${o.name}** — skipped`),
|
|
4862
|
-
];
|
|
4863
|
-
if (retried.length > 0) {
|
|
4864
|
-
lines.push('', '### Retries');
|
|
4865
|
-
for (const o of retried) {
|
|
4866
|
-
lines.push(`- ${o.name}: ${o.attempts} attempts`);
|
|
4867
|
-
}
|
|
4868
|
-
}
|
|
4869
|
-
this.postToChannel(lines.join('\n'));
|
|
4602
|
+
this.channelMessenger.postCompletionReport(workflowName, outcomes, summary, confidence);
|
|
4870
4603
|
}
|
|
4871
4604
|
/** Post a failure report to the channel. */
|
|
4872
4605
|
postFailureReport(workflowName, outcomes, errorMsg) {
|
|
4873
|
-
|
|
4874
|
-
const failed = outcomes.filter((o) => o.status === 'failed');
|
|
4875
|
-
const skipped = outcomes.filter((o) => o.status === 'skipped');
|
|
4876
|
-
const lines = [
|
|
4877
|
-
`## Workflow **${workflowName}** — Failed`,
|
|
4878
|
-
'',
|
|
4879
|
-
`${completed.length}/${outcomes.length} steps passed. Error: ${errorMsg}`,
|
|
4880
|
-
'',
|
|
4881
|
-
'### Steps',
|
|
4882
|
-
...completed.map((o) => `- **${o.name}** (${o.agent}) — passed`),
|
|
4883
|
-
...failed.map((o) => `- **${o.name}** (${o.agent}) — FAILED: ${o.error ?? 'unknown'}`),
|
|
4884
|
-
...skipped.map((o) => `- **${o.name}** — skipped`),
|
|
4885
|
-
];
|
|
4886
|
-
this.postToChannel(lines.join('\n'));
|
|
4606
|
+
this.channelMessenger.postFailureReport(workflowName, outcomes, errorMsg);
|
|
4887
4607
|
}
|
|
4888
4608
|
/**
|
|
4889
4609
|
* Log a human-readable run summary to the console after completion or failure.
|
|
@@ -5209,7 +4929,7 @@ export class WorkflowRunner {
|
|
|
5209
4929
|
if (!stat.isDirectory())
|
|
5210
4930
|
continue;
|
|
5211
4931
|
// Check if this directory has at least one of the needed step files
|
|
5212
|
-
const hasAny = [...stepNames].some(name => existsSync(path.join(dirPath, `${name}.md`)));
|
|
4932
|
+
const hasAny = [...stepNames].some((name) => existsSync(path.join(dirPath, `${name}.md`)));
|
|
5213
4933
|
if (!hasAny)
|
|
5214
4934
|
continue;
|
|
5215
4935
|
if (!best || stat.mtimeMs > best.mtime) {
|
|
@@ -5338,7 +5058,11 @@ export class WorkflowRunner {
|
|
|
5338
5058
|
for (const step of workflow.steps) {
|
|
5339
5059
|
const isNonAgent = step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
|
|
5340
5060
|
const cachedOutput = completedSteps.has(step.name) ? this.loadStepOutput(runId, step.name) : undefined;
|
|
5341
|
-
const status = completedSteps.has(step.name)
|
|
5061
|
+
const status = completedSteps.has(step.name)
|
|
5062
|
+
? 'completed'
|
|
5063
|
+
: step.name === failedStepName
|
|
5064
|
+
? 'failed'
|
|
5065
|
+
: 'pending';
|
|
5342
5066
|
const stepRow = {
|
|
5343
5067
|
id: this.generateId(),
|
|
5344
5068
|
runId,
|
|
@@ -5351,7 +5075,7 @@ export class WorkflowRunner {
|
|
|
5351
5075
|
: step.type === 'worktree'
|
|
5352
5076
|
? (step.branch ?? '')
|
|
5353
5077
|
: step.type === 'integration'
|
|
5354
|
-
?
|
|
5078
|
+
? `${step.integration}.${step.action}`
|
|
5355
5079
|
: (step.task ?? ''),
|
|
5356
5080
|
dependsOn: step.dependsOn ?? [],
|
|
5357
5081
|
output: cachedOutput,
|