agent-relay 4.0.0 → 4.0.2
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/README.md +5 -5
- 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/index.cjs +1980 -1024
- package/dist/src/cli/commands/core.d.ts.map +1 -1
- package/dist/src/cli/commands/core.js +4 -3
- package/dist/src/cli/commands/core.js.map +1 -1
- package/dist/src/cli/commands/on/relayfile-binary.d.ts +2 -0
- package/dist/src/cli/commands/on/relayfile-binary.d.ts.map +1 -0
- package/dist/src/cli/commands/on/relayfile-binary.js +208 -0
- package/dist/src/cli/commands/on/relayfile-binary.js.map +1 -0
- package/dist/src/cli/commands/on/start.d.ts.map +1 -1
- package/dist/src/cli/commands/on/start.js +62 -26
- package/dist/src/cli/commands/on/start.js.map +1 -1
- package/dist/src/cli/commands/on/workspace.d.ts.map +1 -1
- package/dist/src/cli/commands/on/workspace.js +4 -0
- package/dist/src/cli/commands/on/workspace.js.map +1 -1
- package/dist/src/cli/commands/on.js +1 -1
- package/dist/src/cli/commands/on.js.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
- package/dist/src/cli/lib/broker-lifecycle.js +15 -8
- package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
- package/dist/src/cli/lib/client-factory.d.ts +2 -2
- package/dist/src/cli/lib/client-factory.d.ts.map +1 -1
- package/dist/src/cli/lib/client-factory.js.map +1 -1
- package/dist/src/cost/pricing.d.ts +18 -0
- package/dist/src/cost/pricing.d.ts.map +1 -0
- package/dist/src/cost/pricing.js +111 -0
- package/dist/src/cost/pricing.js.map +1 -0
- package/dist/src/cost/tracker.d.ts +13 -0
- package/dist/src/cost/tracker.d.ts.map +1 -0
- package/dist/src/cost/tracker.js +152 -0
- package/dist/src/cost/tracker.js.map +1 -0
- package/dist/src/cost/types.d.ts +23 -0
- package/dist/src/cost/types.d.ts.map +1 -0
- package/dist/src/cost/types.js +2 -0
- package/dist/src/cost/types.js.map +1 -0
- package/package.json +10 -10
- package/packages/acp-bridge/package.json +2 -2
- package/packages/brand/package.json +1 -1
- package/packages/cloud/package.json +3 -3
- package/packages/config/package.json +1 -1
- package/packages/hooks/package.json +4 -4
- package/packages/memory/package.json +2 -2
- package/packages/openclaw/package.json +2 -2
- package/packages/policy/package.json +2 -2
- package/packages/sdk/dist/broker-path.d.ts +3 -2
- package/packages/sdk/dist/broker-path.d.ts.map +1 -1
- package/packages/sdk/dist/broker-path.js +119 -32
- package/packages/sdk/dist/broker-path.js.map +1 -1
- package/packages/sdk/dist/client.d.ts +12 -2
- package/packages/sdk/dist/client.d.ts.map +1 -1
- package/packages/sdk/dist/client.js +20 -1
- package/packages/sdk/dist/client.js.map +1 -1
- package/packages/sdk/dist/index.d.ts +1 -1
- package/packages/sdk/dist/index.d.ts.map +1 -1
- package/packages/sdk/dist/index.js.map +1 -1
- package/packages/sdk/dist/relay.d.ts +2 -1
- package/packages/sdk/dist/relay.d.ts.map +1 -1
- package/packages/sdk/dist/relay.js +1 -1
- package/packages/sdk/dist/relay.js.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js +117 -0
- package/packages/sdk/dist/workflows/__tests__/channel-messenger.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js +4 -3
- package/packages/sdk/dist/workflows/__tests__/run-summary-table.test.js.map +1 -1
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js +378 -0
- package/packages/sdk/dist/workflows/__tests__/step-executor.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js +145 -0
- package/packages/sdk/dist/workflows/__tests__/template-resolver.test.js.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts +2 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js +170 -0
- package/packages/sdk/dist/workflows/__tests__/verification.test.js.map +1 -0
- package/packages/sdk/dist/workflows/builder.d.ts +3 -2
- package/packages/sdk/dist/workflows/builder.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/builder.js +1 -3
- package/packages/sdk/dist/workflows/builder.js.map +1 -1
- package/packages/sdk/dist/workflows/channel-messenger.d.ts +28 -0
- package/packages/sdk/dist/workflows/channel-messenger.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/channel-messenger.js +255 -0
- package/packages/sdk/dist/workflows/channel-messenger.js.map +1 -0
- package/packages/sdk/dist/workflows/index.d.ts +7 -0
- package/packages/sdk/dist/workflows/index.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/index.js +7 -0
- package/packages/sdk/dist/workflows/index.js.map +1 -1
- package/packages/sdk/dist/workflows/process-spawner.d.ts +35 -0
- package/packages/sdk/dist/workflows/process-spawner.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/process-spawner.js +141 -0
- package/packages/sdk/dist/workflows/process-spawner.js.map +1 -0
- package/packages/sdk/dist/workflows/run.d.ts +2 -1
- package/packages/sdk/dist/workflows/run.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/run.js.map +1 -1
- package/packages/sdk/dist/workflows/runner.d.ts +6 -6
- package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
- package/packages/sdk/dist/workflows/runner.js +443 -719
- package/packages/sdk/dist/workflows/runner.js.map +1 -1
- package/packages/sdk/dist/workflows/step-executor.d.ts +95 -0
- package/packages/sdk/dist/workflows/step-executor.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/step-executor.js +393 -0
- package/packages/sdk/dist/workflows/step-executor.js.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts +33 -0
- package/packages/sdk/dist/workflows/template-resolver.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/template-resolver.js +144 -0
- package/packages/sdk/dist/workflows/template-resolver.js.map +1 -0
- package/packages/sdk/dist/workflows/verification.d.ts +33 -0
- package/packages/sdk/dist/workflows/verification.d.ts.map +1 -0
- package/packages/sdk/dist/workflows/verification.js +122 -0
- package/packages/sdk/dist/workflows/verification.js.map +1 -0
- package/packages/sdk/package.json +2 -2
- package/packages/sdk/src/broker-path.ts +136 -30
- package/packages/sdk/src/client.ts +37 -3
- package/packages/sdk/src/index.ts +1 -0
- package/packages/sdk/src/relay.ts +6 -2
- package/packages/sdk/src/workflows/__tests__/channel-messenger.test.ts +137 -0
- package/packages/sdk/src/workflows/__tests__/run-summary-table.test.ts +4 -3
- package/packages/sdk/src/workflows/__tests__/step-executor.test.ts +444 -0
- package/packages/sdk/src/workflows/__tests__/template-resolver.test.ts +162 -0
- package/packages/sdk/src/workflows/__tests__/verification.test.ts +229 -0
- package/packages/sdk/src/workflows/builder.ts +6 -6
- package/packages/sdk/src/workflows/channel-messenger.ts +314 -0
- package/packages/sdk/src/workflows/index.ts +12 -0
- package/packages/sdk/src/workflows/process-spawner.ts +201 -0
- package/packages/sdk/src/workflows/run.ts +2 -1
- package/packages/sdk/src/workflows/runner.ts +636 -951
- package/packages/sdk/src/workflows/step-executor.ts +579 -0
- package/packages/sdk/src/workflows/template-resolver.ts +180 -0
- package/packages/sdk/src/workflows/verification.ts +184 -0
- package/packages/sdk-py/pyproject.toml +1 -1
- package/packages/telemetry/package.json +1 -1
- package/packages/trajectory/package.json +2 -2
- package/packages/user-directory/package.json +2 -2
- package/packages/utils/package.json +2 -2
|
@@ -39,8 +39,21 @@ import {
|
|
|
39
39
|
} from './custom-steps.js';
|
|
40
40
|
import { collectCliSession, type CliSessionReport } from './cli-session-collector.js';
|
|
41
41
|
import { executeApiStep } from './api-executor.js';
|
|
42
|
+
import { ChannelMessenger } from './channel-messenger.js';
|
|
42
43
|
import { InMemoryWorkflowDb } from './memory-db.js';
|
|
44
|
+
import { buildCommand as buildProcessCommand, spawnProcess } from './process-spawner.js';
|
|
43
45
|
import { formatRunSummaryTable } from './run-summary-table.js';
|
|
46
|
+
import {
|
|
47
|
+
StepExecutor as WorkflowStepLifecycleExecutor,
|
|
48
|
+
type StepExecutorDeps as WorkflowStepLifecycleExecutorDeps,
|
|
49
|
+
} from './step-executor.js';
|
|
50
|
+
import {
|
|
51
|
+
interpolateStepTask as interpolateStepTaskTemplate,
|
|
52
|
+
resolveDotPath as resolveTemplateDotPath,
|
|
53
|
+
resolveTemplate,
|
|
54
|
+
TemplateResolver,
|
|
55
|
+
type VariableContext,
|
|
56
|
+
} from './template-resolver.js';
|
|
44
57
|
import type {
|
|
45
58
|
AgentCli,
|
|
46
59
|
AgentDefinition,
|
|
@@ -73,6 +86,13 @@ import type {
|
|
|
73
86
|
WorkflowStepStatus,
|
|
74
87
|
} from './types.js';
|
|
75
88
|
import { WorkflowTrajectory, type StepOutcome } from './trajectory.js';
|
|
89
|
+
import {
|
|
90
|
+
runVerification,
|
|
91
|
+
stripInjectedTaskEcho,
|
|
92
|
+
type VerificationOptions,
|
|
93
|
+
type VerificationResult,
|
|
94
|
+
WorkflowCompletionError,
|
|
95
|
+
} from './verification.js';
|
|
76
96
|
|
|
77
97
|
// ── AgentRelay SDK imports ──────────────────────────────────────────────────
|
|
78
98
|
|
|
@@ -81,6 +101,59 @@ import { AgentRelay } from '../relay.js';
|
|
|
81
101
|
import type { Agent, AgentRelayOptions } from '../relay.js';
|
|
82
102
|
import { RelayCast, RelayError, type AgentClient } from '@relaycast/sdk';
|
|
83
103
|
|
|
104
|
+
// ── Environment filtering ──────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
/** Keys explicitly allowed to propagate to spawned child processes. */
|
|
107
|
+
const ENV_ALLOWLIST = new Set([
|
|
108
|
+
'PATH',
|
|
109
|
+
'HOME',
|
|
110
|
+
'USER',
|
|
111
|
+
'SHELL',
|
|
112
|
+
'LANG',
|
|
113
|
+
'TERM',
|
|
114
|
+
'TMPDIR',
|
|
115
|
+
'TZ',
|
|
116
|
+
'NODE_ENV',
|
|
117
|
+
'NODE_PATH',
|
|
118
|
+
'NODE_OPTIONS',
|
|
119
|
+
'NODE_EXTRA_CA_CERTS',
|
|
120
|
+
'RUST_LOG',
|
|
121
|
+
'RUST_BACKTRACE',
|
|
122
|
+
'RELAY_API_KEY',
|
|
123
|
+
'RELAYCAST_BASE_URL',
|
|
124
|
+
'AGENT_RELAY_DASHBOARD_PORT',
|
|
125
|
+
'AGENT_RELAY_RUN_ID_FILE',
|
|
126
|
+
'EDITOR',
|
|
127
|
+
'VISUAL',
|
|
128
|
+
'GIT_AUTHOR_NAME',
|
|
129
|
+
'GIT_AUTHOR_EMAIL',
|
|
130
|
+
'GIT_COMMITTER_NAME',
|
|
131
|
+
'GIT_COMMITTER_EMAIL',
|
|
132
|
+
'HTTPS_PROXY',
|
|
133
|
+
'HTTP_PROXY',
|
|
134
|
+
'NO_PROXY',
|
|
135
|
+
'https_proxy',
|
|
136
|
+
'http_proxy',
|
|
137
|
+
'no_proxy',
|
|
138
|
+
'XDG_CONFIG_HOME',
|
|
139
|
+
'XDG_DATA_HOME',
|
|
140
|
+
'XDG_CACHE_HOME',
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
/** Return a filtered copy of process.env containing only allowlisted keys. */
|
|
144
|
+
function filteredEnv(extra?: Record<string, string | undefined>): Record<string, string | undefined> {
|
|
145
|
+
const env: Record<string, string | undefined> = {};
|
|
146
|
+
for (const key of ENV_ALLOWLIST) {
|
|
147
|
+
if (process.env[key] !== undefined) {
|
|
148
|
+
env[key] = process.env[key];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (extra) {
|
|
152
|
+
Object.assign(env, extra);
|
|
153
|
+
}
|
|
154
|
+
return env;
|
|
155
|
+
}
|
|
156
|
+
|
|
84
157
|
// ── DB adapter interface ────────────────────────────────────────────────────
|
|
85
158
|
|
|
86
159
|
/** Minimal DB adapter so the runner is not coupled to a specific driver. */
|
|
@@ -114,27 +187,6 @@ class SpawnExitError extends Error {
|
|
|
114
187
|
}
|
|
115
188
|
}
|
|
116
189
|
|
|
117
|
-
class WorkflowCompletionError extends Error {
|
|
118
|
-
completionReason?: WorkflowStepCompletionReason;
|
|
119
|
-
|
|
120
|
-
constructor(message: string, completionReason?: WorkflowStepCompletionReason) {
|
|
121
|
-
super(message);
|
|
122
|
-
this.name = 'WorkflowCompletionError';
|
|
123
|
-
this.completionReason = completionReason;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
interface VerificationResult {
|
|
128
|
-
passed: boolean;
|
|
129
|
-
completionReason?: WorkflowStepCompletionReason;
|
|
130
|
-
error?: string;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
interface VerificationOptions {
|
|
134
|
-
allowFailure?: boolean;
|
|
135
|
-
completionMarkerFound?: boolean;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
190
|
interface CompletionDecisionResult {
|
|
139
191
|
completionReason: WorkflowStepCompletionReason;
|
|
140
192
|
ownerDecision?: WorkflowOwnerDecision;
|
|
@@ -157,7 +209,14 @@ export type WorkflowEvent =
|
|
|
157
209
|
ownerName: string;
|
|
158
210
|
specialistName: string;
|
|
159
211
|
}
|
|
160
|
-
| {
|
|
212
|
+
| {
|
|
213
|
+
type: 'step:completed';
|
|
214
|
+
runId: string;
|
|
215
|
+
stepName: string;
|
|
216
|
+
output?: string;
|
|
217
|
+
exitCode?: number;
|
|
218
|
+
exitSignal?: string;
|
|
219
|
+
}
|
|
161
220
|
| {
|
|
162
221
|
type: 'step:review-completed';
|
|
163
222
|
runId: string;
|
|
@@ -167,7 +226,14 @@ export type WorkflowEvent =
|
|
|
167
226
|
}
|
|
168
227
|
| { type: 'step:owner-timeout'; runId: string; stepName: string; ownerName: string }
|
|
169
228
|
| { type: 'step:agent-report'; runId: string; stepName: string; report: CliSessionReport }
|
|
170
|
-
| {
|
|
229
|
+
| {
|
|
230
|
+
type: 'step:failed';
|
|
231
|
+
runId: string;
|
|
232
|
+
stepName: string;
|
|
233
|
+
error: string;
|
|
234
|
+
exitCode?: number;
|
|
235
|
+
exitSignal?: string;
|
|
236
|
+
}
|
|
171
237
|
| { type: 'step:skipped'; runId: string; stepName: string }
|
|
172
238
|
| { type: 'step:retrying'; runId: string; stepName: string; attempt: number }
|
|
173
239
|
| { type: 'step:nudged'; runId: string; stepName: string; nudgeCount: number }
|
|
@@ -183,7 +249,7 @@ export interface WorkflowRunnerOptions {
|
|
|
183
249
|
relay?: AgentRelayOptions;
|
|
184
250
|
cwd?: string;
|
|
185
251
|
summaryDir?: string;
|
|
186
|
-
executor?:
|
|
252
|
+
executor?: RunnerStepExecutor;
|
|
187
253
|
envSecrets?: Record<string, string>;
|
|
188
254
|
}
|
|
189
255
|
|
|
@@ -194,7 +260,7 @@ export interface WorkflowRunnerOptions {
|
|
|
194
260
|
* (e.g. Daytona sandboxes) while keeping the runner's DAG/retry/verification
|
|
195
261
|
* machinery intact.
|
|
196
262
|
*/
|
|
197
|
-
export interface
|
|
263
|
+
export interface RunnerStepExecutor {
|
|
198
264
|
executeAgentStep(
|
|
199
265
|
step: WorkflowStep,
|
|
200
266
|
agentDef: AgentDefinition,
|
|
@@ -215,12 +281,6 @@ export interface StepExecutor {
|
|
|
215
281
|
): Promise<{ output: string; success: boolean }>;
|
|
216
282
|
}
|
|
217
283
|
|
|
218
|
-
// ── Variable context for template resolution ────────────────────────────────
|
|
219
|
-
|
|
220
|
-
export interface VariableContext {
|
|
221
|
-
[key: string]: string | number | boolean | undefined;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
284
|
// ── Internal step state ─────────────────────────────────────────────────────
|
|
225
285
|
|
|
226
286
|
interface StepState {
|
|
@@ -307,8 +367,10 @@ export class WorkflowRunner {
|
|
|
307
367
|
private readonly relayOptions: AgentRelayOptions;
|
|
308
368
|
private readonly cwd: string;
|
|
309
369
|
private readonly summaryDir: string;
|
|
310
|
-
private readonly executor?:
|
|
370
|
+
private readonly executor?: RunnerStepExecutor;
|
|
311
371
|
private readonly envSecrets?: Record<string, string>;
|
|
372
|
+
private readonly templateResolver: TemplateResolver;
|
|
373
|
+
private readonly channelMessenger: ChannelMessenger;
|
|
312
374
|
|
|
313
375
|
/** @internal exposed for CLI signal-handler shutdown only */
|
|
314
376
|
relay?: AgentRelay;
|
|
@@ -378,6 +440,8 @@ export class WorkflowRunner {
|
|
|
378
440
|
this.workersPath = path.join(this.cwd, '.agent-relay', 'team', 'workers.json');
|
|
379
441
|
this.executor = options.executor;
|
|
380
442
|
this.envSecrets = options.envSecrets;
|
|
443
|
+
this.templateResolver = new TemplateResolver();
|
|
444
|
+
this.channelMessenger = new ChannelMessenger({ postFn: (text) => this.postToChannel(text) });
|
|
381
445
|
}
|
|
382
446
|
|
|
383
447
|
// ── Path resolution ─────────────────────────────────────────────────────
|
|
@@ -531,16 +595,13 @@ export class WorkflowRunner {
|
|
|
531
595
|
participant: 'owner' | 'worker',
|
|
532
596
|
...senders: Array<string | undefined>
|
|
533
597
|
): void {
|
|
534
|
-
const participants =
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
workerSenders: new Set<string>(),
|
|
539
|
-
};
|
|
598
|
+
const participants = this.stepSignalParticipants.get(stepName) ?? {
|
|
599
|
+
ownerSenders: new Set<string>(),
|
|
600
|
+
workerSenders: new Set<string>(),
|
|
601
|
+
};
|
|
540
602
|
this.stepSignalParticipants.set(stepName, participants);
|
|
541
603
|
|
|
542
|
-
const target =
|
|
543
|
-
participant === 'owner' ? participants.ownerSenders : participants.workerSenders;
|
|
604
|
+
const target = participant === 'owner' ? participants.ownerSenders : participants.workerSenders;
|
|
544
605
|
for (const sender of senders) {
|
|
545
606
|
const trimmed = sender?.trim();
|
|
546
607
|
if (trimmed) target.add(trimmed);
|
|
@@ -557,11 +618,7 @@ export class WorkflowRunner {
|
|
|
557
618
|
|
|
558
619
|
private isSignalFromExpectedSender(stepName: string, signal: CompletionEvidenceSignal): boolean {
|
|
559
620
|
const expectedParticipant =
|
|
560
|
-
signal.kind === 'worker_done'
|
|
561
|
-
? 'worker'
|
|
562
|
-
: signal.kind === 'lead_done'
|
|
563
|
-
? 'owner'
|
|
564
|
-
: undefined;
|
|
621
|
+
signal.kind === 'worker_done' ? 'worker' : signal.kind === 'lead_done' ? 'owner' : undefined;
|
|
565
622
|
if (!expectedParticipant) return true;
|
|
566
623
|
|
|
567
624
|
const participants = this.stepSignalParticipants.get(stepName);
|
|
@@ -589,9 +646,7 @@ export class WorkflowRunner {
|
|
|
589
646
|
evidence: StepCompletionEvidence
|
|
590
647
|
): StepCompletionEvidence {
|
|
591
648
|
evidence.channelPosts = evidence.channelPosts.map((post) => {
|
|
592
|
-
const signals = post.signals.filter((signal) =>
|
|
593
|
-
this.isSignalFromExpectedSender(stepName, signal)
|
|
594
|
-
);
|
|
649
|
+
const signals = post.signals.filter((signal) => this.isSignalFromExpectedSender(stepName, signal));
|
|
595
650
|
return {
|
|
596
651
|
...post,
|
|
597
652
|
completionRelevant: signals.length > 0,
|
|
@@ -772,11 +827,7 @@ export class WorkflowRunner {
|
|
|
772
827
|
): CompletionEvidenceSignal[] {
|
|
773
828
|
const signals: CompletionEvidenceSignal[] = [];
|
|
774
829
|
const seen = new Set<string>();
|
|
775
|
-
const add = (
|
|
776
|
-
kind: CompletionEvidenceSignalKind,
|
|
777
|
-
signalText: string,
|
|
778
|
-
value?: string
|
|
779
|
-
): void => {
|
|
830
|
+
const add = (kind: CompletionEvidenceSignalKind, signalText: string, value?: string): void => {
|
|
780
831
|
const trimmed = signalText.trim().slice(0, 280);
|
|
781
832
|
if (!trimmed) return;
|
|
782
833
|
const key = `${kind}:${trimmed}:${value ?? ''}`;
|
|
@@ -839,7 +890,9 @@ export class WorkflowRunner {
|
|
|
839
890
|
}
|
|
840
891
|
|
|
841
892
|
private uniqueEvidenceRoots(roots: Array<string | undefined>): string[] {
|
|
842
|
-
return [
|
|
893
|
+
return [
|
|
894
|
+
...new Set(roots.filter((root): root is string => Boolean(root)).map((root) => path.resolve(root))),
|
|
895
|
+
];
|
|
843
896
|
}
|
|
844
897
|
|
|
845
898
|
private captureFileSnapshot(root: string): Map<string, FileSnapshotEntry> {
|
|
@@ -947,7 +1000,9 @@ export class WorkflowRunner {
|
|
|
947
1000
|
break;
|
|
948
1001
|
case 'completed_by_owner_decision': {
|
|
949
1002
|
const evidence = this.getStepCompletionEvidence(stepName);
|
|
950
|
-
const markerObserved = evidence?.coordinationSignals.some(
|
|
1003
|
+
const markerObserved = evidence?.coordinationSignals.some(
|
|
1004
|
+
(signal) => signal.kind === 'step_complete'
|
|
1005
|
+
);
|
|
951
1006
|
mode = markerObserved ? 'marker' : 'owner_decision';
|
|
952
1007
|
reason = markerObserved ? 'Legacy STEP_COMPLETE marker observed' : 'Owner approved completion';
|
|
953
1008
|
break;
|
|
@@ -969,9 +1024,7 @@ export class WorkflowRunner {
|
|
|
969
1024
|
const evidence = this.getStepCompletionEvidence(stepName);
|
|
970
1025
|
if (!evidence) return undefined;
|
|
971
1026
|
|
|
972
|
-
const signals = evidence.coordinationSignals
|
|
973
|
-
.slice(-6)
|
|
974
|
-
.map((signal) => signal.value ?? signal.text);
|
|
1027
|
+
const signals = evidence.coordinationSignals.slice(-6).map((signal) => signal.value ?? signal.text);
|
|
975
1028
|
const channelPosts = evidence.channelPosts
|
|
976
1029
|
.filter((post) => post.completionRelevant)
|
|
977
1030
|
.slice(-3)
|
|
@@ -1080,7 +1133,7 @@ export class WorkflowRunner {
|
|
|
1080
1133
|
}
|
|
1081
1134
|
|
|
1082
1135
|
return {
|
|
1083
|
-
...(this.relayOptions.env ??
|
|
1136
|
+
...(this.relayOptions.env ?? filteredEnv()),
|
|
1084
1137
|
RELAY_API_KEY: this.relayApiKey,
|
|
1085
1138
|
};
|
|
1086
1139
|
}
|
|
@@ -1366,9 +1419,7 @@ export class WorkflowRunner {
|
|
|
1366
1419
|
// Validate workdir references on steps
|
|
1367
1420
|
for (const step of resolvedSteps) {
|
|
1368
1421
|
if (step.workdir && !dryRunPaths.has(step.workdir)) {
|
|
1369
|
-
errors.push(
|
|
1370
|
-
`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`
|
|
1371
|
-
);
|
|
1422
|
+
errors.push(`Step "${step.name}" references workdir "${step.workdir}" which is not defined in paths`);
|
|
1372
1423
|
}
|
|
1373
1424
|
}
|
|
1374
1425
|
|
|
@@ -1701,79 +1752,15 @@ export class WorkflowRunner {
|
|
|
1701
1752
|
|
|
1702
1753
|
/** Resolve {{variable}} placeholders in all task strings. */
|
|
1703
1754
|
resolveVariables(config: RelayYamlConfig, vars: VariableContext): RelayYamlConfig {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
for (const agent of resolved.agents) {
|
|
1707
|
-
if (agent.task) {
|
|
1708
|
-
agent.task = this.interpolate(agent.task, vars);
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
if (resolved.workflows) {
|
|
1713
|
-
for (const wf of resolved.workflows) {
|
|
1714
|
-
for (const step of wf.steps) {
|
|
1715
|
-
// Resolve variables in task (agent steps) and command (deterministic steps)
|
|
1716
|
-
if (step.task) {
|
|
1717
|
-
step.task = this.interpolate(step.task, vars);
|
|
1718
|
-
}
|
|
1719
|
-
if (step.command) {
|
|
1720
|
-
step.command = this.interpolate(step.command, vars);
|
|
1721
|
-
}
|
|
1722
|
-
// Resolve variables in integration step params
|
|
1723
|
-
if (step.params && typeof step.params === 'object') {
|
|
1724
|
-
for (const key of Object.keys(step.params)) {
|
|
1725
|
-
const val = (step.params as Record<string, unknown>)[key];
|
|
1726
|
-
if (typeof val === 'string') {
|
|
1727
|
-
(step.params as Record<string, string>)[key] = this.interpolate(val, vars);
|
|
1728
|
-
}
|
|
1729
|
-
}
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
return resolved;
|
|
1755
|
+
return this.templateResolver.resolveVariables(config, vars);
|
|
1736
1756
|
}
|
|
1737
1757
|
|
|
1738
1758
|
private interpolate(template: string, vars: VariableContext): string {
|
|
1739
|
-
return template
|
|
1740
|
-
// Skip step-output placeholders — they are resolved at execution time by interpolateStepTask()
|
|
1741
|
-
if (key.startsWith('steps.')) {
|
|
1742
|
-
return _match;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
// Resolve dot-path variables like steps.plan.output
|
|
1746
|
-
const value = this.resolveDotPath(key, vars);
|
|
1747
|
-
if (value === undefined) {
|
|
1748
|
-
throw new Error(`Unresolved variable: {{${key}}}`);
|
|
1749
|
-
}
|
|
1750
|
-
return String(value);
|
|
1751
|
-
});
|
|
1759
|
+
return resolveTemplate(template, vars);
|
|
1752
1760
|
}
|
|
1753
1761
|
|
|
1754
1762
|
private resolveDotPath(key: string, vars: VariableContext): string | number | boolean | undefined {
|
|
1755
|
-
|
|
1756
|
-
if (!key.includes('.')) {
|
|
1757
|
-
return vars[key];
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
// Dot-path — walk into nested context
|
|
1761
|
-
const parts = key.split('.');
|
|
1762
|
-
let current: unknown = vars;
|
|
1763
|
-
for (const part of parts) {
|
|
1764
|
-
if (current === null || current === undefined || typeof current !== 'object') {
|
|
1765
|
-
return undefined;
|
|
1766
|
-
}
|
|
1767
|
-
current = (current as Record<string, unknown>)[part];
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
if (current === undefined || current === null) {
|
|
1771
|
-
return undefined;
|
|
1772
|
-
}
|
|
1773
|
-
if (typeof current === 'string' || typeof current === 'number' || typeof current === 'boolean') {
|
|
1774
|
-
return current;
|
|
1775
|
-
}
|
|
1776
|
-
return String(current);
|
|
1763
|
+
return resolveTemplateDotPath(key, vars);
|
|
1777
1764
|
}
|
|
1778
1765
|
|
|
1779
1766
|
/** Build a nested context from completed step outputs for {{steps.X.output}} resolution. */
|
|
@@ -1796,14 +1783,108 @@ export class WorkflowRunner {
|
|
|
1796
1783
|
|
|
1797
1784
|
/** Interpolate step-output variables, silently skipping unresolved ones (they may be user vars). */
|
|
1798
1785
|
private interpolateStepTask(template: string, context: VariableContext): string {
|
|
1799
|
-
return template
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1786
|
+
return interpolateStepTaskTemplate(template, context);
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
private createStepLifecycleExecutor(
|
|
1790
|
+
workflow: WorkflowDefinition,
|
|
1791
|
+
stepStates: Map<string, StepState>,
|
|
1792
|
+
agentMap: Map<string, AgentDefinition>,
|
|
1793
|
+
errorHandling: ErrorHandlingConfig | undefined,
|
|
1794
|
+
runId: string
|
|
1795
|
+
): WorkflowStepLifecycleExecutor<StepState> {
|
|
1796
|
+
// eslint-disable-next-line prefer-const -- circular: deps closure captures lifecycle before assignment
|
|
1797
|
+
let lifecycle!: WorkflowStepLifecycleExecutor<StepState>;
|
|
1798
|
+
const deps: WorkflowStepLifecycleExecutorDeps<StepState> = {
|
|
1799
|
+
cwd: this.cwd,
|
|
1800
|
+
runId,
|
|
1801
|
+
templateResolver: this.templateResolver,
|
|
1802
|
+
channelMessenger: this.channelMessenger,
|
|
1803
|
+
verificationRunner: (check, output, stepName, injectedTaskText, options) =>
|
|
1804
|
+
this.runVerification(check, output, stepName, injectedTaskText, options),
|
|
1805
|
+
postToChannel: (text) => this.postToChannel(text),
|
|
1806
|
+
persistStepRow: async (stepId, patch) => this.db.updateStep(stepId, patch),
|
|
1807
|
+
persistStepOutput: async (lifecycleRunId, stepName, output) =>
|
|
1808
|
+
this.persistStepOutput(lifecycleRunId, stepName, output),
|
|
1809
|
+
loadStepOutput: (lifecycleRunId, stepName) => this.loadStepOutput(lifecycleRunId, stepName),
|
|
1810
|
+
checkAborted: () => this.checkAborted(),
|
|
1811
|
+
waitIfPaused: () => this.waitIfPaused(),
|
|
1812
|
+
log: (message) => this.log(message),
|
|
1813
|
+
onStepStarted: async (step) => {
|
|
1814
|
+
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
1815
|
+
},
|
|
1816
|
+
onStepCompleted: async (step, state, result) => {
|
|
1817
|
+
this.emit({
|
|
1818
|
+
type: 'step:completed',
|
|
1819
|
+
runId,
|
|
1820
|
+
stepName: step.name,
|
|
1821
|
+
output: result.output,
|
|
1822
|
+
exitCode: result.exitCode,
|
|
1823
|
+
exitSignal: result.exitSignal,
|
|
1824
|
+
});
|
|
1825
|
+
this.finalizeStepEvidence(step.name, result.status, state.row.completedAt, result.completionReason);
|
|
1826
|
+
},
|
|
1827
|
+
onStepFailed: async (step, state, result) => {
|
|
1828
|
+
this.captureStepTerminalEvidence(
|
|
1829
|
+
step.name,
|
|
1830
|
+
{},
|
|
1831
|
+
{
|
|
1832
|
+
exitCode: result.exitCode,
|
|
1833
|
+
exitSignal: result.exitSignal,
|
|
1834
|
+
}
|
|
1835
|
+
);
|
|
1836
|
+
this.emit({
|
|
1837
|
+
type: 'step:failed',
|
|
1838
|
+
runId,
|
|
1839
|
+
stepName: step.name,
|
|
1840
|
+
error: result.error ?? 'Unknown error',
|
|
1841
|
+
exitCode: result.exitCode,
|
|
1842
|
+
exitSignal: result.exitSignal,
|
|
1843
|
+
});
|
|
1844
|
+
this.finalizeStepEvidence(step.name, 'failed', state.row.completedAt, result.completionReason);
|
|
1845
|
+
},
|
|
1846
|
+
executeStep: async (step, state) => {
|
|
1847
|
+
await this.executeStep(step, state, stepStates, agentMap, errorHandling, runId, lifecycle);
|
|
1848
|
+
return {
|
|
1849
|
+
status: state.row.status,
|
|
1850
|
+
output: state.row.output ?? '',
|
|
1851
|
+
completionReason: state.row.completionReason,
|
|
1852
|
+
retries: state.row.retryCount,
|
|
1853
|
+
error: state.row.error,
|
|
1854
|
+
};
|
|
1855
|
+
},
|
|
1856
|
+
onBeginTrack: async (steps) => {
|
|
1857
|
+
if (steps.length > 1 && this.trajectory) {
|
|
1858
|
+
await this.trajectory.beginTrack(steps.map((step) => step.name).join(', '));
|
|
1859
|
+
}
|
|
1860
|
+
},
|
|
1861
|
+
onConverge: async (readySteps, batchOutcomes) => {
|
|
1862
|
+
if (readySteps.length <= 1 || !this.trajectory?.shouldReflectOnConverge()) {
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const completedNames = new Set(
|
|
1867
|
+
batchOutcomes.filter((outcome) => outcome.status === 'completed').map((outcome) => outcome.name)
|
|
1868
|
+
);
|
|
1869
|
+
const unblocked = workflow.steps
|
|
1870
|
+
.filter((step) => step.dependsOn?.some((dependency) => completedNames.has(dependency)))
|
|
1871
|
+
.filter((step) => stepStates.get(step.name)?.row.status === 'pending')
|
|
1872
|
+
.map((step) => step.name);
|
|
1873
|
+
|
|
1874
|
+
await this.trajectory.synthesizeAndReflect(
|
|
1875
|
+
readySteps.map((step) => step.name).join(' + '),
|
|
1876
|
+
batchOutcomes,
|
|
1877
|
+
unblocked.length > 0 ? unblocked : undefined
|
|
1878
|
+
);
|
|
1879
|
+
},
|
|
1880
|
+
markDownstreamSkipped: async (failedStepName) =>
|
|
1881
|
+
this.markDownstreamSkipped(failedStepName, workflow.steps, stepStates, runId),
|
|
1882
|
+
buildCompletionMode: (stepName, completionReason) =>
|
|
1883
|
+
completionReason ? this.buildStepCompletionDecision(stepName, completionReason)?.mode : undefined,
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
lifecycle = new WorkflowStepLifecycleExecutor<StepState>(deps);
|
|
1887
|
+
return lifecycle;
|
|
1807
1888
|
}
|
|
1808
1889
|
|
|
1809
1890
|
// ── Execution ───────────────────────────────────────────────────────────
|
|
@@ -1873,7 +1954,8 @@ export class WorkflowRunner {
|
|
|
1873
1954
|
const stepStates = new Map<string, StepState>();
|
|
1874
1955
|
for (const step of resolvedWorkflow.steps) {
|
|
1875
1956
|
// Handle agent, deterministic, worktree, and integration steps
|
|
1876
|
-
const isNonAgent =
|
|
1957
|
+
const isNonAgent =
|
|
1958
|
+
step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
|
|
1877
1959
|
|
|
1878
1960
|
const stepRow: WorkflowStepRow = {
|
|
1879
1961
|
id: this.generateId(),
|
|
@@ -1888,7 +1970,7 @@ export class WorkflowRunner {
|
|
|
1888
1970
|
: step.type === 'worktree'
|
|
1889
1971
|
? (step.branch ?? '')
|
|
1890
1972
|
: step.type === 'integration'
|
|
1891
|
-
?
|
|
1973
|
+
? `${step.integration}.${step.action}`
|
|
1892
1974
|
: (step.task ?? ''),
|
|
1893
1975
|
dependsOn: step.dependsOn ?? [],
|
|
1894
1976
|
retryCount: 0,
|
|
@@ -1913,8 +1995,7 @@ export class WorkflowRunner {
|
|
|
1913
1995
|
const skippedCount = transitiveDeps.size;
|
|
1914
1996
|
|
|
1915
1997
|
// Determine which run ID to load cached outputs from
|
|
1916
|
-
const cacheRunId = executeOptions.previousRunId
|
|
1917
|
-
?? this.findMostRecentRunWithSteps(transitiveDeps);
|
|
1998
|
+
const cacheRunId = executeOptions.previousRunId ?? this.findMostRecentRunWithSteps(transitiveDeps);
|
|
1918
1999
|
|
|
1919
2000
|
for (const depName of transitiveDeps) {
|
|
1920
2001
|
const state = stepStates.get(depName);
|
|
@@ -2082,11 +2163,12 @@ export class WorkflowRunner {
|
|
|
2082
2163
|
config.swarm.channel = channel;
|
|
2083
2164
|
await this.db.updateRun(runId, { config });
|
|
2084
2165
|
}
|
|
2085
|
-
const relaycastDisabled =
|
|
2086
|
-
this.relayOptions.env?.AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST === '1';
|
|
2166
|
+
const relaycastDisabled = this.relayOptions.env?.AGENT_RELAY_WORKFLOW_DISABLE_RELAYCAST === '1';
|
|
2087
2167
|
const requiresBroker =
|
|
2088
2168
|
!this.executor &&
|
|
2089
|
-
workflow.steps.some(
|
|
2169
|
+
workflow.steps.some(
|
|
2170
|
+
(step) => step.type !== 'deterministic' && step.type !== 'worktree' && step.type !== 'integration'
|
|
2171
|
+
);
|
|
2090
2172
|
// Skip broker/relay init when an external executor handles agent spawning
|
|
2091
2173
|
if (requiresBroker) {
|
|
2092
2174
|
if (!relaycastDisabled) {
|
|
@@ -2324,10 +2406,8 @@ export class WorkflowRunner {
|
|
|
2324
2406
|
this.log(`Executing ${workflow.steps.length} steps (pattern: ${config.swarm.pattern})`);
|
|
2325
2407
|
await this.executeSteps(workflow, stepStates, agentMap, config.errorHandling, runId);
|
|
2326
2408
|
|
|
2327
|
-
const errorStrategy =
|
|
2328
|
-
|
|
2329
|
-
const continueOnError =
|
|
2330
|
-
errorStrategy === 'continue' || errorStrategy === 'skip';
|
|
2409
|
+
const errorStrategy = config.errorHandling?.strategy ?? workflow.onError ?? 'fail-fast';
|
|
2410
|
+
const continueOnError = errorStrategy === 'continue' || errorStrategy === 'skip';
|
|
2331
2411
|
const allCompleted = [...stepStates.values()].every(
|
|
2332
2412
|
(s) =>
|
|
2333
2413
|
s.row.status === 'completed' ||
|
|
@@ -2479,8 +2559,6 @@ export class WorkflowRunner {
|
|
|
2479
2559
|
runId: string
|
|
2480
2560
|
): Promise<void> {
|
|
2481
2561
|
const rawStrategy = errorHandling?.strategy ?? workflow.onError ?? 'fail-fast';
|
|
2482
|
-
// Map shorthand onError values to canonical strategy names.
|
|
2483
|
-
// 'retry' maps to 'fail-fast' so downstream steps are properly skipped after retries exhaust.
|
|
2484
2562
|
const strategy =
|
|
2485
2563
|
rawStrategy === 'fail'
|
|
2486
2564
|
? 'fail-fast'
|
|
@@ -2490,109 +2568,17 @@ export class WorkflowRunner {
|
|
|
2490
2568
|
? 'fail-fast'
|
|
2491
2569
|
: rawStrategy;
|
|
2492
2570
|
|
|
2493
|
-
|
|
2494
|
-
while (true) {
|
|
2495
|
-
this.checkAborted();
|
|
2496
|
-
await this.waitIfPaused();
|
|
2497
|
-
|
|
2498
|
-
const readySteps = this.findReadySteps(workflow.steps, stepStates);
|
|
2499
|
-
if (readySteps.length === 0) {
|
|
2500
|
-
// No steps ready — either all done or blocked
|
|
2501
|
-
break;
|
|
2502
|
-
}
|
|
2571
|
+
const lifecycle = this.createStepLifecycleExecutor(workflow, stepStates, agentMap, errorHandling, runId);
|
|
2503
2572
|
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
// N simultaneous registration requests which causes spawn timeouts.
|
|
2514
|
-
const STAGGER_THRESHOLD = 3;
|
|
2515
|
-
const STAGGER_DELAY_MS = 2_000;
|
|
2516
|
-
const results = await Promise.allSettled(
|
|
2517
|
-
readySteps.map((step, i) => {
|
|
2518
|
-
const delay = readySteps.length > STAGGER_THRESHOLD ? i * STAGGER_DELAY_MS : 0;
|
|
2519
|
-
if (delay === 0) {
|
|
2520
|
-
return this.executeStep(step, stepStates, agentMap, errorHandling, runId);
|
|
2521
|
-
}
|
|
2522
|
-
return new Promise<void>((resolve) => setTimeout(resolve, delay)).then(() =>
|
|
2523
|
-
this.executeStep(step, stepStates, agentMap, errorHandling, runId)
|
|
2524
|
-
);
|
|
2525
|
-
})
|
|
2526
|
-
);
|
|
2527
|
-
|
|
2528
|
-
// Collect outcomes from this batch for convergence reflection
|
|
2529
|
-
const batchOutcomes: StepOutcome[] = [];
|
|
2530
|
-
|
|
2531
|
-
for (let i = 0; i < results.length; i++) {
|
|
2532
|
-
const result = results[i];
|
|
2533
|
-
const step = readySteps[i];
|
|
2534
|
-
const state = stepStates.get(step.name);
|
|
2535
|
-
|
|
2536
|
-
if (result.status === 'rejected') {
|
|
2537
|
-
const error = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
2538
|
-
if (state && state.row.status !== 'failed') {
|
|
2539
|
-
await this.markStepFailed(state, error, runId);
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
batchOutcomes.push({
|
|
2543
|
-
name: step.name,
|
|
2544
|
-
agent: step.agent ?? 'deterministic',
|
|
2545
|
-
status: 'failed',
|
|
2546
|
-
attempts: (state?.row.retryCount ?? 0) + 1,
|
|
2547
|
-
error,
|
|
2548
|
-
});
|
|
2549
|
-
|
|
2550
|
-
if (strategy === 'fail-fast') {
|
|
2551
|
-
// Mark all pending downstream steps as skipped
|
|
2552
|
-
await this.markDownstreamSkipped(step.name, workflow.steps, stepStates, runId);
|
|
2553
|
-
throw new Error(`Step "${step.name}" failed: ${error}`);
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
if (strategy === 'continue') {
|
|
2557
|
-
await this.markDownstreamSkipped(step.name, workflow.steps, stepStates, runId);
|
|
2558
|
-
}
|
|
2559
|
-
} else {
|
|
2560
|
-
batchOutcomes.push({
|
|
2561
|
-
name: step.name,
|
|
2562
|
-
agent: step.agent ?? 'deterministic',
|
|
2563
|
-
status: state?.row.status === 'completed' ? 'completed' : 'failed',
|
|
2564
|
-
attempts: (state?.row.retryCount ?? 0) + 1,
|
|
2565
|
-
output: state?.row.output,
|
|
2566
|
-
verificationPassed: state?.row.status === 'completed' && step.verification !== undefined,
|
|
2567
|
-
completionMode: state?.row.completionReason
|
|
2568
|
-
? this.buildStepCompletionDecision(step.name, state.row.completionReason)?.mode
|
|
2569
|
-
: undefined,
|
|
2570
|
-
});
|
|
2571
|
-
}
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
// Reflect at convergence when a parallel batch completes
|
|
2575
|
-
if (readySteps.length > 1 && this.trajectory?.shouldReflectOnConverge()) {
|
|
2576
|
-
const label = readySteps.map((s) => s.name).join(' + ');
|
|
2577
|
-
// Find steps that this batch unblocks
|
|
2578
|
-
const completedNames = new Set(
|
|
2579
|
-
batchOutcomes.filter((o) => o.status === 'completed').map((o) => o.name)
|
|
2580
|
-
);
|
|
2581
|
-
const unblocked = workflow.steps
|
|
2582
|
-
.filter((s) => s.dependsOn?.some((dep) => completedNames.has(dep)))
|
|
2583
|
-
.filter((s) => {
|
|
2584
|
-
const st = stepStates.get(s.name);
|
|
2585
|
-
return st && st.row.status === 'pending';
|
|
2586
|
-
})
|
|
2587
|
-
.map((s) => s.name);
|
|
2588
|
-
|
|
2589
|
-
await this.trajectory.synthesizeAndReflect(
|
|
2590
|
-
label,
|
|
2591
|
-
batchOutcomes,
|
|
2592
|
-
unblocked.length > 0 ? unblocked : undefined
|
|
2593
|
-
);
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2573
|
+
await lifecycle.executeAll(
|
|
2574
|
+
workflow.steps,
|
|
2575
|
+
agentMap,
|
|
2576
|
+
{
|
|
2577
|
+
...(errorHandling ?? { strategy: 'fail-fast' }),
|
|
2578
|
+
strategy,
|
|
2579
|
+
},
|
|
2580
|
+
stepStates
|
|
2581
|
+
);
|
|
2596
2582
|
}
|
|
2597
2583
|
|
|
2598
2584
|
private findReadySteps(steps: WorkflowStep[], stepStates: Map<string, StepState>): WorkflowStep[] {
|
|
@@ -2626,7 +2612,7 @@ export class WorkflowRunner {
|
|
|
2626
2612
|
const child = cpSpawn('sh', ['-c', check.command], {
|
|
2627
2613
|
stdio: 'pipe',
|
|
2628
2614
|
cwd: this.cwd,
|
|
2629
|
-
env:
|
|
2615
|
+
env: filteredEnv(),
|
|
2630
2616
|
});
|
|
2631
2617
|
|
|
2632
2618
|
const stdoutChunks: string[] = [];
|
|
@@ -2742,24 +2728,26 @@ export class WorkflowRunner {
|
|
|
2742
2728
|
|
|
2743
2729
|
private async executeStep(
|
|
2744
2730
|
step: WorkflowStep,
|
|
2731
|
+
state: StepState,
|
|
2745
2732
|
stepStates: Map<string, StepState>,
|
|
2746
2733
|
agentMap: Map<string, AgentDefinition>,
|
|
2747
2734
|
errorHandling: ErrorHandlingConfig | undefined,
|
|
2748
|
-
runId: string
|
|
2735
|
+
runId: string,
|
|
2736
|
+
lifecycle: WorkflowStepLifecycleExecutor<StepState>
|
|
2749
2737
|
): Promise<void> {
|
|
2750
2738
|
// Branch: deterministic steps execute shell commands
|
|
2751
2739
|
if (this.isDeterministicStep(step)) {
|
|
2752
|
-
return this.executeDeterministicStep(step, stepStates, runId, errorHandling);
|
|
2740
|
+
return this.executeDeterministicStep(step, state, stepStates, runId, errorHandling, lifecycle);
|
|
2753
2741
|
}
|
|
2754
2742
|
|
|
2755
2743
|
// Branch: worktree steps set up git worktrees
|
|
2756
2744
|
if (this.isWorktreeStep(step)) {
|
|
2757
|
-
return this.executeWorktreeStep(step, stepStates, runId);
|
|
2745
|
+
return this.executeWorktreeStep(step, state, stepStates, runId, lifecycle);
|
|
2758
2746
|
}
|
|
2759
2747
|
|
|
2760
2748
|
// Branch: integration steps interact with external services
|
|
2761
2749
|
if (this.isIntegrationStep(step)) {
|
|
2762
|
-
return this.executeIntegrationStep(step, stepStates, runId);
|
|
2750
|
+
return this.executeIntegrationStep(step, state, stepStates, runId, lifecycle);
|
|
2763
2751
|
}
|
|
2764
2752
|
|
|
2765
2753
|
// Agent step execution
|
|
@@ -2772,115 +2760,71 @@ export class WorkflowRunner {
|
|
|
2772
2760
|
*/
|
|
2773
2761
|
private async executeDeterministicStep(
|
|
2774
2762
|
step: WorkflowStep,
|
|
2763
|
+
state: StepState,
|
|
2775
2764
|
stepStates: Map<string, StepState>,
|
|
2776
2765
|
runId: string,
|
|
2777
|
-
errorHandling: ErrorHandlingConfig | undefined
|
|
2766
|
+
errorHandling: ErrorHandlingConfig | undefined,
|
|
2767
|
+
lifecycle: WorkflowStepLifecycleExecutor<StepState>
|
|
2778
2768
|
): Promise<void> {
|
|
2779
|
-
const state = stepStates.get(step.name);
|
|
2780
|
-
if (!state) throw new Error(`Step state not found: ${step.name}`);
|
|
2781
|
-
|
|
2782
2769
|
const maxRetries = step.retries ?? errorHandling?.maxRetries ?? 0;
|
|
2783
2770
|
const retryDelay = errorHandling?.retryDelayMs ?? 1000;
|
|
2784
|
-
let lastError
|
|
2771
|
+
let lastError = 'Unknown error';
|
|
2785
2772
|
let lastCompletionReason: WorkflowStepCompletionReason | undefined;
|
|
2786
2773
|
let lastExitCode: number | undefined;
|
|
2787
2774
|
let lastExitSignal: string | undefined;
|
|
2788
2775
|
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
if (attempt > 0) {
|
|
2776
|
+
const result = await lifecycle.monitorStep(step, state, {
|
|
2777
|
+
maxRetries,
|
|
2778
|
+
retryDelayMs: retryDelay,
|
|
2779
|
+
startMessage: `**[${step.name}]** Started (deterministic)`,
|
|
2780
|
+
onRetry: async (attempt, total) => {
|
|
2796
2781
|
this.emit({ type: 'step:retrying', runId, stepName: step.name, attempt });
|
|
2797
|
-
this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${
|
|
2782
|
+
this.postToChannel(`**[${step.name}]** Retrying (attempt ${attempt + 1}/${total + 1})`);
|
|
2798
2783
|
this.recordStepToolSideEffect(step.name, {
|
|
2799
2784
|
type: 'retry',
|
|
2800
|
-
detail: `Retrying attempt ${attempt + 1}/${
|
|
2801
|
-
raw: { attempt, maxRetries },
|
|
2802
|
-
});
|
|
2803
|
-
state.row.retryCount = attempt;
|
|
2804
|
-
await this.db.updateStep(state.row.id, {
|
|
2805
|
-
retryCount: attempt,
|
|
2806
|
-
updatedAt: new Date().toISOString(),
|
|
2785
|
+
detail: `Retrying attempt ${attempt + 1}/${total + 1}`,
|
|
2786
|
+
raw: { attempt, maxRetries: total },
|
|
2807
2787
|
});
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
state.row.status = 'running';
|
|
2813
|
-
state.row.error = undefined;
|
|
2814
|
-
state.row.completionReason = undefined;
|
|
2815
|
-
state.row.startedAt = new Date().toISOString();
|
|
2816
|
-
await this.db.updateStep(state.row.id, {
|
|
2817
|
-
status: 'running',
|
|
2818
|
-
error: undefined,
|
|
2819
|
-
completionReason: undefined,
|
|
2820
|
-
startedAt: state.row.startedAt,
|
|
2821
|
-
updatedAt: new Date().toISOString(),
|
|
2822
|
-
});
|
|
2823
|
-
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
2824
|
-
this.postToChannel(`**[${step.name}]** Started (deterministic)`);
|
|
2825
|
-
|
|
2826
|
-
// Resolve variables in the command (e.g., {{steps.plan.output}}, {{branch-name}})
|
|
2827
|
-
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2828
|
-
let resolvedCommand = this.interpolateStepTask(step.command ?? '', stepOutputContext);
|
|
2788
|
+
},
|
|
2789
|
+
execute: async () => {
|
|
2790
|
+
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2791
|
+
let resolvedCommand = this.interpolateStepTask(step.command ?? '', stepOutputContext);
|
|
2829
2792
|
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
});
|
|
2793
|
+
resolvedCommand = resolvedCommand.replace(/\{\{([\w][\w.\-]*)\}\}/g, (_match, key: string) => {
|
|
2794
|
+
if (key.startsWith('steps.')) return _match;
|
|
2795
|
+
const value = this.resolveDotPath(key, stepOutputContext);
|
|
2796
|
+
return value !== undefined ? String(value) : _match;
|
|
2797
|
+
});
|
|
2836
2798
|
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2799
|
+
const stepCwd = this.resolveEffectiveCwd(step);
|
|
2800
|
+
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2840
2801
|
|
|
2841
|
-
try {
|
|
2842
|
-
// Delegate to executor if present
|
|
2843
2802
|
if (this.executor?.executeDeterministicStep) {
|
|
2844
|
-
const
|
|
2845
|
-
lastExitCode =
|
|
2803
|
+
const executorResult = await this.executor.executeDeterministicStep(step, resolvedCommand, stepCwd);
|
|
2804
|
+
lastExitCode = executorResult.exitCode;
|
|
2805
|
+
lastExitSignal = undefined;
|
|
2846
2806
|
const failOnError = step.failOnError !== false;
|
|
2847
|
-
if (failOnError &&
|
|
2807
|
+
if (failOnError && executorResult.exitCode !== 0) {
|
|
2848
2808
|
throw new Error(
|
|
2849
|
-
`Command failed with exit code ${
|
|
2809
|
+
`Command failed with exit code ${executorResult.exitCode}: ${executorResult.output.slice(0, 500)}`
|
|
2850
2810
|
);
|
|
2851
2811
|
}
|
|
2852
2812
|
const output =
|
|
2853
|
-
step.captureOutput !== false
|
|
2813
|
+
step.captureOutput !== false
|
|
2814
|
+
? executorResult.output
|
|
2815
|
+
: `Command completed (exit code ${executorResult.exitCode})`;
|
|
2854
2816
|
this.captureStepTerminalEvidence(
|
|
2855
2817
|
step.name,
|
|
2856
|
-
{ stdout:
|
|
2857
|
-
{ exitCode:
|
|
2818
|
+
{ stdout: executorResult.output, combined: executorResult.output },
|
|
2819
|
+
{ exitCode: executorResult.exitCode }
|
|
2858
2820
|
);
|
|
2859
2821
|
const verificationResult = step.verification
|
|
2860
2822
|
? this.runVerification(step.verification, output, step.name)
|
|
2861
2823
|
: undefined;
|
|
2862
|
-
|
|
2863
|
-
// Mark completed
|
|
2864
|
-
state.row.status = 'completed';
|
|
2865
|
-
state.row.output = output;
|
|
2866
|
-
state.row.completionReason = verificationResult?.completionReason;
|
|
2867
|
-
state.row.completedAt = new Date().toISOString();
|
|
2868
|
-
await this.db.updateStep(state.row.id, {
|
|
2869
|
-
status: 'completed',
|
|
2824
|
+
return {
|
|
2870
2825
|
output,
|
|
2871
2826
|
completionReason: verificationResult?.completionReason,
|
|
2872
|
-
|
|
2873
|
-
updatedAt: new Date().toISOString(),
|
|
2874
|
-
});
|
|
2875
|
-
await this.persistStepOutput(runId, step.name, output);
|
|
2876
|
-
this.emit({ type: 'step:completed', runId, stepName: step.name, output });
|
|
2877
|
-
this.finalizeStepEvidence(
|
|
2878
|
-
step.name,
|
|
2879
|
-
'completed',
|
|
2880
|
-
state.row.completedAt,
|
|
2881
|
-
verificationResult?.completionReason
|
|
2882
|
-
);
|
|
2883
|
-
return;
|
|
2827
|
+
};
|
|
2884
2828
|
}
|
|
2885
2829
|
|
|
2886
2830
|
let commandStdout = '';
|
|
@@ -2889,13 +2833,11 @@ export class WorkflowRunner {
|
|
|
2889
2833
|
const child = cpSpawn('sh', ['-c', resolvedCommand], {
|
|
2890
2834
|
stdio: 'pipe',
|
|
2891
2835
|
cwd: stepCwd,
|
|
2892
|
-
env:
|
|
2836
|
+
env: filteredEnv(),
|
|
2893
2837
|
});
|
|
2894
2838
|
|
|
2895
2839
|
const stdoutChunks: string[] = [];
|
|
2896
2840
|
const stderrChunks: string[] = [];
|
|
2897
|
-
|
|
2898
|
-
// Wire abort signal
|
|
2899
2841
|
const abortSignal = this.abortController?.signal;
|
|
2900
2842
|
let abortHandler: (() => void) | undefined;
|
|
2901
2843
|
if (abortSignal && !abortSignal.aborted) {
|
|
@@ -2906,7 +2848,6 @@ export class WorkflowRunner {
|
|
|
2906
2848
|
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
2907
2849
|
}
|
|
2908
2850
|
|
|
2909
|
-
// Handle timeout
|
|
2910
2851
|
let timedOut = false;
|
|
2911
2852
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2912
2853
|
if (step.timeoutMs) {
|
|
@@ -2950,13 +2891,10 @@ export class WorkflowRunner {
|
|
|
2950
2891
|
lastExitCode = code ?? undefined;
|
|
2951
2892
|
lastExitSignal = signal ?? undefined;
|
|
2952
2893
|
|
|
2953
|
-
// Check exit code unless failOnError is explicitly false
|
|
2954
2894
|
const failOnError = step.failOnError !== false;
|
|
2955
2895
|
if (failOnError && code !== 0 && code !== null) {
|
|
2956
2896
|
reject(
|
|
2957
|
-
new Error(
|
|
2958
|
-
`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ''}`
|
|
2959
|
-
)
|
|
2897
|
+
new Error(`Command failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ''}`)
|
|
2960
2898
|
);
|
|
2961
2899
|
return;
|
|
2962
2900
|
}
|
|
@@ -2972,6 +2910,7 @@ export class WorkflowRunner {
|
|
|
2972
2910
|
reject(new Error(`Failed to execute command: ${err.message}`));
|
|
2973
2911
|
});
|
|
2974
2912
|
});
|
|
2913
|
+
|
|
2975
2914
|
this.captureStepTerminalEvidence(
|
|
2976
2915
|
step.name,
|
|
2977
2916
|
{
|
|
@@ -2986,47 +2925,38 @@ export class WorkflowRunner {
|
|
|
2986
2925
|
? this.runVerification(step.verification, output, step.name)
|
|
2987
2926
|
: undefined;
|
|
2988
2927
|
|
|
2989
|
-
|
|
2990
|
-
state.row.status = 'completed';
|
|
2991
|
-
state.row.output = output;
|
|
2992
|
-
state.row.completionReason = verificationResult?.completionReason;
|
|
2993
|
-
state.row.completedAt = new Date().toISOString();
|
|
2994
|
-
await this.db.updateStep(state.row.id, {
|
|
2995
|
-
status: 'completed',
|
|
2928
|
+
return {
|
|
2996
2929
|
output,
|
|
2997
2930
|
completionReason: verificationResult?.completionReason,
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
2931
|
+
};
|
|
2932
|
+
},
|
|
2933
|
+
toCompletionResult: ({ output, completionReason }, attempt) => ({
|
|
2934
|
+
status: 'completed',
|
|
2935
|
+
output,
|
|
2936
|
+
completionReason,
|
|
2937
|
+
retries: attempt,
|
|
2938
|
+
exitCode: lastExitCode,
|
|
2939
|
+
exitSignal: lastExitSignal,
|
|
2940
|
+
}),
|
|
2941
|
+
onAttemptFailed: async (error) => {
|
|
2942
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
2943
|
+
lastCompletionReason = error instanceof WorkflowCompletionError ? error.completionReason : undefined;
|
|
2944
|
+
},
|
|
2945
|
+
getFailureResult: () => ({
|
|
2946
|
+
status: 'failed',
|
|
2947
|
+
output: '',
|
|
2948
|
+
error: lastError,
|
|
2949
|
+
retries: state.row.retryCount,
|
|
2950
|
+
exitCode: lastExitCode,
|
|
2951
|
+
exitSignal: lastExitSignal,
|
|
2952
|
+
completionReason: lastCompletionReason,
|
|
2953
|
+
}),
|
|
2954
|
+
});
|
|
3004
2955
|
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
'completed',
|
|
3009
|
-
state.row.completedAt,
|
|
3010
|
-
verificationResult?.completionReason
|
|
3011
|
-
);
|
|
3012
|
-
return;
|
|
3013
|
-
} catch (err) {
|
|
3014
|
-
lastError = err instanceof Error ? err.message : String(err);
|
|
3015
|
-
lastCompletionReason =
|
|
3016
|
-
err instanceof WorkflowCompletionError ? err.completionReason : undefined;
|
|
3017
|
-
}
|
|
2956
|
+
if (result.status === 'failed') {
|
|
2957
|
+
this.postToChannel(`**[${step.name}]** Failed: ${result.error ?? 'Unknown error'}`);
|
|
2958
|
+
throw new Error(`Step "${step.name}" failed: ${result.error ?? 'Unknown error'}`);
|
|
3018
2959
|
}
|
|
3019
|
-
|
|
3020
|
-
const errorMsg = lastError ?? 'Unknown error';
|
|
3021
|
-
this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
|
|
3022
|
-
await this.markStepFailed(
|
|
3023
|
-
state,
|
|
3024
|
-
errorMsg,
|
|
3025
|
-
runId,
|
|
3026
|
-
{ exitCode: lastExitCode, exitSignal: lastExitSignal },
|
|
3027
|
-
lastCompletionReason
|
|
3028
|
-
);
|
|
3029
|
-
throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
|
|
3030
2960
|
}
|
|
3031
2961
|
|
|
3032
2962
|
/**
|
|
@@ -3036,223 +2966,188 @@ export class WorkflowRunner {
|
|
|
3036
2966
|
*/
|
|
3037
2967
|
private async executeWorktreeStep(
|
|
3038
2968
|
step: WorkflowStep,
|
|
2969
|
+
state: StepState,
|
|
3039
2970
|
stepStates: Map<string, StepState>,
|
|
3040
|
-
runId: string
|
|
2971
|
+
runId: string,
|
|
2972
|
+
lifecycle: WorkflowStepLifecycleExecutor<StepState>
|
|
3041
2973
|
): Promise<void> {
|
|
3042
|
-
const state = stepStates.get(step.name);
|
|
3043
|
-
if (!state) throw new Error(`Step state not found: ${step.name}`);
|
|
3044
2974
|
let lastExitCode: number | undefined;
|
|
3045
2975
|
let lastExitSignal: string | undefined;
|
|
2976
|
+
let worktreeBranch = '';
|
|
2977
|
+
let createdBranch = false;
|
|
3046
2978
|
|
|
3047
|
-
|
|
2979
|
+
const result = await lifecycle.monitorStep(step, state, {
|
|
2980
|
+
startMessage: `**[${step.name}]** Started (worktree setup)`,
|
|
2981
|
+
execute: async () => {
|
|
2982
|
+
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
2983
|
+
const branch = this.interpolateStepTask(step.branch ?? '', stepOutputContext);
|
|
2984
|
+
const baseBranch = step.baseBranch
|
|
2985
|
+
? this.interpolateStepTask(step.baseBranch, stepOutputContext)
|
|
2986
|
+
: 'HEAD';
|
|
2987
|
+
const worktreePath = step.path
|
|
2988
|
+
? this.interpolateStepTask(step.path, stepOutputContext)
|
|
2989
|
+
: path.join('.worktrees', step.name);
|
|
2990
|
+
const createBranch = step.createBranch !== false;
|
|
2991
|
+
const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
|
|
2992
|
+
|
|
2993
|
+
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
2994
|
+
|
|
2995
|
+
if (!branch) {
|
|
2996
|
+
throw new Error('Worktree step missing required "branch" field');
|
|
2997
|
+
}
|
|
3048
2998
|
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
state.row.error = undefined;
|
|
3052
|
-
state.row.completionReason = undefined;
|
|
3053
|
-
state.row.startedAt = new Date().toISOString();
|
|
3054
|
-
await this.db.updateStep(state.row.id, {
|
|
3055
|
-
status: 'running',
|
|
3056
|
-
error: undefined,
|
|
3057
|
-
completionReason: undefined,
|
|
3058
|
-
startedAt: state.row.startedAt,
|
|
3059
|
-
updatedAt: new Date().toISOString(),
|
|
3060
|
-
});
|
|
3061
|
-
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
3062
|
-
this.postToChannel(`**[${step.name}]** Started (worktree setup)`);
|
|
3063
|
-
|
|
3064
|
-
// Resolve variables in branch name and path
|
|
3065
|
-
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
3066
|
-
const branch = this.interpolateStepTask(step.branch ?? '', stepOutputContext);
|
|
3067
|
-
const baseBranch = step.baseBranch
|
|
3068
|
-
? this.interpolateStepTask(step.baseBranch, stepOutputContext)
|
|
3069
|
-
: 'HEAD';
|
|
3070
|
-
const worktreePath = step.path
|
|
3071
|
-
? this.interpolateStepTask(step.path, stepOutputContext)
|
|
3072
|
-
: path.join('.worktrees', step.name);
|
|
3073
|
-
const createBranch = step.createBranch !== false;
|
|
3074
|
-
|
|
3075
|
-
// Resolve workdir for worktree steps (same as deterministic/agent steps)
|
|
3076
|
-
const stepCwd = this.resolveStepWorkdir(step) ?? this.cwd;
|
|
3077
|
-
this.beginStepEvidence(step.name, [stepCwd], state.row.startedAt);
|
|
3078
|
-
|
|
3079
|
-
if (!branch) {
|
|
3080
|
-
const errorMsg = 'Worktree step missing required "branch" field';
|
|
3081
|
-
await this.markStepFailed(state, errorMsg, runId);
|
|
3082
|
-
throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
|
|
3083
|
-
}
|
|
2999
|
+
const absoluteWorktreePath = path.resolve(stepCwd, worktreePath);
|
|
3000
|
+
let branchExists = false;
|
|
3084
3001
|
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
stdio: 'pipe',
|
|
3097
|
-
cwd: stepCwd,
|
|
3098
|
-
env: { ...process.env },
|
|
3099
|
-
});
|
|
3100
|
-
checkChild.on('close', (code) => {
|
|
3101
|
-
branchExists = code === 0;
|
|
3102
|
-
resolve();
|
|
3002
|
+
await new Promise<void>((resolve) => {
|
|
3003
|
+
const checkChild = cpSpawn('git', ['rev-parse', '--verify', '--quiet', branch], {
|
|
3004
|
+
stdio: 'pipe',
|
|
3005
|
+
cwd: stepCwd,
|
|
3006
|
+
env: filteredEnv(),
|
|
3007
|
+
});
|
|
3008
|
+
checkChild.on('close', (code) => {
|
|
3009
|
+
branchExists = code === 0;
|
|
3010
|
+
resolve();
|
|
3011
|
+
});
|
|
3012
|
+
checkChild.on('error', () => resolve());
|
|
3103
3013
|
});
|
|
3104
|
-
checkChild.on('error', () => resolve());
|
|
3105
|
-
});
|
|
3106
3014
|
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
} else {
|
|
3116
|
-
// Branch doesn't exist and we're not creating it
|
|
3117
|
-
const errorMsg = `Branch "${branch}" does not exist and createBranch is false`;
|
|
3118
|
-
await this.markStepFailed(state, errorMsg, runId);
|
|
3119
|
-
throw new Error(`Step "${step.name}" failed: ${errorMsg}`);
|
|
3120
|
-
}
|
|
3015
|
+
let worktreeArgs: string[];
|
|
3016
|
+
if (branchExists) {
|
|
3017
|
+
worktreeArgs = ['worktree', 'add', absoluteWorktreePath, branch];
|
|
3018
|
+
} else if (createBranch) {
|
|
3019
|
+
worktreeArgs = ['worktree', 'add', '-b', branch, absoluteWorktreePath, baseBranch];
|
|
3020
|
+
} else {
|
|
3021
|
+
throw new Error(`Branch "${branch}" does not exist and createBranch is false`);
|
|
3022
|
+
}
|
|
3121
3023
|
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
env: { ...process.env },
|
|
3131
|
-
});
|
|
3024
|
+
let commandStdout = '';
|
|
3025
|
+
let commandStderr = '';
|
|
3026
|
+
const output = await new Promise<string>((resolve, reject) => {
|
|
3027
|
+
const child = cpSpawn('git', worktreeArgs, {
|
|
3028
|
+
stdio: 'pipe',
|
|
3029
|
+
cwd: stepCwd,
|
|
3030
|
+
env: filteredEnv(),
|
|
3031
|
+
});
|
|
3132
3032
|
|
|
3133
|
-
|
|
3134
|
-
|
|
3033
|
+
const stdoutChunks: string[] = [];
|
|
3034
|
+
const stderrChunks: string[] = [];
|
|
3035
|
+
const abortSignal = this.abortController?.signal;
|
|
3036
|
+
let abortHandler: (() => void) | undefined;
|
|
3037
|
+
if (abortSignal && !abortSignal.aborted) {
|
|
3038
|
+
abortHandler = () => {
|
|
3039
|
+
child.kill('SIGTERM');
|
|
3040
|
+
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
3041
|
+
};
|
|
3042
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
3043
|
+
}
|
|
3135
3044
|
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
}
|
|
3045
|
+
let timedOut = false;
|
|
3046
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
3047
|
+
if (step.timeoutMs) {
|
|
3048
|
+
timer = setTimeout(() => {
|
|
3049
|
+
timedOut = true;
|
|
3050
|
+
child.kill('SIGTERM');
|
|
3051
|
+
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
3052
|
+
}, step.timeoutMs);
|
|
3053
|
+
}
|
|
3146
3054
|
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
if (step.timeoutMs) {
|
|
3151
|
-
timer = setTimeout(() => {
|
|
3152
|
-
timedOut = true;
|
|
3153
|
-
child.kill('SIGTERM');
|
|
3154
|
-
setTimeout(() => child.kill('SIGKILL'), 5000);
|
|
3155
|
-
}, step.timeoutMs);
|
|
3156
|
-
}
|
|
3055
|
+
child.stdout?.on('data', (chunk: Buffer) => {
|
|
3056
|
+
stdoutChunks.push(chunk.toString());
|
|
3057
|
+
});
|
|
3157
3058
|
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3059
|
+
child.stderr?.on('data', (chunk: Buffer) => {
|
|
3060
|
+
stderrChunks.push(chunk.toString());
|
|
3061
|
+
});
|
|
3161
3062
|
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3063
|
+
child.on('close', (code, signal) => {
|
|
3064
|
+
if (timer) clearTimeout(timer);
|
|
3065
|
+
if (abortHandler && abortSignal) {
|
|
3066
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
3067
|
+
}
|
|
3165
3068
|
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
}
|
|
3069
|
+
if (abortSignal?.aborted) {
|
|
3070
|
+
reject(new Error(`Step "${step.name}" aborted`));
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3171
3073
|
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3074
|
+
if (timedOut) {
|
|
3075
|
+
reject(
|
|
3076
|
+
new Error(`Step "${step.name}" timed out (no step timeout set, check global swarm.timeoutMs)`)
|
|
3077
|
+
);
|
|
3078
|
+
return;
|
|
3079
|
+
}
|
|
3176
3080
|
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
return;
|
|
3182
|
-
}
|
|
3081
|
+
commandStdout = stdoutChunks.join('');
|
|
3082
|
+
commandStderr = stderrChunks.join('');
|
|
3083
|
+
lastExitCode = code ?? undefined;
|
|
3084
|
+
lastExitSignal = signal ?? undefined;
|
|
3183
3085
|
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3086
|
+
if (code !== 0 && code !== null) {
|
|
3087
|
+
reject(
|
|
3088
|
+
new Error(
|
|
3089
|
+
`git worktree add failed with exit code ${code}${commandStderr ? `: ${commandStderr.slice(0, 500)}` : ''}`
|
|
3090
|
+
)
|
|
3091
|
+
);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3191
3094
|
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
new Error(
|
|
3195
|
-
`git worktree add failed with exit code ${code}${stderr ? `: ${stderr.slice(0, 500)}` : ''}`
|
|
3196
|
-
)
|
|
3197
|
-
);
|
|
3198
|
-
return;
|
|
3199
|
-
}
|
|
3095
|
+
resolve(absoluteWorktreePath);
|
|
3096
|
+
});
|
|
3200
3097
|
|
|
3201
|
-
|
|
3202
|
-
|
|
3098
|
+
child.on('error', (err) => {
|
|
3099
|
+
if (timer) clearTimeout(timer);
|
|
3100
|
+
if (abortHandler && abortSignal) {
|
|
3101
|
+
abortSignal.removeEventListener('abort', abortHandler);
|
|
3102
|
+
}
|
|
3103
|
+
reject(new Error(`Failed to execute git worktree command: ${err.message}`));
|
|
3104
|
+
});
|
|
3203
3105
|
});
|
|
3204
3106
|
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
step.name,
|
|
3215
|
-
{
|
|
3216
|
-
stdout: commandStdout || output,
|
|
3217
|
-
stderr: commandStderr,
|
|
3218
|
-
combined: [commandStdout || output, commandStderr].filter(Boolean).join('\n'),
|
|
3219
|
-
},
|
|
3220
|
-
{ exitCode: commandExitCode, exitSignal: commandExitSignal }
|
|
3221
|
-
);
|
|
3107
|
+
this.captureStepTerminalEvidence(
|
|
3108
|
+
step.name,
|
|
3109
|
+
{
|
|
3110
|
+
stdout: commandStdout || output,
|
|
3111
|
+
stderr: commandStderr,
|
|
3112
|
+
combined: [commandStdout || output, commandStderr].filter(Boolean).join('\n'),
|
|
3113
|
+
},
|
|
3114
|
+
{ exitCode: lastExitCode, exitSignal: lastExitSignal }
|
|
3115
|
+
);
|
|
3222
3116
|
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3117
|
+
worktreeBranch = branch;
|
|
3118
|
+
createdBranch = !branchExists && createBranch;
|
|
3119
|
+
return { output };
|
|
3120
|
+
},
|
|
3121
|
+
toCompletionResult: ({ output }, attempt) => ({
|
|
3228
3122
|
status: 'completed',
|
|
3229
3123
|
output,
|
|
3230
|
-
|
|
3231
|
-
updatedAt: new Date().toISOString(),
|
|
3232
|
-
});
|
|
3233
|
-
|
|
3234
|
-
// Persist step output
|
|
3235
|
-
await this.persistStepOutput(runId, step.name, output);
|
|
3236
|
-
|
|
3237
|
-
this.emit({ type: 'step:completed', runId, stepName: step.name, output });
|
|
3238
|
-
this.postToChannel(
|
|
3239
|
-
`**[${step.name}]** Worktree created at: ${output}\n Branch: ${branch}${!branchExists && createBranch ? ' (created)' : ''}`
|
|
3240
|
-
);
|
|
3241
|
-
this.recordStepToolSideEffect(step.name, {
|
|
3242
|
-
type: 'worktree_created',
|
|
3243
|
-
detail: `Worktree created at ${output}`,
|
|
3244
|
-
raw: { branch, createdBranch: !branchExists && createBranch },
|
|
3245
|
-
});
|
|
3246
|
-
this.finalizeStepEvidence(step.name, 'completed', state.row.completedAt);
|
|
3247
|
-
} catch (err) {
|
|
3248
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
3249
|
-
this.postToChannel(`**[${step.name}]** Failed: ${errorMsg}`);
|
|
3250
|
-
await this.markStepFailed(state, errorMsg, runId, {
|
|
3124
|
+
retries: attempt,
|
|
3251
3125
|
exitCode: lastExitCode,
|
|
3252
3126
|
exitSignal: lastExitSignal,
|
|
3253
|
-
})
|
|
3254
|
-
|
|
3127
|
+
}),
|
|
3128
|
+
getFailureResult: (error) => ({
|
|
3129
|
+
status: 'failed',
|
|
3130
|
+
output: '',
|
|
3131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3132
|
+
retries: state.row.retryCount,
|
|
3133
|
+
exitCode: lastExitCode,
|
|
3134
|
+
exitSignal: lastExitSignal,
|
|
3135
|
+
}),
|
|
3136
|
+
});
|
|
3137
|
+
|
|
3138
|
+
if (result.status === 'failed') {
|
|
3139
|
+
this.postToChannel(`**[${step.name}]** Failed: ${result.error ?? 'Unknown error'}`);
|
|
3140
|
+
throw new Error(`Step "${step.name}" failed: ${result.error ?? 'Unknown error'}`);
|
|
3255
3141
|
}
|
|
3142
|
+
|
|
3143
|
+
this.postToChannel(
|
|
3144
|
+
`**[${step.name}]** Worktree created at: ${result.output}\n Branch: ${worktreeBranch}${createdBranch ? ' (created)' : ''}`
|
|
3145
|
+
);
|
|
3146
|
+
this.recordStepToolSideEffect(step.name, {
|
|
3147
|
+
type: 'worktree_created',
|
|
3148
|
+
detail: `Worktree created at ${result.output}`,
|
|
3149
|
+
raw: { branch: worktreeBranch, createdBranch },
|
|
3150
|
+
});
|
|
3256
3151
|
}
|
|
3257
3152
|
|
|
3258
3153
|
/**
|
|
@@ -3260,69 +3155,56 @@ export class WorkflowRunner {
|
|
|
3260
3155
|
*/
|
|
3261
3156
|
private async executeIntegrationStep(
|
|
3262
3157
|
step: WorkflowStep,
|
|
3158
|
+
state: StepState,
|
|
3263
3159
|
stepStates: Map<string, StepState>,
|
|
3264
|
-
runId: string
|
|
3160
|
+
runId: string,
|
|
3161
|
+
lifecycle: WorkflowStepLifecycleExecutor<StepState>
|
|
3265
3162
|
): Promise<void> {
|
|
3266
|
-
const
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
state.row.completionReason = undefined;
|
|
3275
|
-
state.row.startedAt = new Date().toISOString();
|
|
3276
|
-
await this.db.updateStep(state.row.id, {
|
|
3277
|
-
status: 'running',
|
|
3278
|
-
error: undefined,
|
|
3279
|
-
completionReason: undefined,
|
|
3280
|
-
startedAt: state.row.startedAt,
|
|
3281
|
-
updatedAt: new Date().toISOString(),
|
|
3282
|
-
});
|
|
3283
|
-
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
3284
|
-
this.postToChannel(`**[${step.name}]** Started (integration: ${step.integration}.${step.action})`);
|
|
3285
|
-
|
|
3286
|
-
// Resolve {{steps.X.output}} in params
|
|
3287
|
-
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
3288
|
-
const resolvedParams: Record<string, string> = {};
|
|
3289
|
-
for (const [key, value] of Object.entries(step.params ?? {})) {
|
|
3290
|
-
resolvedParams[key] = this.interpolateStepTask(value, stepOutputContext);
|
|
3291
|
-
}
|
|
3163
|
+
const result = await lifecycle.monitorStep(step, state, {
|
|
3164
|
+
startMessage: `**[${step.name}]** Started (integration: ${step.integration}.${step.action})`,
|
|
3165
|
+
execute: async () => {
|
|
3166
|
+
const stepOutputContext = this.buildStepOutputContext(stepStates, runId);
|
|
3167
|
+
const resolvedParams: Record<string, string> = {};
|
|
3168
|
+
for (const [key, value] of Object.entries(step.params ?? {})) {
|
|
3169
|
+
resolvedParams[key] = this.interpolateStepTask(value, stepOutputContext);
|
|
3170
|
+
}
|
|
3292
3171
|
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
}
|
|
3172
|
+
if (!this.executor?.executeIntegrationStep) {
|
|
3173
|
+
throw new Error(
|
|
3174
|
+
`Integration steps require a cloud executor. Step "${step.name}" cannot run locally. ` +
|
|
3175
|
+
`Use "cloud run" to execute workflows with integration steps.`
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3300
3178
|
|
|
3301
|
-
|
|
3179
|
+
const integrationResult = await this.executor.executeIntegrationStep(step, resolvedParams, {
|
|
3180
|
+
workspaceId: this.workspaceId,
|
|
3181
|
+
});
|
|
3302
3182
|
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3183
|
+
if (!integrationResult.success) {
|
|
3184
|
+
throw new Error(`Integration step "${step.name}" failed: ${integrationResult.output}`);
|
|
3185
|
+
}
|
|
3306
3186
|
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
state.row.completedAt = new Date().toISOString();
|
|
3311
|
-
await this.db.updateStep(state.row.id, {
|
|
3187
|
+
return { output: integrationResult.output };
|
|
3188
|
+
},
|
|
3189
|
+
toCompletionResult: ({ output }, attempt) => ({
|
|
3312
3190
|
status: 'completed',
|
|
3313
|
-
output
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3191
|
+
output,
|
|
3192
|
+
retries: attempt,
|
|
3193
|
+
}),
|
|
3194
|
+
getFailureResult: (error) => ({
|
|
3195
|
+
status: 'failed',
|
|
3196
|
+
output: '',
|
|
3197
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3198
|
+
retries: state.row.retryCount,
|
|
3199
|
+
}),
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
if (result.status === 'failed') {
|
|
3203
|
+
this.postToChannel(`**[${step.name}]** Failed: ${result.error ?? 'Unknown error'}`);
|
|
3204
|
+
throw new Error(`Step "${step.name}" failed: ${result.error ?? 'Unknown error'}`);
|
|
3325
3205
|
}
|
|
3206
|
+
|
|
3207
|
+
this.postToChannel(`**[${step.name}]** Completed (integration: ${step.integration}.${step.action})`);
|
|
3326
3208
|
}
|
|
3327
3209
|
|
|
3328
3210
|
/**
|
|
@@ -3367,7 +3249,11 @@ export class WorkflowRunner {
|
|
|
3367
3249
|
const output = await executeApiStep(
|
|
3368
3250
|
specialistDef.constraints?.model ?? 'claude-sonnet-4-20250514',
|
|
3369
3251
|
resolvedTask,
|
|
3370
|
-
{
|
|
3252
|
+
{
|
|
3253
|
+
envSecrets: this.envSecrets,
|
|
3254
|
+
skills: specialistDef.skills,
|
|
3255
|
+
defaultMaxTokens: specialistDef.constraints?.maxTokens,
|
|
3256
|
+
}
|
|
3371
3257
|
);
|
|
3372
3258
|
|
|
3373
3259
|
state.row.status = 'completed';
|
|
@@ -3402,7 +3288,8 @@ export class WorkflowRunner {
|
|
|
3402
3288
|
const usesOwnerFlow = specialistDef.interactive !== false;
|
|
3403
3289
|
const currentPattern = this.currentConfig?.swarm?.pattern ?? '';
|
|
3404
3290
|
const isHubPattern = WorkflowRunner.HUB_PATTERNS.has(currentPattern);
|
|
3405
|
-
const usesAutoHardening =
|
|
3291
|
+
const usesAutoHardening =
|
|
3292
|
+
usesOwnerFlow && isHubPattern && !this.isExplicitInteractiveWorker(specialistDef);
|
|
3406
3293
|
const ownerDef = usesAutoHardening ? this.resolveAutoStepOwner(specialistDef, agentMap) : specialistDef;
|
|
3407
3294
|
// Reviewer resolution is deferred to just before the review gate runs (see below)
|
|
3408
3295
|
// so that activeReviewers is up-to-date for concurrent steps.
|
|
@@ -3479,9 +3366,7 @@ export class WorkflowRunner {
|
|
|
3479
3366
|
updatedAt: new Date().toISOString(),
|
|
3480
3367
|
});
|
|
3481
3368
|
this.emit({ type: 'step:started', runId, stepName: step.name });
|
|
3482
|
-
this.log(
|
|
3483
|
-
`[${step.name}] Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`
|
|
3484
|
-
);
|
|
3369
|
+
this.log(`[${step.name}] Started (owner: ${ownerDef.name}, specialist: ${specialistDef.name})`);
|
|
3485
3370
|
this.initializeStepSignalParticipants(step.name, ownerDef.name, specialistDef.name);
|
|
3486
3371
|
await this.trajectory?.stepStarted(step, ownerDef.name, {
|
|
3487
3372
|
role: usesDedicatedOwner ? 'owner' : 'specialist',
|
|
@@ -3569,13 +3454,20 @@ export class WorkflowRunner {
|
|
|
3569
3454
|
ownerElapsed = result.ownerElapsed;
|
|
3570
3455
|
completionReason = result.completionReason;
|
|
3571
3456
|
} else {
|
|
3572
|
-
const ownerTask = this.injectStepOwnerContract(
|
|
3457
|
+
const ownerTask = this.injectStepOwnerContract(
|
|
3458
|
+
step,
|
|
3459
|
+
resolvedTask,
|
|
3460
|
+
effectiveOwner,
|
|
3461
|
+
effectiveSpecialist
|
|
3462
|
+
);
|
|
3573
3463
|
const explicitInteractiveWorker = this.isExplicitInteractiveWorker(effectiveOwner);
|
|
3574
3464
|
let explicitWorkerHandle: Agent | undefined;
|
|
3575
3465
|
let explicitWorkerCompleted = false;
|
|
3576
3466
|
let explicitWorkerOutput = '';
|
|
3577
3467
|
|
|
3578
|
-
this.log(
|
|
3468
|
+
this.log(
|
|
3469
|
+
`[${step.name}] Spawning owner "${effectiveOwner.name}" (cli: ${effectiveOwner.cli})${step.workdir ? ` [workdir: ${step.workdir}]` : ''}`
|
|
3470
|
+
);
|
|
3579
3471
|
const resolvedStep = { ...step, task: ownerTask };
|
|
3580
3472
|
const ownerStartTime = Date.now();
|
|
3581
3473
|
const spawnResult = this.executor
|
|
@@ -3583,7 +3475,7 @@ export class WorkflowRunner {
|
|
|
3583
3475
|
: await this.spawnAndWait(effectiveOwner, resolvedStep, timeoutMs, {
|
|
3584
3476
|
evidenceStepName: step.name,
|
|
3585
3477
|
evidenceRole: usesOwnerFlow ? 'owner' : 'specialist',
|
|
3586
|
-
preserveOnIdle:
|
|
3478
|
+
preserveOnIdle: !isHubPattern || !this.isLeadLikeAgent(effectiveOwner) ? false : undefined,
|
|
3587
3479
|
logicalName: effectiveOwner.name,
|
|
3588
3480
|
onSpawned: explicitInteractiveWorker
|
|
3589
3481
|
? ({ agent }) => {
|
|
@@ -3614,7 +3506,7 @@ export class WorkflowRunner {
|
|
|
3614
3506
|
? effectiveOwner.interactive === false
|
|
3615
3507
|
? undefined
|
|
3616
3508
|
: ownerTask
|
|
3617
|
-
: spawnResult.promptTaskText ?? ownerTask;
|
|
3509
|
+
: (spawnResult.promptTaskText ?? ownerTask);
|
|
3618
3510
|
lastExitCode = typeof spawnResult === 'string' ? undefined : spawnResult.exitCode;
|
|
3619
3511
|
lastExitSignal = typeof spawnResult === 'string' ? undefined : spawnResult.exitSignal;
|
|
3620
3512
|
ownerElapsed = Date.now() - ownerStartTime;
|
|
@@ -3745,19 +3637,20 @@ export class WorkflowRunner {
|
|
|
3745
3637
|
// Persist step output to disk so it survives restarts and is inspectable
|
|
3746
3638
|
await this.persistStepOutput(runId, step.name, combinedOutput);
|
|
3747
3639
|
|
|
3748
|
-
this.emit({
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3640
|
+
this.emit({
|
|
3641
|
+
type: 'step:completed',
|
|
3642
|
+
runId,
|
|
3643
|
+
stepName: step.name,
|
|
3644
|
+
output: combinedOutput,
|
|
3645
|
+
exitCode: lastExitCode,
|
|
3646
|
+
exitSignal: lastExitSignal,
|
|
3647
|
+
});
|
|
3648
|
+
this.finalizeStepEvidence(step.name, 'completed', state.row.completedAt, completionReason);
|
|
3755
3649
|
await this.trajectory?.stepCompleted(step, combinedOutput, attempt + 1);
|
|
3756
3650
|
return;
|
|
3757
3651
|
} catch (err) {
|
|
3758
3652
|
lastError = err instanceof Error ? err.message : String(err);
|
|
3759
|
-
lastCompletionReason =
|
|
3760
|
-
err instanceof WorkflowCompletionError ? err.completionReason : undefined;
|
|
3653
|
+
lastCompletionReason = err instanceof WorkflowCompletionError ? err.completionReason : undefined;
|
|
3761
3654
|
if (lastCompletionReason === 'retry_requested_by_owner' && attempt >= maxRetries) {
|
|
3762
3655
|
lastError = this.buildOwnerRetryBudgetExceededMessage(step.name, maxRetries, lastError);
|
|
3763
3656
|
}
|
|
@@ -3795,10 +3688,16 @@ export class WorkflowRunner {
|
|
|
3795
3688
|
verificationValue,
|
|
3796
3689
|
});
|
|
3797
3690
|
this.postToChannel(`**[${step.name}]** Failed: ${lastError ?? 'Unknown error'}`);
|
|
3798
|
-
await this.markStepFailed(
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3691
|
+
await this.markStepFailed(
|
|
3692
|
+
state,
|
|
3693
|
+
lastError ?? 'Unknown error',
|
|
3694
|
+
runId,
|
|
3695
|
+
{
|
|
3696
|
+
exitCode: lastExitCode,
|
|
3697
|
+
exitSignal: lastExitSignal,
|
|
3698
|
+
},
|
|
3699
|
+
lastCompletionReason
|
|
3700
|
+
);
|
|
3802
3701
|
throw new Error(
|
|
3803
3702
|
`Step "${step.name}" failed after ${maxRetries} retries: ${lastError ?? 'Unknown error'}`
|
|
3804
3703
|
);
|
|
@@ -3814,9 +3713,7 @@ export class WorkflowRunner {
|
|
|
3814
3713
|
const normalizedDecision = ownerDecisionError?.startsWith(prefix)
|
|
3815
3714
|
? ownerDecisionError.slice(prefix.length).trim()
|
|
3816
3715
|
: ownerDecisionError?.trim();
|
|
3817
|
-
const decisionSuffix = normalizedDecision
|
|
3818
|
-
? ` Latest owner decision: ${normalizedDecision}`
|
|
3819
|
-
: '';
|
|
3716
|
+
const decisionSuffix = normalizedDecision ? ` Latest owner decision: ${normalizedDecision}` : '';
|
|
3820
3717
|
|
|
3821
3718
|
if (maxRetries === 0) {
|
|
3822
3719
|
return (
|
|
@@ -4025,13 +3922,7 @@ export class WorkflowRunner {
|
|
|
4025
3922
|
}
|
|
4026
3923
|
},
|
|
4027
3924
|
onChunk: ({ agentName, chunk }) => {
|
|
4028
|
-
this.forwardAgentChunkToChannel(
|
|
4029
|
-
step.name,
|
|
4030
|
-
'Worker',
|
|
4031
|
-
agentName,
|
|
4032
|
-
chunk,
|
|
4033
|
-
supervised.specialist.name
|
|
4034
|
-
);
|
|
3925
|
+
this.forwardAgentChunkToChannel(step.name, 'Worker', agentName, chunk, supervised.specialist.name);
|
|
4035
3926
|
},
|
|
4036
3927
|
}).catch((error) => {
|
|
4037
3928
|
if (!workerSpawned) {
|
|
@@ -4053,11 +3944,7 @@ export class WorkflowRunner {
|
|
|
4053
3944
|
});
|
|
4054
3945
|
if (
|
|
4055
3946
|
step.verification?.type === 'output_contains' &&
|
|
4056
|
-
this.outputContainsVerificationToken(
|
|
4057
|
-
result.output,
|
|
4058
|
-
step.verification.value,
|
|
4059
|
-
result.promptTaskText
|
|
4060
|
-
)
|
|
3947
|
+
this.outputContainsVerificationToken(result.output, step.verification.value, result.promptTaskText)
|
|
4061
3948
|
) {
|
|
4062
3949
|
this.log(
|
|
4063
3950
|
`[${step.name}] Verification gate observed: output contains ${JSON.stringify(step.verification.value)}`
|
|
@@ -4340,9 +4227,7 @@ export class WorkflowRunner {
|
|
|
4340
4227
|
const evidenceReason = this.judgeOwnerCompletionByEvidence(step.name, ownerOutput);
|
|
4341
4228
|
if (evidenceReason) {
|
|
4342
4229
|
if (!hasMarker) {
|
|
4343
|
-
this.log(
|
|
4344
|
-
`[${step.name}] Evidence-based completion resolved without legacy STEP_COMPLETE marker`
|
|
4345
|
-
);
|
|
4230
|
+
this.log(`[${step.name}] Evidence-based completion resolved without legacy STEP_COMPLETE marker`);
|
|
4346
4231
|
}
|
|
4347
4232
|
return {
|
|
4348
4233
|
completionReason: 'completed_by_evidence',
|
|
@@ -4354,7 +4239,12 @@ export class WorkflowRunner {
|
|
|
4354
4239
|
// Process-exit fallback: if the agent exited cleanly (code 0) and verification
|
|
4355
4240
|
// passes (or no verification is configured), infer completion rather than failing.
|
|
4356
4241
|
// This reduces dependence on agents posting exact coordination signals.
|
|
4357
|
-
const processExitFallback = this.tryProcessExitFallback(
|
|
4242
|
+
const processExitFallback = this.tryProcessExitFallback(
|
|
4243
|
+
step,
|
|
4244
|
+
specialistOutput,
|
|
4245
|
+
verificationTaskText,
|
|
4246
|
+
ownerOutput
|
|
4247
|
+
);
|
|
4358
4248
|
if (processExitFallback) {
|
|
4359
4249
|
this.log(
|
|
4360
4250
|
`[${step.name}] Completion inferred from clean process exit (code 0)` +
|
|
@@ -4384,13 +4274,9 @@ export class WorkflowRunner {
|
|
|
4384
4274
|
}
|
|
4385
4275
|
}
|
|
4386
4276
|
|
|
4387
|
-
private hasOwnerCompletionMarker(
|
|
4388
|
-
step: WorkflowStep,
|
|
4389
|
-
output: string,
|
|
4390
|
-
injectedTaskText: string
|
|
4391
|
-
): boolean {
|
|
4277
|
+
private hasOwnerCompletionMarker(step: WorkflowStep, output: string, injectedTaskText: string): boolean {
|
|
4392
4278
|
const marker = `STEP_COMPLETE:${step.name}`;
|
|
4393
|
-
const strippedOutput =
|
|
4279
|
+
const strippedOutput = stripInjectedTaskEcho(output, injectedTaskText);
|
|
4394
4280
|
if (strippedOutput.includes(marker)) {
|
|
4395
4281
|
return true;
|
|
4396
4282
|
}
|
|
@@ -4483,36 +4369,11 @@ export class WorkflowRunner {
|
|
|
4483
4369
|
.join('\n');
|
|
4484
4370
|
}
|
|
4485
4371
|
|
|
4486
|
-
private
|
|
4487
|
-
if (!injectedTaskText) {
|
|
4488
|
-
return output;
|
|
4489
|
-
}
|
|
4490
|
-
|
|
4491
|
-
const candidates = [
|
|
4492
|
-
injectedTaskText,
|
|
4493
|
-
injectedTaskText.replace(/\r\n/g, '\n'),
|
|
4494
|
-
injectedTaskText.replace(/\n/g, '\r\n'),
|
|
4495
|
-
].filter((candidate, index, all) => candidate.length > 0 && all.indexOf(candidate) === index);
|
|
4496
|
-
|
|
4497
|
-
for (const candidate of candidates) {
|
|
4498
|
-
const start = output.indexOf(candidate);
|
|
4499
|
-
if (start !== -1) {
|
|
4500
|
-
return output.slice(0, start) + output.slice(start + candidate.length);
|
|
4501
|
-
}
|
|
4502
|
-
}
|
|
4503
|
-
|
|
4504
|
-
return output;
|
|
4505
|
-
}
|
|
4506
|
-
|
|
4507
|
-
private outputContainsVerificationToken(
|
|
4508
|
-
output: string,
|
|
4509
|
-
token: string,
|
|
4510
|
-
injectedTaskText?: string
|
|
4511
|
-
): boolean {
|
|
4372
|
+
private outputContainsVerificationToken(output: string, token: string, injectedTaskText?: string): boolean {
|
|
4512
4373
|
if (!token) {
|
|
4513
4374
|
return false;
|
|
4514
4375
|
}
|
|
4515
|
-
return
|
|
4376
|
+
return stripInjectedTaskEcho(output, injectedTaskText).includes(token);
|
|
4516
4377
|
}
|
|
4517
4378
|
|
|
4518
4379
|
private prepareInteractiveSpawnTask(
|
|
@@ -4565,13 +4426,9 @@ export class WorkflowRunner {
|
|
|
4565
4426
|
if (!sanitized) return null;
|
|
4566
4427
|
|
|
4567
4428
|
const hasExplicitSelfRelease =
|
|
4568
|
-
/Calling\s+(?:[\w.-]+\.)?remove_agent\(\{[^<\n]*"reason":"task completed"/i.test(
|
|
4569
|
-
sanitized
|
|
4570
|
-
);
|
|
4429
|
+
/Calling\s+(?:[\w.-]+\.)?remove_agent\(\{[^<\n]*"reason":"task completed"/i.test(sanitized);
|
|
4571
4430
|
const hasPositiveConclusion =
|
|
4572
|
-
/\b(complete(?:d)?|done|verified|looks correct|safe handoff|artifact verified)\b/i.test(
|
|
4573
|
-
sanitized
|
|
4574
|
-
) ||
|
|
4431
|
+
/\b(complete(?:d)?|done|verified|looks correct|safe handoff|artifact verified)\b/i.test(sanitized) ||
|
|
4575
4432
|
/\bartifacts?\b.*\b(correct|verified|complete)\b/i.test(sanitized) ||
|
|
4576
4433
|
hasExplicitSelfRelease;
|
|
4577
4434
|
const evidence = this.getStepCompletionEvidence(stepName);
|
|
@@ -4616,15 +4473,18 @@ export class WorkflowRunner {
|
|
|
4616
4473
|
if (gracePeriodMs === 0) return null;
|
|
4617
4474
|
|
|
4618
4475
|
// Never infer completion when the owner explicitly requested retry/fail/clarification.
|
|
4619
|
-
if (
|
|
4476
|
+
if (
|
|
4477
|
+
ownerOutput &&
|
|
4478
|
+
/OWNER_DECISION:\s*(?:INCOMPLETE_RETRY|INCOMPLETE_FAIL|NEEDS_CLARIFICATION)\b/i.test(ownerOutput)
|
|
4479
|
+
) {
|
|
4620
4480
|
return null;
|
|
4621
4481
|
}
|
|
4622
4482
|
|
|
4623
4483
|
const evidence = this.getStepCompletionEvidence(step.name);
|
|
4624
|
-
const hasCleanExit =
|
|
4625
|
-
(
|
|
4626
|
-
signal.kind === 'process_exit' && signal.value === '0'
|
|
4627
|
-
|
|
4484
|
+
const hasCleanExit =
|
|
4485
|
+
evidence?.coordinationSignals.some(
|
|
4486
|
+
(signal) => signal.kind === 'process_exit' && signal.value === '0'
|
|
4487
|
+
) ?? false;
|
|
4628
4488
|
|
|
4629
4489
|
if (!hasCleanExit) return null;
|
|
4630
4490
|
|
|
@@ -4744,9 +4604,7 @@ export class WorkflowRunner {
|
|
|
4744
4604
|
let reviewerHandle: Agent | undefined;
|
|
4745
4605
|
let reviewerReleased = false;
|
|
4746
4606
|
let reviewOutput = '';
|
|
4747
|
-
let completedReview:
|
|
4748
|
-
| { decision: 'approved' | 'rejected'; reason?: string }
|
|
4749
|
-
| undefined;
|
|
4607
|
+
let completedReview: { decision: 'approved' | 'rejected'; reason?: string } | undefined;
|
|
4750
4608
|
let reviewCompletionPromise: Promise<void> | undefined;
|
|
4751
4609
|
const reviewCompletionStarted = { value: false };
|
|
4752
4610
|
|
|
@@ -4785,9 +4643,7 @@ export class WorkflowRunner {
|
|
|
4785
4643
|
const message = error instanceof Error ? error.message : String(error);
|
|
4786
4644
|
if (/\btimed out\b/i.test(message)) {
|
|
4787
4645
|
this.log(`[${step.name}] Review safety backstop timeout fired after ${safetyTimeoutMs}ms`);
|
|
4788
|
-
throw new Error(
|
|
4789
|
-
`Step "${step.name}" review safety backstop timed out after ${safetyTimeoutMs}ms`
|
|
4790
|
-
);
|
|
4646
|
+
throw new Error(`Step "${step.name}" review safety backstop timed out after ${safetyTimeoutMs}ms`);
|
|
4791
4647
|
}
|
|
4792
4648
|
throw error;
|
|
4793
4649
|
}
|
|
@@ -4995,17 +4851,10 @@ export class WorkflowRunner {
|
|
|
4995
4851
|
task: string,
|
|
4996
4852
|
extraArgs: string[] = []
|
|
4997
4853
|
): { cmd: string; args: string[] } {
|
|
4998
|
-
|
|
4999
|
-
throw new Error('cli "api" uses direct API calls, not a subprocess command');
|
|
5000
|
-
}
|
|
5001
|
-
const resolvedCli: AgentCli = cli === 'cursor' ? resolveCursorCli() : cli;
|
|
5002
|
-
const def = getCliDefinition(resolvedCli);
|
|
5003
|
-
if (!def || def.binaries.length === 0) {
|
|
5004
|
-
throw new Error(`Unknown or non-executable CLI: ${resolvedCli}`);
|
|
5005
|
-
}
|
|
4854
|
+
const [cmd, ...args] = buildProcessCommand(cli, extraArgs, task);
|
|
5006
4855
|
return {
|
|
5007
|
-
cmd
|
|
5008
|
-
args
|
|
4856
|
+
cmd,
|
|
4857
|
+
args,
|
|
5009
4858
|
};
|
|
5010
4859
|
}
|
|
5011
4860
|
|
|
@@ -5130,11 +4979,15 @@ export class WorkflowRunner {
|
|
|
5130
4979
|
const stderrChunks: string[] = [];
|
|
5131
4980
|
|
|
5132
4981
|
try {
|
|
5133
|
-
const {
|
|
5134
|
-
|
|
4982
|
+
const {
|
|
4983
|
+
stdout: output,
|
|
4984
|
+
exitCode,
|
|
4985
|
+
exitSignal,
|
|
4986
|
+
} = await new Promise<{ stdout: string; exitCode?: number; exitSignal?: string }>((resolve, reject) => {
|
|
4987
|
+
const child = spawnProcess([cmd, ...args], {
|
|
5135
4988
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
5136
4989
|
cwd: this.resolveEffectiveCwd(step, agentDef),
|
|
5137
|
-
env: this.getRelayEnv() ??
|
|
4990
|
+
env: this.getRelayEnv() ?? filteredEnv(),
|
|
5138
4991
|
});
|
|
5139
4992
|
|
|
5140
4993
|
// Update workers.json with PID now that we have it
|
|
@@ -5250,14 +5103,11 @@ export class WorkflowRunner {
|
|
|
5250
5103
|
const stderr = stderrChunks.join('');
|
|
5251
5104
|
const combinedOutput = stdout + stderr;
|
|
5252
5105
|
this.lastFailedStepOutput.set(step.name, combinedOutput);
|
|
5253
|
-
this.captureStepTerminalEvidence(
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
combined: combinedOutput,
|
|
5259
|
-
}
|
|
5260
|
-
);
|
|
5106
|
+
this.captureStepTerminalEvidence(step.name, {
|
|
5107
|
+
stdout,
|
|
5108
|
+
stderr,
|
|
5109
|
+
combined: combinedOutput,
|
|
5110
|
+
});
|
|
5261
5111
|
stopHeartbeat?.();
|
|
5262
5112
|
logStream.end();
|
|
5263
5113
|
this.unregisterWorker(agentName);
|
|
@@ -5485,18 +5335,16 @@ export class WorkflowRunner {
|
|
|
5485
5335
|
{ allowFailure: true }
|
|
5486
5336
|
);
|
|
5487
5337
|
if (verificationResult.passed) {
|
|
5488
|
-
this.log(
|
|
5489
|
-
`[${step.name}] Agent timed out but verification passed — treating as complete`
|
|
5490
|
-
);
|
|
5338
|
+
this.log(`[${step.name}] Agent timed out but verification passed — treating as complete`);
|
|
5491
5339
|
this.postToChannel(
|
|
5492
5340
|
`**[${step.name}]** Agent idle after completing work — verification passed, releasing`
|
|
5493
5341
|
);
|
|
5494
|
-
await agent.release();
|
|
5342
|
+
await agent.release().catch(() => undefined);
|
|
5495
5343
|
timeoutRecovered = true;
|
|
5496
5344
|
}
|
|
5497
5345
|
}
|
|
5498
5346
|
if (!timeoutRecovered) {
|
|
5499
|
-
await agent.release();
|
|
5347
|
+
await agent.release().catch(() => undefined);
|
|
5500
5348
|
throw new Error(`Step "${step.name}" timed out after ${timeoutMs ?? 'unknown'}ms`);
|
|
5501
5349
|
}
|
|
5502
5350
|
}
|
|
@@ -5614,8 +5462,7 @@ export class WorkflowRunner {
|
|
|
5614
5462
|
const nameLC = agentDef.name.toLowerCase();
|
|
5615
5463
|
return [...WorkflowRunner.HUB_ROLES].some(
|
|
5616
5464
|
(hubRole) =>
|
|
5617
|
-
new RegExp(`\\b${hubRole}\\b`, 'i').test(nameLC) ||
|
|
5618
|
-
new RegExp(`\\b${hubRole}\\b`, 'i').test(role)
|
|
5465
|
+
new RegExp(`\\b${hubRole}\\b`, 'i').test(nameLC) || new RegExp(`\\b${hubRole}\\b`, 'i').test(role)
|
|
5619
5466
|
);
|
|
5620
5467
|
}
|
|
5621
5468
|
|
|
@@ -5676,18 +5523,16 @@ export class WorkflowRunner {
|
|
|
5676
5523
|
if (step.verification && step.verification.type === 'output_contains') {
|
|
5677
5524
|
const token = step.verification.value;
|
|
5678
5525
|
const ptyOutput = (this.ptyOutputBuffers.get(agent.name) ?? []).join('');
|
|
5679
|
-
const verificationPassed = this.outputContainsVerificationToken(
|
|
5680
|
-
ptyOutput,
|
|
5681
|
-
token,
|
|
5682
|
-
promptTaskText
|
|
5683
|
-
);
|
|
5526
|
+
const verificationPassed = this.outputContainsVerificationToken(ptyOutput, token, promptTaskText);
|
|
5684
5527
|
if (!verificationPassed) {
|
|
5685
5528
|
// The broker fires agent_idle only once per idle transition.
|
|
5686
5529
|
// If the agent is still working (will produce output then idle again),
|
|
5687
5530
|
// continuing the loop works. But if the agent is permanently idle,
|
|
5688
5531
|
// waitForIdle won't resolve again. Wait briefly for new output,
|
|
5689
5532
|
// then release and let upstream verification handle the result.
|
|
5690
|
-
this.log(
|
|
5533
|
+
this.log(
|
|
5534
|
+
`[${step.name}] Agent "${agent.name}" went idle but verification not yet passed — waiting for more output`
|
|
5535
|
+
);
|
|
5691
5536
|
const idleGraceSecs = 15;
|
|
5692
5537
|
const graceResult = await Promise.race([
|
|
5693
5538
|
agent.waitForExit(idleGraceSecs * 1000).then((r) => ({ kind: 'exit' as const, result: r })),
|
|
@@ -5702,15 +5547,19 @@ export class WorkflowRunner {
|
|
|
5702
5547
|
}
|
|
5703
5548
|
// Grace period timed out — agent is permanently idle without verification.
|
|
5704
5549
|
// Release and let upstream executeAgentStep handle verification.
|
|
5705
|
-
this.log(
|
|
5706
|
-
|
|
5707
|
-
|
|
5550
|
+
this.log(
|
|
5551
|
+
`[${step.name}] Agent "${agent.name}" still idle after ${idleGraceSecs}s grace — releasing`
|
|
5552
|
+
);
|
|
5553
|
+
this.postToChannel(
|
|
5554
|
+
`**[${step.name}]** Agent \`${agent.name}\` idle — releasing (verification pending)`
|
|
5555
|
+
);
|
|
5556
|
+
await agent.release().catch(() => undefined);
|
|
5708
5557
|
return 'released';
|
|
5709
5558
|
}
|
|
5710
5559
|
}
|
|
5711
5560
|
this.log(`[${step.name}] Agent "${agent.name}" went idle — treating as complete`);
|
|
5712
5561
|
this.postToChannel(`**[${step.name}]** Agent \`${agent.name}\` idle — treating as complete`);
|
|
5713
|
-
await agent.release();
|
|
5562
|
+
await agent.release().catch(() => undefined);
|
|
5714
5563
|
return 'released';
|
|
5715
5564
|
}
|
|
5716
5565
|
// Exit won the race, or idle returned 'exited'/'timeout' — pass through.
|
|
@@ -5783,7 +5632,7 @@ export class WorkflowRunner {
|
|
|
5783
5632
|
`**[${step.name}]** Agent \`${agent.name}\` still idle after ${nudgeCount} nudge(s) — force-releasing`
|
|
5784
5633
|
);
|
|
5785
5634
|
this.emit({ type: 'step:force-released', runId: this.currentRunId ?? '', stepName: step.name });
|
|
5786
|
-
await agent.release();
|
|
5635
|
+
await agent.release().catch(() => undefined);
|
|
5787
5636
|
return 'force-released';
|
|
5788
5637
|
}
|
|
5789
5638
|
}
|
|
@@ -5865,84 +5714,18 @@ export class WorkflowRunner {
|
|
|
5865
5714
|
injectedTaskText?: string,
|
|
5866
5715
|
options?: VerificationOptions
|
|
5867
5716
|
): VerificationResult {
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
|
|
5876
|
-
|
|
5877
|
-
|
|
5878
|
-
source: 'verification',
|
|
5879
|
-
text: message,
|
|
5880
|
-
observedAt,
|
|
5881
|
-
value: check.value,
|
|
5882
|
-
});
|
|
5883
|
-
if (options?.allowFailure) {
|
|
5884
|
-
return {
|
|
5885
|
-
passed: false,
|
|
5886
|
-
completionReason: 'failed_verification',
|
|
5887
|
-
error: message,
|
|
5888
|
-
};
|
|
5889
|
-
}
|
|
5890
|
-
throw new WorkflowCompletionError(message, 'failed_verification');
|
|
5891
|
-
};
|
|
5892
|
-
|
|
5893
|
-
switch (check.type) {
|
|
5894
|
-
case 'output_contains': {
|
|
5895
|
-
const token = check.value;
|
|
5896
|
-
if (!this.outputContainsVerificationToken(output, token, injectedTaskText)) {
|
|
5897
|
-
return fail(`Verification failed for "${stepName}": output does not contain "${token}"`);
|
|
5898
|
-
}
|
|
5899
|
-
break;
|
|
5717
|
+
return runVerification(
|
|
5718
|
+
check,
|
|
5719
|
+
output,
|
|
5720
|
+
stepName,
|
|
5721
|
+
injectedTaskText,
|
|
5722
|
+
{ ...options, cwd: this.cwd },
|
|
5723
|
+
{
|
|
5724
|
+
recordStepToolSideEffect: (name, effect) => this.recordStepToolSideEffect(name, effect),
|
|
5725
|
+
getOrCreateStepEvidenceRecord: (name) => this.getOrCreateStepEvidenceRecord(name),
|
|
5726
|
+
log: (message) => this.log(message),
|
|
5900
5727
|
}
|
|
5901
|
-
|
|
5902
|
-
case 'exit_code':
|
|
5903
|
-
// exit_code verification is implicitly satisfied if the agent exited successfully
|
|
5904
|
-
break;
|
|
5905
|
-
|
|
5906
|
-
case 'file_exists':
|
|
5907
|
-
if (!existsSync(path.resolve(this.cwd, check.value))) {
|
|
5908
|
-
return fail(`Verification failed for "${stepName}": file "${check.value}" does not exist`);
|
|
5909
|
-
}
|
|
5910
|
-
break;
|
|
5911
|
-
|
|
5912
|
-
case 'custom':
|
|
5913
|
-
// Custom verifications are evaluated by callers; no-op here
|
|
5914
|
-
return { passed: false };
|
|
5915
|
-
}
|
|
5916
|
-
|
|
5917
|
-
if (options?.completionMarkerFound === false) {
|
|
5918
|
-
this.log(
|
|
5919
|
-
`[${stepName}] Verification passed without legacy STEP_COMPLETE marker; allowing completion`
|
|
5920
|
-
);
|
|
5921
|
-
}
|
|
5922
|
-
|
|
5923
|
-
const successMessage =
|
|
5924
|
-
options?.completionMarkerFound === false
|
|
5925
|
-
? `Verification passed without legacy STEP_COMPLETE marker`
|
|
5926
|
-
: `Verification passed`;
|
|
5927
|
-
const observedAt = new Date().toISOString();
|
|
5928
|
-
this.recordStepToolSideEffect(stepName, {
|
|
5929
|
-
type: 'verification_observed',
|
|
5930
|
-
detail: successMessage,
|
|
5931
|
-
observedAt,
|
|
5932
|
-
raw: { passed: true, type: check.type, value: check.value },
|
|
5933
|
-
});
|
|
5934
|
-
this.getOrCreateStepEvidenceRecord(stepName).evidence.coordinationSignals.push({
|
|
5935
|
-
kind: 'verification_passed',
|
|
5936
|
-
source: 'verification',
|
|
5937
|
-
text: successMessage,
|
|
5938
|
-
observedAt,
|
|
5939
|
-
value: check.value,
|
|
5940
|
-
});
|
|
5941
|
-
|
|
5942
|
-
return {
|
|
5943
|
-
passed: true,
|
|
5944
|
-
completionReason: 'completed_verified',
|
|
5945
|
-
};
|
|
5728
|
+
);
|
|
5946
5729
|
}
|
|
5947
5730
|
|
|
5948
5731
|
// ── State helpers ─────────────────────────────────────────────────────
|
|
@@ -6116,30 +5899,7 @@ export class WorkflowRunner {
|
|
|
6116
5899
|
agentMap: Map<string, AgentDefinition>,
|
|
6117
5900
|
stepStates: Map<string, StepState>
|
|
6118
5901
|
): string | undefined {
|
|
6119
|
-
|
|
6120
|
-
if (nonInteractive.length === 0) return undefined;
|
|
6121
|
-
|
|
6122
|
-
// Map agent names to their step names so the lead knows exact {{steps.X.output}} references
|
|
6123
|
-
const agentToSteps = new Map<string, string[]>();
|
|
6124
|
-
for (const [stepName, state] of stepStates) {
|
|
6125
|
-
const agentName = state.row.agentName;
|
|
6126
|
-
if (!agentName) continue; // Skip deterministic steps
|
|
6127
|
-
if (!agentToSteps.has(agentName)) agentToSteps.set(agentName, []);
|
|
6128
|
-
agentToSteps.get(agentName)!.push(stepName);
|
|
6129
|
-
}
|
|
6130
|
-
|
|
6131
|
-
const lines = nonInteractive.map((a) => {
|
|
6132
|
-
const steps = agentToSteps.get(a.name) ?? [];
|
|
6133
|
-
const stepRefs = steps.map((s) => `{{steps.${s}.output}}`).join(', ');
|
|
6134
|
-
return `- ${a.name} (${a.cli}) — will return output when complete${stepRefs ? `. Access via: ${stepRefs}` : ''}`;
|
|
6135
|
-
});
|
|
6136
|
-
return (
|
|
6137
|
-
'\n\n---\n' +
|
|
6138
|
-
'Note: The following agents are non-interactive workers and cannot receive messages:\n' +
|
|
6139
|
-
lines.join('\n') +
|
|
6140
|
-
'\n' +
|
|
6141
|
-
'Do NOT attempt to message these agents. Use the {{steps.<name>.output}} references above to access their results.'
|
|
6142
|
-
);
|
|
5902
|
+
return this.channelMessenger.buildNonInteractiveAwareness(agentMap, stepStates);
|
|
6143
5903
|
}
|
|
6144
5904
|
|
|
6145
5905
|
/**
|
|
@@ -6155,53 +5915,11 @@ export class WorkflowRunner {
|
|
|
6155
5915
|
* key, but they won't call `register` unless explicitly told to.
|
|
6156
5916
|
*/
|
|
6157
5917
|
private buildRelayRegistrationNote(cli: string, agentName: string): string {
|
|
6158
|
-
|
|
6159
|
-
return (
|
|
6160
|
-
'---\n' +
|
|
6161
|
-
'RELAY SETUP — do this FIRST before any other relay tool:\n' +
|
|
6162
|
-
`1. Call: register(name="${agentName}")\n` +
|
|
6163
|
-
' This authenticates you in the Relaycast workspace.\n' +
|
|
6164
|
-
' ALL relay tools (mcp__relaycast__message_dm_send, mcp__relaycast__message_inbox_check, mcp__relaycast__message_post, etc.) require\n' +
|
|
6165
|
-
' registration first — they will fail with "Not registered" otherwise.\n' +
|
|
6166
|
-
`2. Your agent name is "${agentName}" — use this exact name when registering.`
|
|
6167
|
-
);
|
|
5918
|
+
return this.channelMessenger.buildRelayRegistrationNote(cli, agentName);
|
|
6168
5919
|
}
|
|
6169
5920
|
|
|
6170
5921
|
private buildDelegationGuidance(cli: string, timeoutMs?: number): string {
|
|
6171
|
-
|
|
6172
|
-
? `You have approximately ${Math.round(timeoutMs / 60000)} minutes before this step times out. ` +
|
|
6173
|
-
'Plan accordingly — delegate early if the work is substantial.\n\n'
|
|
6174
|
-
: '';
|
|
6175
|
-
|
|
6176
|
-
// Option 2 (sub-agents via Task tool) is only available in Claude
|
|
6177
|
-
const subAgentOption =
|
|
6178
|
-
cli === 'claude'
|
|
6179
|
-
? 'Option 2 — Use built-in sub-agents (Task tool) for research or scoped work:\n' +
|
|
6180
|
-
' - Good for exploring code, reading files, or making targeted changes\n' +
|
|
6181
|
-
' - Can run multiple sub-agents in parallel\n\n'
|
|
6182
|
-
: '';
|
|
6183
|
-
|
|
6184
|
-
return (
|
|
6185
|
-
'---\n' +
|
|
6186
|
-
'AUTONOMOUS DELEGATION — READ THIS BEFORE STARTING:\n' +
|
|
6187
|
-
timeoutNote +
|
|
6188
|
-
'Before diving in, assess whether this task is too large or complex for a single agent. ' +
|
|
6189
|
-
'If it involves multiple independent subtasks, touches many files, or could take a long time, ' +
|
|
6190
|
-
'you should break it down and delegate to helper agents to avoid timeouts.\n\n' +
|
|
6191
|
-
'Option 1 — Spawn relay agents (for real parallel coding work):\n' +
|
|
6192
|
-
' - mcp__relaycast__agent_add(name="helper-1", cli="claude", task="Specific subtask description")\n' +
|
|
6193
|
-
' - Coordinate via mcp__relaycast__message_dm_send(to="helper-1", text="...")\n' +
|
|
6194
|
-
' - Check on them with mcp__relaycast__message_inbox_check()\n' +
|
|
6195
|
-
' - Clean up when done: mcp__relaycast__agent_remove(name="helper-1")\n\n' +
|
|
6196
|
-
subAgentOption +
|
|
6197
|
-
'Guidelines:\n' +
|
|
6198
|
-
'- You are the lead — delegate but stay in control, track progress, integrate results\n' +
|
|
6199
|
-
'- Give each helper a clear, self-contained task with enough context to work independently\n' +
|
|
6200
|
-
"- For simple or quick work, just do it yourself — don't over-delegate\n" +
|
|
6201
|
-
'- Always release spawned relay agents when their work is complete\n' +
|
|
6202
|
-
'- When spawning non-claude agents (codex, gemini, etc.), prepend to their task:\n' +
|
|
6203
|
-
' "RELAY SETUP: First call register(name=\'<exact-agent-name>\') before any other relay tool."'
|
|
6204
|
-
);
|
|
5922
|
+
return this.channelMessenger.buildDelegationGuidance(cli, timeoutMs);
|
|
6205
5923
|
}
|
|
6206
5924
|
|
|
6207
5925
|
/** Post a message to the workflow channel. Fire-and-forget — never throws or blocks. */
|
|
@@ -6237,52 +5955,12 @@ export class WorkflowRunner {
|
|
|
6237
5955
|
summary: string,
|
|
6238
5956
|
confidence: number
|
|
6239
5957
|
): void {
|
|
6240
|
-
|
|
6241
|
-
const skipped = outcomes.filter((o) => o.status === 'skipped');
|
|
6242
|
-
const retried = outcomes.filter((o) => o.attempts > 1);
|
|
6243
|
-
|
|
6244
|
-
const lines: string[] = [
|
|
6245
|
-
`## Workflow **${workflowName}** — Complete`,
|
|
6246
|
-
'',
|
|
6247
|
-
summary,
|
|
6248
|
-
`Confidence: ${Math.round(confidence * 100)}%`,
|
|
6249
|
-
'',
|
|
6250
|
-
'### Steps',
|
|
6251
|
-
...completed.map(
|
|
6252
|
-
(o) =>
|
|
6253
|
-
`- **${o.name}** (${o.agent}) — passed${o.verificationPassed ? ' (verified)' : ''}${o.attempts > 1 ? ` after ${o.attempts} attempts` : ''}`
|
|
6254
|
-
),
|
|
6255
|
-
...skipped.map((o) => `- **${o.name}** — skipped`),
|
|
6256
|
-
];
|
|
6257
|
-
|
|
6258
|
-
if (retried.length > 0) {
|
|
6259
|
-
lines.push('', '### Retries');
|
|
6260
|
-
for (const o of retried) {
|
|
6261
|
-
lines.push(`- ${o.name}: ${o.attempts} attempts`);
|
|
6262
|
-
}
|
|
6263
|
-
}
|
|
6264
|
-
|
|
6265
|
-
this.postToChannel(lines.join('\n'));
|
|
5958
|
+
this.channelMessenger.postCompletionReport(workflowName, outcomes, summary, confidence);
|
|
6266
5959
|
}
|
|
6267
5960
|
|
|
6268
5961
|
/** Post a failure report to the channel. */
|
|
6269
5962
|
private postFailureReport(workflowName: string, outcomes: StepOutcome[], errorMsg: string): void {
|
|
6270
|
-
|
|
6271
|
-
const failed = outcomes.filter((o) => o.status === 'failed');
|
|
6272
|
-
const skipped = outcomes.filter((o) => o.status === 'skipped');
|
|
6273
|
-
|
|
6274
|
-
const lines: string[] = [
|
|
6275
|
-
`## Workflow **${workflowName}** — Failed`,
|
|
6276
|
-
'',
|
|
6277
|
-
`${completed.length}/${outcomes.length} steps passed. Error: ${errorMsg}`,
|
|
6278
|
-
'',
|
|
6279
|
-
'### Steps',
|
|
6280
|
-
...completed.map((o) => `- **${o.name}** (${o.agent}) — passed`),
|
|
6281
|
-
...failed.map((o) => `- **${o.name}** (${o.agent}) — FAILED: ${o.error ?? 'unknown'}`),
|
|
6282
|
-
...skipped.map((o) => `- **${o.name}** — skipped`),
|
|
6283
|
-
];
|
|
6284
|
-
|
|
6285
|
-
this.postToChannel(lines.join('\n'));
|
|
5963
|
+
this.channelMessenger.postFailureReport(workflowName, outcomes, errorMsg);
|
|
6286
5964
|
}
|
|
6287
5965
|
|
|
6288
5966
|
/**
|
|
@@ -6296,7 +5974,9 @@ export class WorkflowRunner {
|
|
|
6296
5974
|
|
|
6297
5975
|
console.log('');
|
|
6298
5976
|
console.log(chalk.dim('━'.repeat(70)));
|
|
6299
|
-
console.log(
|
|
5977
|
+
console.log(
|
|
5978
|
+
` Workflow "${workflowName}" — ${failed.length === 0 ? chalk.green('COMPLETED') : chalk.red('FAILED')}`
|
|
5979
|
+
);
|
|
6300
5980
|
console.log(
|
|
6301
5981
|
` ${chalk.green(`${completed.length} passed`)}, ${chalk.red(`${failed.length} failed`)}, ${chalk.dim(`${skipped.length} skipped`)}`
|
|
6302
5982
|
);
|
|
@@ -6628,9 +6308,7 @@ export class WorkflowRunner {
|
|
|
6628
6308
|
if (!stat.isDirectory()) continue;
|
|
6629
6309
|
|
|
6630
6310
|
// Check if this directory has at least one of the needed step files
|
|
6631
|
-
const hasAny = [...stepNames].some(name =>
|
|
6632
|
-
existsSync(path.join(dirPath, `${name}.md`))
|
|
6633
|
-
);
|
|
6311
|
+
const hasAny = [...stepNames].some((name) => existsSync(path.join(dirPath, `${name}.md`)));
|
|
6634
6312
|
if (!hasAny) continue;
|
|
6635
6313
|
|
|
6636
6314
|
if (!best || stat.mtimeMs > best.mtime) {
|
|
@@ -6750,10 +6428,13 @@ export class WorkflowRunner {
|
|
|
6750
6428
|
}
|
|
6751
6429
|
const fallbackTime = new Date().toISOString();
|
|
6752
6430
|
|
|
6753
|
-
const completedSteps = new Set(
|
|
6431
|
+
const completedSteps = new Set(
|
|
6432
|
+
workflow.steps.filter((step) => cachedStepNames.has(step.name)).map((step) => step.name)
|
|
6433
|
+
);
|
|
6754
6434
|
// Heuristic: mark the first eligible non-completed step as failed (the likely failure point)
|
|
6755
6435
|
const failedStepName = workflow.steps.find(
|
|
6756
|
-
(step) =>
|
|
6436
|
+
(step) =>
|
|
6437
|
+
!completedSteps.has(step.name) && (step.dependsOn ?? []).every((dep) => completedSteps.has(dep))
|
|
6757
6438
|
)?.name;
|
|
6758
6439
|
|
|
6759
6440
|
const runStartedAt = new Date(earliestMtime).toISOString();
|
|
@@ -6771,10 +6452,14 @@ export class WorkflowRunner {
|
|
|
6771
6452
|
|
|
6772
6453
|
const stepStates = new Map<string, StepState>();
|
|
6773
6454
|
for (const step of workflow.steps) {
|
|
6774
|
-
const isNonAgent =
|
|
6455
|
+
const isNonAgent =
|
|
6456
|
+
step.type === 'deterministic' || step.type === 'worktree' || step.type === 'integration';
|
|
6775
6457
|
const cachedOutput = completedSteps.has(step.name) ? this.loadStepOutput(runId, step.name) : undefined;
|
|
6776
|
-
const status: WorkflowStepStatus =
|
|
6777
|
-
|
|
6458
|
+
const status: WorkflowStepStatus = completedSteps.has(step.name)
|
|
6459
|
+
? 'completed'
|
|
6460
|
+
: step.name === failedStepName
|
|
6461
|
+
? 'failed'
|
|
6462
|
+
: 'pending';
|
|
6778
6463
|
|
|
6779
6464
|
const stepRow: WorkflowStepRow = {
|
|
6780
6465
|
id: this.generateId(),
|
|
@@ -6789,7 +6474,7 @@ export class WorkflowRunner {
|
|
|
6789
6474
|
: step.type === 'worktree'
|
|
6790
6475
|
? (step.branch ?? '')
|
|
6791
6476
|
: step.type === 'integration'
|
|
6792
|
-
?
|
|
6477
|
+
? `${step.integration}.${step.action}`
|
|
6793
6478
|
: (step.task ?? ''),
|
|
6794
6479
|
dependsOn: step.dependsOn ?? [],
|
|
6795
6480
|
output: cachedOutput,
|