@workflow-manager/runner 0.1.0 → 0.3.0

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.
Files changed (40) hide show
  1. package/README.md +123 -12
  2. package/dist/adapters.d.ts +4 -0
  3. package/dist/adapters.js +11 -0
  4. package/dist/claudeCodeExecutor.d.ts +4 -0
  5. package/dist/claudeCodeExecutor.js +173 -0
  6. package/dist/cliRunRenderer.d.ts +36 -0
  7. package/dist/cliRunRenderer.js +286 -0
  8. package/dist/engine.d.ts +4 -2
  9. package/dist/engine.js +622 -44
  10. package/dist/events.d.ts +1 -1
  11. package/dist/events.js +4 -2
  12. package/dist/index.js +371 -41
  13. package/dist/manPage.d.ts +1 -0
  14. package/dist/manPage.js +147 -0
  15. package/dist/mockExecutor.d.ts +2 -2
  16. package/dist/mockExecutor.js +62 -13
  17. package/dist/opencodeExecutor.d.ts +2 -2
  18. package/dist/opencodeExecutor.js +101 -138
  19. package/dist/parser.js +98 -2
  20. package/dist/piAgentExecutor.d.ts +4 -0
  21. package/dist/piAgentExecutor.js +298 -0
  22. package/dist/remote/api.d.ts +1 -0
  23. package/dist/remote/commands.d.ts +2 -0
  24. package/dist/remote/commands.js +76 -4
  25. package/dist/runnerApi.d.ts +7 -0
  26. package/dist/runnerApi.js +221 -0
  27. package/dist/runnerSession.d.ts +62 -0
  28. package/dist/runnerSession.js +260 -0
  29. package/dist/runtimePreflight.d.ts +16 -0
  30. package/dist/runtimePreflight.js +189 -0
  31. package/dist/skillResolver.d.ts +6 -0
  32. package/dist/skillResolver.js +75 -0
  33. package/dist/types.d.ts +148 -2
  34. package/man/wfm.1 +54 -4
  35. package/package.json +28 -4
  36. package/skills/commit-discipline/SKILL.md +109 -0
  37. package/skills/doc-sync/SKILL.md +83 -0
  38. package/skills/repo-hygiene/SKILL.md +70 -0
  39. package/skills/spec-driven-development/SKILL.md +33 -0
  40. package/skills/workflow-manager-cli/SKILL.md +14 -9
package/dist/engine.js CHANGED
@@ -1,7 +1,12 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { createInterface } from "node:readline";
3
+ import { resolveTaskAdapter } from "./adapters.js";
2
4
  import { EventLog } from "./events.js";
5
+ import { executeClaudeCodeStep, shouldUseRealClaudeCode } from "./claudeCodeExecutor.js";
3
6
  import { executeMockStep } from "./mockExecutor.js";
4
7
  import { executeOpencodeStep, shouldUseRealOpencode } from "./opencodeExecutor.js";
8
+ import { executePiAgentStep } from "./piAgentExecutor.js";
9
+ import { validateRuntimeRequirements } from "./runtimePreflight.js";
5
10
  function nodeType(step) {
6
11
  if (step.kind === "approval")
7
12
  return "HUMAN";
@@ -9,9 +14,92 @@ function nodeType(step) {
9
14
  return "SYSTEM";
10
15
  return "AGENT";
11
16
  }
17
+ function stepAdapter(step) {
18
+ return step.kind === "task" ? resolveTaskAdapter(step.taskSpec?.adapterKey) : "approval";
19
+ }
12
20
  function stepObjective(step, workflowObjective) {
13
21
  return step.objective ?? `${workflowObjective} :: ${step.key}`;
14
22
  }
23
+ function stepLabel(step) {
24
+ return step.title ?? step.objective ?? step.key;
25
+ }
26
+ const EXECUTION_STATUSES = ["SUCCESS", "QA_REJECTED", "YIELD_EXTERNAL", "FAILED"];
27
+ const QA_ACTIONS = ["PROCEED", "RETRY_CURRENT", "ROLLBACK_PREVIOUS", "RESTART_ALL"];
28
+ function isExecutionStatus(value) {
29
+ return typeof value === "string" && EXECUTION_STATUSES.includes(value);
30
+ }
31
+ function isQaAction(value) {
32
+ return typeof value === "string" && QA_ACTIONS.includes(value);
33
+ }
34
+ function validatedExecutorOutput(step, input, attempt, output) {
35
+ if (isExecutionStatus(output.execution_status) && isQaAction(output.qa_routing.action)) {
36
+ return output;
37
+ }
38
+ const invalidExecutionStatus = isExecutionStatus(output.execution_status) ? null : String(output.execution_status);
39
+ const invalidQaAction = isQaAction(output.qa_routing.action) ? null : String(output.qa_routing.action);
40
+ const reason = [
41
+ invalidExecutionStatus ? `execution_status=${invalidExecutionStatus}` : null,
42
+ invalidQaAction ? `qa_routing.action=${invalidQaAction}` : null,
43
+ ]
44
+ .filter(Boolean)
45
+ .join(", ");
46
+ return {
47
+ step_id: step.key,
48
+ execution_status: "FAILED",
49
+ qa_routing: {
50
+ action: "PROCEED",
51
+ feedback_reason: `Invalid executor output for ${step.key}: ${reason}`,
52
+ },
53
+ mutated_payload: {
54
+ stepKey: step.key,
55
+ attempt,
56
+ adapter: input.priming_configuration.adapter ?? stepAdapter(step),
57
+ invalidExecutionStatus,
58
+ invalidQaAction,
59
+ },
60
+ metadata: {
61
+ execution_time_ms: output.metadata.execution_time_ms,
62
+ external_intervention_required: false,
63
+ },
64
+ };
65
+ }
66
+ function orderStepsByDependencies(steps) {
67
+ const byKey = new Map(steps.map((step) => [step.key, step]));
68
+ const ordered = [];
69
+ const visiting = new Set();
70
+ const visited = new Set();
71
+ const visit = (step) => {
72
+ if (visited.has(step.key)) {
73
+ return;
74
+ }
75
+ if (visiting.has(step.key)) {
76
+ throw new Error(`Circular dependency detected at step ${step.key}`);
77
+ }
78
+ visiting.add(step.key);
79
+ for (const dependency of step.dependsOn ?? []) {
80
+ const dependencyStep = byKey.get(dependency);
81
+ if (dependencyStep) {
82
+ visit(dependencyStep);
83
+ }
84
+ }
85
+ visiting.delete(step.key);
86
+ visited.add(step.key);
87
+ ordered.push(step);
88
+ };
89
+ for (const step of steps) {
90
+ visit(step);
91
+ }
92
+ return ordered;
93
+ }
94
+ function summarizeContext(value) {
95
+ if (typeof value === "string") {
96
+ return { type: "string", length: value.length };
97
+ }
98
+ if (value && typeof value === "object") {
99
+ return { type: "object", keys: Object.keys(value).sort() };
100
+ }
101
+ return { type: "none" };
102
+ }
15
103
  function requiresValidation(step) {
16
104
  if (step.approvalSpec?.validation?.required)
17
105
  return step.approvalSpec.validation.mode ?? "human";
@@ -21,6 +109,9 @@ function requiresValidation(step) {
21
109
  return step.approvalSpec?.validation?.mode ?? "human";
22
110
  return step.validation?.mode ?? "none";
23
111
  }
112
+ export function canUseInteractiveConfirmation(step) {
113
+ return requiresValidation(step) === "human";
114
+ }
24
115
  function canConfirm(step, options, output) {
25
116
  const mode = requiresValidation(step);
26
117
  if (mode === "none" && output.execution_status !== "YIELD_EXTERNAL")
@@ -36,22 +127,366 @@ function canConfirm(step, options, output) {
36
127
  return { ok: true };
37
128
  return { ok: false, reason: `Missing confirmation for ${step.key} (${mode})` };
38
129
  }
39
- function executeStep(step, input, attempt) {
40
- const adapterKey = step.taskSpec?.adapterKey ?? "mock";
130
+ function summarizeText(value, maxLength = 180) {
131
+ const normalized = value.replace(/\s+/g, " ").trim();
132
+ if (normalized.length <= maxLength) {
133
+ return normalized;
134
+ }
135
+ return `${normalized.slice(0, maxLength - 3)}...`;
136
+ }
137
+ function summarizeArtifact(value) {
138
+ if (typeof value === "string") {
139
+ return summarizeText(value);
140
+ }
141
+ if (typeof value === "number" || typeof value === "boolean") {
142
+ return String(value);
143
+ }
144
+ if (Array.isArray(value)) {
145
+ const items = value.slice(0, 3).map((entry) => summarizeArtifact(entry)).filter(Boolean);
146
+ return items.length > 0 ? items.join(", ") : "Array value";
147
+ }
148
+ if (!value || typeof value !== "object") {
149
+ return "No review artifact available.";
150
+ }
151
+ const record = value;
152
+ const preferredKeys = [
153
+ "summary",
154
+ "output",
155
+ "storyMarkdown",
156
+ "chapterMarkdown",
157
+ "stdout",
158
+ "stderr",
159
+ "paragraph",
160
+ "objective",
161
+ "feedback_reason",
162
+ "feedbackReason",
163
+ ];
164
+ for (const key of preferredKeys) {
165
+ const candidate = record[key];
166
+ if (typeof candidate === "string" && candidate.trim()) {
167
+ return summarizeText(candidate);
168
+ }
169
+ }
170
+ const primitives = Object.entries(record)
171
+ .filter(([, candidate]) => typeof candidate === "string" || typeof candidate === "number" || typeof candidate === "boolean")
172
+ .slice(0, 4)
173
+ .map(([key, candidate]) => `${key}=${summarizeText(String(candidate), 60)}`);
174
+ if (primitives.length > 0) {
175
+ return primitives.join(", ");
176
+ }
177
+ const keys = Object.keys(record).slice(0, 5);
178
+ return keys.length > 0 ? `Object with keys: ${keys.join(", ")}` : "No review artifact available.";
179
+ }
180
+ function buildApprovalPreview(step, stepRuns, previousOutput, currentOutput) {
181
+ const items = [];
182
+ if (currentOutput && Object.keys(currentOutput).length > 0 && step.kind !== "approval") {
183
+ items.push({
184
+ stepKey: step.key,
185
+ title: `Output from ${step.key}`,
186
+ summary: summarizeArtifact(currentOutput),
187
+ source: "current_step",
188
+ status: stepRuns.get(step.key)?.status,
189
+ });
190
+ }
191
+ for (const dependency of step.dependsOn ?? []) {
192
+ items.push({
193
+ stepKey: dependency,
194
+ title: `Dependency ${dependency}`,
195
+ summary: summarizeArtifact(previousOutput[dependency]),
196
+ source: "dependency",
197
+ status: stepRuns.get(dependency)?.status,
198
+ });
199
+ }
200
+ if (items.length === 0) {
201
+ items.push({
202
+ stepKey: null,
203
+ title: step.kind === "approval" ? "Manual approval gate" : "No review artifact",
204
+ summary: step.kind === "approval"
205
+ ? "This step is a manual checkpoint. Approving it continues the workflow."
206
+ : "This step did not emit a review artifact. Approving it continues the workflow.",
207
+ source: "approval_gate",
208
+ });
209
+ }
210
+ const summary = step.kind === "approval"
211
+ ? `Approve this gate to continue after ${items
212
+ .filter((item) => item.stepKey)
213
+ .map((item) => item.stepKey)
214
+ .join(", ") || "the previous step"}.`
215
+ : `Approve the results of ${step.key} before the workflow continues.`;
216
+ return {
217
+ stepLabel: stepLabel(step),
218
+ objective: step.objective ?? step.title ?? null,
219
+ summary,
220
+ items,
221
+ };
222
+ }
223
+ export async function promptForApprovalDecision(stepKey, reason, validation, preview, actor, signal) {
224
+ if (!process.stdin.isTTY)
225
+ return null;
226
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
227
+ const decisionVerb = validation === "external" ? "Resume" : "Approve";
228
+ const positiveAnswers = new Set(["a", "approve", "y", "yes", "r", "resume"]);
229
+ const negativeAnswers = new Set(["c", "cancel", "n", "no"]);
230
+ const render = () => {
231
+ process.stderr.write(`\n${decisionVerb} required for ${stepKey}\n`);
232
+ process.stderr.write(`- Reason: ${reason}\n`);
233
+ process.stderr.write(`- Validation: ${validation}\n`);
234
+ if (preview) {
235
+ process.stderr.write(`- Step: ${preview.stepLabel}\n`);
236
+ process.stderr.write(`- What you are deciding: ${preview.summary}\n`);
237
+ for (const item of preview.items) {
238
+ const status = item.status ? ` [${item.status}]` : "";
239
+ process.stderr.write(` - ${item.title}${status}: ${item.summary}\n`);
240
+ }
241
+ }
242
+ };
243
+ render();
244
+ return new Promise((resolve) => {
245
+ let settled = false;
246
+ const finish = (value) => {
247
+ if (settled) {
248
+ return;
249
+ }
250
+ settled = true;
251
+ signal?.removeEventListener("abort", onAbort);
252
+ rl.close();
253
+ process.stdin.resume();
254
+ resolve(value);
255
+ };
256
+ const onAbort = () => {
257
+ finish(null);
258
+ };
259
+ signal?.addEventListener("abort", onAbort, { once: true });
260
+ const ask = () => {
261
+ rl.question(`${decisionVerb} now? [a]pprove/[r]esume / [c]ancel / [v]iew: `, (answer) => {
262
+ if (signal?.aborted) {
263
+ finish(null);
264
+ return;
265
+ }
266
+ const normalized = answer.trim().toLowerCase();
267
+ if (positiveAnswers.has(normalized)) {
268
+ finish({ decision: "approved", actor, source: "terminal" });
269
+ return;
270
+ }
271
+ if (negativeAnswers.has(normalized)) {
272
+ finish({ decision: "cancelled", actor, source: "terminal", note: "cancelled in terminal" });
273
+ return;
274
+ }
275
+ if (normalized === "v" || normalized === "view" || normalized === "") {
276
+ render();
277
+ ask();
278
+ return;
279
+ }
280
+ process.stderr.write("Enter 'a'/'r' to continue, 'c' to cancel, or 'v' to reprint the decision details.\n");
281
+ ask();
282
+ });
283
+ };
284
+ if (signal?.aborted) {
285
+ finish(null);
286
+ return;
287
+ }
288
+ ask();
289
+ });
290
+ }
291
+ async function executeStep(step, input, attempt, workflow, workflowFilePath, hooks) {
292
+ if (step.kind !== "task") {
293
+ return executeMockStep(step, input, attempt, hooks);
294
+ }
295
+ const adapterKey = resolveTaskAdapter(step.taskSpec?.adapterKey);
296
+ if (adapterKey === "pi-agent") {
297
+ return executePiAgentStep(step, input, attempt, workflow, workflowFilePath, hooks);
298
+ }
41
299
  if (adapterKey === "opencode" && shouldUseRealOpencode(step)) {
42
- return executeOpencodeStep(step, input, attempt);
300
+ return executeOpencodeStep(step, input, attempt, hooks);
43
301
  }
44
- return executeMockStep(step, input, attempt);
302
+ if (adapterKey === "claude-code" && shouldUseRealClaudeCode(step)) {
303
+ return executeClaudeCodeStep(step, input, attempt, workflow, workflowFilePath, hooks);
304
+ }
305
+ return executeMockStep(step, input, attempt, hooks);
45
306
  }
46
- export function runWorkflow(definition, options) {
47
- const runId = randomUUID();
307
+ export async function runWorkflow(definition, options) {
308
+ const runId = options?.runId ?? randomUUID();
48
309
  const actor = options?.actor ?? "cli";
49
310
  const primaryObjective = options?.objective ?? definition.title;
50
311
  const workflowObjectives = definition.objectives ?? [];
51
312
  const globalState = { ...(options?.input ?? {}) };
313
+ const workflowFilePath = options?.workflowFilePath ?? "";
52
314
  const eventLog = new EventLog();
315
+ const observer = options?.observer;
316
+ const controller = options?.controller;
53
317
  let runStatus = "queued";
318
+ let currentStepKey = null;
319
+ let runStartedAt = null;
320
+ let runUpdatedAt = new Date().toISOString();
321
+ let runEndedAt = null;
322
+ let waitingForApproval = null;
54
323
  const stepRuns = new Map();
324
+ const stepRuntime = new Map();
325
+ const emptyExecution = () => ({
326
+ executionStatus: null,
327
+ qaAction: null,
328
+ feedbackReason: null,
329
+ });
330
+ const touchRun = (ended = false) => {
331
+ const now = new Date().toISOString();
332
+ runUpdatedAt = now;
333
+ if (ended) {
334
+ runEndedAt = now;
335
+ }
336
+ };
337
+ const touchStep = (stepKey, patch) => {
338
+ const current = stepRuntime.get(stepKey);
339
+ if (!current) {
340
+ return;
341
+ }
342
+ const now = new Date().toISOString();
343
+ if (patch.startedAt !== undefined)
344
+ current.startedAt = patch.startedAt;
345
+ if (patch.finishedAt !== undefined)
346
+ current.finishedAt = patch.finishedAt;
347
+ if (patch.lastExecution)
348
+ current.lastExecution = { ...patch.lastExecution };
349
+ current.updatedAt = patch.updatedAt ?? now;
350
+ touchRun(false);
351
+ };
352
+ const buildStepDetails = () => definition.steps.map((step) => {
353
+ const run = stepRuns.get(step.key);
354
+ const runtime = stepRuntime.get(step.key);
355
+ return {
356
+ stepKey: step.key,
357
+ status: run.status,
358
+ attempt: run.attempt,
359
+ confirmed: run.confirmed,
360
+ adapter: stepAdapter(step),
361
+ startedAt: runtime.startedAt,
362
+ updatedAt: runtime.updatedAt,
363
+ finishedAt: runtime.finishedAt,
364
+ kind: step.kind,
365
+ objective: step.objective ?? step.title ?? null,
366
+ dependsOn: step.dependsOn ?? [],
367
+ config: {
368
+ model: typeof step.taskSpec?.init?.model === "string" ? step.taskSpec.init.model : null,
369
+ skills: step.taskSpec?.init?.skills ?? [],
370
+ mcps: step.taskSpec?.init?.mcps ?? [],
371
+ systemPrompts: step.taskSpec?.init?.systemPrompts ?? [],
372
+ contextSummary: summarizeContext(step.taskSpec?.init?.context),
373
+ },
374
+ lastExecution: { ...runtime.lastExecution },
375
+ };
376
+ });
377
+ const buildSnapshot = () => ({
378
+ runId,
379
+ workflowKey: definition.key,
380
+ workflowTitle: definition.title,
381
+ status: runStatus,
382
+ currentStepKey,
383
+ startedAt: runStartedAt,
384
+ updatedAt: runUpdatedAt,
385
+ endedAt: runEndedAt,
386
+ objective: primaryObjective,
387
+ objectives: [...workflowObjectives],
388
+ waitingForApproval: waitingForApproval ? { ...waitingForApproval } : null,
389
+ steps: definition.steps.map((step) => {
390
+ const run = stepRuns.get(step.key);
391
+ const runtime = stepRuntime.get(step.key);
392
+ return {
393
+ stepKey: step.key,
394
+ status: run.status,
395
+ attempt: run.attempt,
396
+ confirmed: run.confirmed,
397
+ adapter: stepAdapter(step),
398
+ startedAt: runtime.startedAt,
399
+ updatedAt: runtime.updatedAt,
400
+ finishedAt: runtime.finishedAt,
401
+ };
402
+ }),
403
+ });
404
+ const emitSnapshot = () => {
405
+ observer?.onSnapshot(buildSnapshot(), buildStepDetails());
406
+ };
407
+ const pushEvent = (type, payload = {}, stepKey, eventActor = "system") => {
408
+ const event = eventLog.push(runId, type, payload, stepKey, eventActor);
409
+ observer?.onEvent(event);
410
+ };
411
+ const emitLog = (stepKey, stream, text) => {
412
+ observer?.onLog({
413
+ id: randomUUID(),
414
+ runId,
415
+ stepKey,
416
+ stream,
417
+ text,
418
+ occurredAt: new Date().toISOString(),
419
+ });
420
+ };
421
+ const waitForDecision = async (step, stepRun, reason, validation, requireConfirmationEvent, preview = null) => {
422
+ stepRun.status = "waiting_for_approval";
423
+ runStatus = "waiting_for_approval";
424
+ waitingForApproval = {
425
+ stepKey: step.key,
426
+ reason,
427
+ validation,
428
+ preview,
429
+ };
430
+ touchStep(step.key, { finishedAt: null });
431
+ pushEvent("step.waiting_for_approval", { reason, validation, preview }, step.key);
432
+ pushEvent("run.waiting_for_approval", { reason, preview }, step.key, actor);
433
+ emitSnapshot();
434
+ const request = { stepKey: step.key, reason, validation };
435
+ let resolution = null;
436
+ if (controller) {
437
+ const promptAbort = new AbortController();
438
+ const promptTask = options?.approvalPrompt
439
+ ? options.approvalPrompt({ ...request, preview, signal: promptAbort.signal }).catch((error) => {
440
+ if (promptAbort.signal.aborted) {
441
+ return null;
442
+ }
443
+ throw error;
444
+ })
445
+ : null;
446
+ try {
447
+ resolution = await controller.waitForDecision(request);
448
+ }
449
+ finally {
450
+ promptAbort.abort();
451
+ await promptTask;
452
+ }
453
+ }
454
+ else if (options?.approvalPrompt) {
455
+ resolution = await options.approvalPrompt({ ...request, preview });
456
+ }
457
+ else if (options?.interactive && process.stdin.isTTY) {
458
+ resolution = await promptForApprovalDecision(step.key, reason, validation, preview, actor);
459
+ }
460
+ if (!resolution) {
461
+ return null;
462
+ }
463
+ const resolutionActor = resolution.actor?.trim() ? resolution.actor : actor;
464
+ pushEvent("approval.resolved", {
465
+ decision: resolution.decision,
466
+ actor: resolutionActor,
467
+ note: resolution.note ?? null,
468
+ source: resolution.source ?? "api",
469
+ }, step.key, resolutionActor);
470
+ if (resolution.decision === "cancelled") {
471
+ stepRun.status = "cancelled";
472
+ runStatus = "cancelled";
473
+ currentStepKey = step.key;
474
+ waitingForApproval = null;
475
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
476
+ touchRun(true);
477
+ pushEvent("run.cancelled", { stepKey: step.key, reason: resolution.note ?? "cancelled by API", source: resolution.source ?? "api" }, step.key, resolutionActor);
478
+ emitSnapshot();
479
+ return resolution;
480
+ }
481
+ waitingForApproval = null;
482
+ runStatus = "running";
483
+ if (requireConfirmationEvent) {
484
+ stepRun.confirmed = true;
485
+ pushEvent("step.confirmed", { by: resolutionActor, validation, note: resolution.note ?? null, source: resolution.source ?? "api" }, step.key, resolutionActor);
486
+ }
487
+ emitSnapshot();
488
+ return resolution;
489
+ };
55
490
  for (const step of definition.steps) {
56
491
  stepRuns.set(step.key, {
57
492
  stepKey: step.key,
@@ -59,21 +494,64 @@ export function runWorkflow(definition, options) {
59
494
  attempt: 0,
60
495
  confirmed: false,
61
496
  });
497
+ stepRuntime.set(step.key, {
498
+ startedAt: null,
499
+ updatedAt: runUpdatedAt,
500
+ finishedAt: null,
501
+ lastExecution: emptyExecution(),
502
+ });
503
+ }
504
+ emitSnapshot();
505
+ pushEvent("run.created", { workflowKey: definition.key }, undefined, actor);
506
+ const runtimeErrors = validateRuntimeRequirements(definition);
507
+ if (runtimeErrors.length > 0) {
508
+ runStatus = "failed";
509
+ touchRun(true);
510
+ pushEvent("run.failed", { reason: runtimeErrors.join("; "), runtimeErrors }, undefined, actor);
511
+ emitSnapshot();
512
+ return {
513
+ runId,
514
+ status: runStatus,
515
+ outputs: globalState,
516
+ stepRuns: definition.steps.map((step) => stepRuns.get(step.key)),
517
+ events: eventLog.all(),
518
+ };
519
+ }
520
+ let orderedSteps;
521
+ try {
522
+ orderedSteps = orderStepsByDependencies(definition.steps);
523
+ }
524
+ catch (err) {
525
+ runStatus = "failed";
526
+ touchRun(true);
527
+ pushEvent("run.failed", { reason: err.message }, undefined, actor);
528
+ emitSnapshot();
529
+ return {
530
+ runId,
531
+ status: runStatus,
532
+ outputs: globalState,
533
+ stepRuns: definition.steps.map((step) => stepRuns.get(step.key)),
534
+ events: eventLog.all(),
535
+ };
62
536
  }
63
- eventLog.push(runId, "run.created", { workflowKey: definition.key }, undefined, actor);
64
537
  runStatus = "running";
65
- eventLog.push(runId, "run.started", { objective: primaryObjective, objectives: workflowObjectives }, undefined, actor);
538
+ runStartedAt = new Date().toISOString();
539
+ touchRun(false);
540
+ pushEvent("run.started", { objective: primaryObjective, objectives: workflowObjectives }, undefined, actor);
541
+ emitSnapshot();
66
542
  let index = 0;
67
543
  let guard = 0;
68
544
  const maxSteps = Math.max(definition.steps.length * 30, 30);
69
- while (index < definition.steps.length) {
545
+ while (index < orderedSteps.length) {
70
546
  guard += 1;
71
547
  if (guard > maxSteps) {
72
548
  runStatus = "failed";
73
- eventLog.push(runId, "run.failed", { reason: "Execution guard exceeded" });
549
+ touchRun(true);
550
+ pushEvent("run.failed", { reason: "Execution guard exceeded" });
551
+ emitSnapshot();
74
552
  break;
75
553
  }
76
- const step = definition.steps[index];
554
+ const step = orderedSteps[index];
77
555
  const stepRun = stepRuns.get(step.key);
78
556
  if (!stepRun)
79
557
  throw new Error(`Missing step run for ${step.key}`);
@@ -81,15 +559,26 @@ export function runWorkflow(definition, options) {
81
559
  const depsComplete = dependencies.every((depKey) => stepRuns.get(depKey)?.status === "succeeded");
82
560
  if (!depsComplete) {
83
561
  runStatus = "failed";
84
- eventLog.push(runId, "run.failed", { reason: `Dependencies not satisfied for ${step.key}` }, step.key);
562
+ currentStepKey = step.key;
563
+ touchRun(true);
564
+ pushEvent("run.failed", { reason: `Dependencies not satisfied for ${step.key}` }, step.key);
565
+ emitSnapshot();
85
566
  break;
86
567
  }
87
568
  stepRun.status = "runnable";
88
- eventLog.push(runId, "step.runnable", { stepKey: step.key }, step.key);
569
+ currentStepKey = step.key;
570
+ waitingForApproval = null;
571
+ touchStep(step.key, { finishedAt: null });
572
+ pushEvent("step.runnable", { stepKey: step.key }, step.key);
573
+ emitSnapshot();
89
574
  stepRun.status = "running";
90
575
  stepRun.attempt += 1;
91
- eventLog.push(runId, "step.claimed", { attempt: stepRun.attempt }, step.key);
92
- eventLog.push(runId, "step.execution_started", { attempt: stepRun.attempt }, step.key);
576
+ delete stepRun.output;
577
+ delete globalState[step.key];
578
+ touchStep(step.key, { startedAt: new Date().toISOString(), finishedAt: null });
579
+ pushEvent("step.claimed", { attempt: stepRun.attempt }, step.key);
580
+ pushEvent("step.execution_started", { attempt: stepRun.attempt }, step.key);
581
+ emitSnapshot();
93
582
  const previousOutput = {};
94
583
  for (const dep of dependencies) {
95
584
  previousOutput[dep] = stepRuns.get(dep)?.output ?? null;
@@ -112,106 +601,195 @@ export function runWorkflow(definition, options) {
112
601
  mcp_endpoints: step.taskSpec?.init?.mcps ?? [],
113
602
  system_prompts: step.taskSpec?.init?.systemPrompts ?? [],
114
603
  context: step.taskSpec?.init?.context,
115
- adapter: step.taskSpec?.adapterKey ?? "mock",
604
+ adapter: resolveTaskAdapter(step.taskSpec?.adapterKey),
116
605
  model: step.taskSpec?.init?.model,
117
606
  },
118
607
  };
119
- const output = executeStep(step, inputEnvelope, stepRun.attempt);
120
- eventLog.push(runId, "step.execution_finished", {
608
+ const hooks = {
609
+ onStarted: (payload) => {
610
+ pushEvent("agent.started", { attempt: stepRun.attempt, ...(payload ?? {}) }, step.key);
611
+ },
612
+ onStdout: (chunk) => {
613
+ emitLog(step.key, "stdout", chunk);
614
+ pushEvent("agent.stdout", { stream: "stdout", text: chunk }, step.key);
615
+ },
616
+ onStderr: (chunk) => {
617
+ emitLog(step.key, "stderr", chunk);
618
+ pushEvent("agent.stderr", { stream: "stderr", text: chunk }, step.key);
619
+ },
620
+ onFinished: (payload) => {
621
+ pushEvent("agent.finished", { attempt: stepRun.attempt, ...(payload ?? {}) }, step.key);
622
+ },
623
+ };
624
+ const output = validatedExecutorOutput(step, inputEnvelope, stepRun.attempt, await executeStep(step, inputEnvelope, stepRun.attempt, definition, workflowFilePath, hooks));
625
+ touchStep(step.key, {
626
+ lastExecution: {
627
+ executionStatus: output.execution_status,
628
+ qaAction: output.qa_routing.action,
629
+ feedbackReason: output.qa_routing.feedback_reason,
630
+ },
631
+ });
632
+ pushEvent("step.execution_finished", {
121
633
  status: output.execution_status,
122
634
  action: output.qa_routing.action,
123
- adapter: step.taskSpec?.adapterKey ?? "mock",
635
+ feedbackReason: output.qa_routing.feedback_reason,
636
+ adapter: resolveTaskAdapter(step.taskSpec?.adapterKey),
124
637
  init: {
125
638
  skills: step.taskSpec?.init?.skills ?? [],
126
639
  mcps: step.taskSpec?.init?.mcps ?? [],
127
640
  context: step.taskSpec?.init?.context ?? {},
641
+ systemPrompts: step.taskSpec?.init?.systemPrompts ?? [],
642
+ model: step.taskSpec?.init?.model ?? null,
128
643
  },
129
644
  }, step.key);
130
- const confirmation = canConfirm(step, options ?? {}, output);
131
- if (!confirmation.ok) {
132
- stepRun.status = "waiting_for_approval";
133
- runStatus = "waiting_for_approval";
134
- eventLog.push(runId, "step.waiting_for_approval", { reason: confirmation.reason, validation: requiresValidation(step) }, step.key);
135
- eventLog.push(runId, "run.waiting_for_approval", { reason: "confirmation required" }, step.key, actor);
136
- break;
645
+ emitSnapshot();
646
+ const approvalPreview = buildApprovalPreview(step, stepRuns, previousOutput, output.mutated_payload);
647
+ const confirmed = canConfirm(step, options ?? {}, output).ok;
648
+ if (!confirmed) {
649
+ const validation = requiresValidation(step);
650
+ const decision = await waitForDecision(step, stepRun, `confirmation required for ${step.key}`, validation, true, approvalPreview);
651
+ if (decision === null || decision.decision === "cancelled") {
652
+ break;
653
+ }
654
+ }
655
+ if (!stepRun.confirmed) {
656
+ stepRun.confirmed = true;
657
+ waitingForApproval = null;
658
+ pushEvent("step.confirmed", { by: actor, validation: requiresValidation(step) }, step.key, actor);
659
+ emitSnapshot();
137
660
  }
138
- stepRun.confirmed = true;
139
- eventLog.push(runId, "step.confirmed", { by: actor, validation: requiresValidation(step) }, step.key, actor);
140
661
  if (output.execution_status === "YIELD_EXTERNAL") {
141
- stepRun.status = "waiting_for_approval";
142
- runStatus = "waiting_for_approval";
143
- eventLog.push(runId, "step.waiting_for_approval", { reason: "external intervention" }, step.key);
144
- eventLog.push(runId, "approval.resolved", { decision: "approved" }, step.key, actor);
662
+ if (step.kind === "approval") {
663
+ stepRun.status = "succeeded";
664
+ runStatus = "running";
665
+ waitingForApproval = null;
666
+ stepRun.output = output.mutated_payload;
667
+ globalState[step.key] = output.mutated_payload;
668
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
669
+ currentStepKey = null;
670
+ emitSnapshot();
671
+ index += 1;
672
+ continue;
673
+ }
674
+ const decision = await waitForDecision(step, stepRun, "external intervention", "external", false, approvalPreview);
675
+ if (decision === null || decision.decision === "cancelled") {
676
+ break;
677
+ }
145
678
  stepRun.status = "succeeded";
146
679
  runStatus = "running";
680
+ waitingForApproval = null;
147
681
  stepRun.output = output.mutated_payload;
148
682
  globalState[step.key] = output.mutated_payload;
683
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
684
+ currentStepKey = null;
685
+ emitSnapshot();
149
686
  index += 1;
150
687
  continue;
151
688
  }
152
689
  if (output.execution_status === "FAILED") {
153
690
  stepRun.status = "failed";
154
691
  runStatus = "failed";
155
- eventLog.push(runId, "run.failed", { stepKey: step.key, reason: "step failed" }, step.key);
692
+ currentStepKey = step.key;
693
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
694
+ touchRun(true);
695
+ pushEvent("run.failed", { stepKey: step.key, reason: "step failed" }, step.key);
696
+ emitSnapshot();
156
697
  break;
157
698
  }
158
699
  if (output.execution_status === "QA_REJECTED") {
159
700
  const retryMax = step.retryPolicy?.maxAttempts ?? definition.defaultRetryPolicy?.maxAttempts ?? 1;
160
- if (output.qa_routing.action === "RETRY_CURRENT") {
701
+ const qaAction = String(output.qa_routing.action);
702
+ if (qaAction === "RETRY_CURRENT") {
161
703
  if (stepRun.attempt < retryMax) {
162
704
  stepRun.status = "pending";
163
- eventLog.push(runId, "step.retried", { stepKey: step.key, attempt: stepRun.attempt + 1 }, step.key);
705
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
706
+ pushEvent("step.retried", { stepKey: step.key, attempt: stepRun.attempt + 1 }, step.key);
707
+ emitSnapshot();
164
708
  continue;
165
709
  }
166
710
  stepRun.status = "failed";
167
711
  runStatus = "failed";
168
- eventLog.push(runId, "run.failed", { stepKey: step.key, reason: "max retry exceeded" }, step.key);
712
+ currentStepKey = step.key;
713
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
714
+ touchRun(true);
715
+ pushEvent("run.failed", { stepKey: step.key, reason: "max retry exceeded" }, step.key);
716
+ emitSnapshot();
169
717
  break;
170
718
  }
171
- if (output.qa_routing.action === "ROLLBACK_PREVIOUS") {
719
+ if (qaAction === "ROLLBACK_PREVIOUS") {
172
720
  if (index === 0) {
173
721
  stepRun.status = "failed";
174
722
  runStatus = "failed";
175
- eventLog.push(runId, "run.failed", { stepKey: step.key, reason: "cannot rollback before first step" }, step.key);
723
+ currentStepKey = step.key;
724
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
725
+ touchRun(true);
726
+ pushEvent("run.failed", { stepKey: step.key, reason: "cannot rollback before first step" }, step.key);
727
+ emitSnapshot();
176
728
  break;
177
729
  }
178
- const prevStep = definition.steps[index - 1];
730
+ const prevStep = orderedSteps[index - 1];
179
731
  const prevRun = stepRuns.get(prevStep.key);
180
732
  prevRun.status = "pending";
733
+ prevRun.attempt = 0;
181
734
  prevRun.confirmed = false;
182
- eventLog.push(runId, "step.retried", { stepKey: prevStep.key, via: step.key }, prevStep.key);
735
+ delete prevRun.output;
736
+ delete globalState[prevStep.key];
737
+ delete stepRun.output;
738
+ delete globalState[step.key];
739
+ touchStep(prevStep.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
740
+ pushEvent("step.retried", { stepKey: prevStep.key, via: step.key }, prevStep.key);
183
741
  stepRun.status = "pending";
184
742
  stepRun.confirmed = false;
743
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
744
+ emitSnapshot();
185
745
  index -= 1;
186
746
  continue;
187
747
  }
188
- if (output.qa_routing.action === "RESTART_ALL") {
748
+ if (qaAction === "RESTART_ALL") {
189
749
  for (const s of definition.steps) {
190
750
  const sr = stepRuns.get(s.key);
191
751
  sr.status = "pending";
192
752
  sr.attempt = 0;
193
753
  sr.confirmed = false;
194
754
  delete sr.output;
755
+ delete globalState[s.key];
756
+ touchStep(s.key, { startedAt: null, finishedAt: null, lastExecution: emptyExecution() });
195
757
  }
196
- eventLog.push(runId, "step.retried", { mode: "restart_all", triggeredBy: step.key }, step.key);
758
+ currentStepKey = null;
759
+ pushEvent("step.retried", { mode: "restart_all", triggeredBy: step.key }, step.key);
760
+ emitSnapshot();
197
761
  index = 0;
198
762
  continue;
199
763
  }
764
+ stepRun.status = "failed";
765
+ runStatus = "failed";
766
+ currentStepKey = step.key;
767
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
768
+ touchRun(true);
769
+ pushEvent("run.failed", { stepKey: step.key, reason: `Unknown QA action: ${qaAction}` }, step.key);
770
+ emitSnapshot();
771
+ break;
200
772
  }
201
773
  stepRun.status = "succeeded";
202
774
  stepRun.output = output.mutated_payload;
203
775
  globalState[step.key] = output.mutated_payload;
776
+ touchStep(step.key, { finishedAt: new Date().toISOString() });
777
+ currentStepKey = null;
778
+ emitSnapshot();
204
779
  index += 1;
205
780
  }
206
781
  if (runStatus === "running") {
207
782
  runStatus = "succeeded";
208
- eventLog.push(runId, "run.completed", { steps: definition.steps.length }, undefined, actor);
783
+ currentStepKey = null;
784
+ touchRun(true);
785
+ pushEvent("run.completed", { steps: definition.steps.length }, undefined, actor);
786
+ emitSnapshot();
209
787
  }
210
788
  return {
211
789
  runId,
212
790
  status: runStatus,
213
791
  outputs: globalState,
214
- stepRuns: definition.steps.map((s) => stepRuns.get(s.key)),
792
+ stepRuns: definition.steps.map((step) => stepRuns.get(step.key)),
215
793
  events: eventLog.all(),
216
794
  };
217
795
  }