bosun 0.42.2 → 0.42.4

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 (49) hide show
  1. package/.env.example +9 -0
  2. package/agent/agent-event-bus.mjs +10 -0
  3. package/agent/agent-supervisor.mjs +20 -0
  4. package/bosun-tui.mjs +107 -105
  5. package/cli.mjs +10 -0
  6. package/config/config.mjs +25 -0
  7. package/config/executor-config.mjs +124 -1
  8. package/infra/container-runner.mjs +565 -1
  9. package/infra/monitor.mjs +18 -0
  10. package/infra/tracing.mjs +544 -240
  11. package/infra/tui-bridge.mjs +13 -1
  12. package/kanban/kanban-adapter.mjs +128 -4
  13. package/lib/repo-map.mjs +114 -3
  14. package/package.json +11 -4
  15. package/server/ui-server.mjs +3 -0
  16. package/task/task-archiver.mjs +18 -6
  17. package/task/task-attachments.mjs +14 -10
  18. package/task/task-cli.mjs +24 -4
  19. package/task/task-executor.mjs +19 -0
  20. package/task/task-store.mjs +194 -37
  21. package/telegram/telegram-bot.mjs +4 -1
  22. package/tui/app.mjs +131 -171
  23. package/tui/components/status-header.mjs +178 -75
  24. package/tui/lib/header-config.mjs +68 -0
  25. package/tui/lib/ws-bridge.mjs +61 -9
  26. package/tui/screens/agents.mjs +127 -0
  27. package/tui/screens/tasks.mjs +1 -48
  28. package/ui/app.js +8 -5
  29. package/ui/components/kanban-board.js +65 -3
  30. package/ui/components/session-list.js +18 -32
  31. package/ui/demo-defaults.js +52 -2
  32. package/ui/modules/session-api.js +100 -0
  33. package/ui/modules/state.js +71 -15
  34. package/ui/tabs/workflows.js +25 -1
  35. package/ui/tui/App.js +298 -0
  36. package/ui/tui/TasksScreen.js +564 -0
  37. package/ui/tui/constants.js +55 -0
  38. package/ui/tui/tasks-screen-helpers.js +301 -0
  39. package/ui/tui/useTasks.js +61 -0
  40. package/ui/tui/useWebSocket.js +166 -0
  41. package/ui/tui/useWorkflows.js +30 -0
  42. package/workflow/workflow-engine.mjs +412 -7
  43. package/workflow/workflow-nodes.mjs +616 -75
  44. package/workflow-templates/agents.mjs +3 -0
  45. package/workflow-templates/planning.mjs +7 -0
  46. package/workflow-templates/sub-workflows.mjs +5 -0
  47. package/workflow-templates/task-execution.mjs +3 -0
  48. package/workspace/command-diagnostics.mjs +1 -1
  49. package/workspace/context-cache.mjs +182 -9
@@ -408,6 +408,190 @@ function cleanObject(value = {}) {
408
408
  Object.entries(value).filter(([, entry]) => entry !== undefined),
409
409
  );
410
410
  }
411
+
412
+ function normalizeDagText(value, maxLength = 240) {
413
+ const text = String(value ?? "")
414
+ .replace(/\s+/g, " ")
415
+ .trim();
416
+ if (!text) return "";
417
+ if (!Number.isFinite(maxLength) || maxLength <= 0 || text.length <= maxLength) return text;
418
+ return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
419
+ }
420
+
421
+ function cloneDagGraphNode(node = {}) {
422
+ return {
423
+ nodeId: String(node.nodeId || "").trim(),
424
+ label: node.label || null,
425
+ type: node.type || null,
426
+ status: node.status || NodeStatus.PENDING,
427
+ dependencies: Array.isArray(node.dependencies) ? [...node.dependencies] : [],
428
+ attempts: Number(node.attempts || 0) || 0,
429
+ lastError: node.lastError || null,
430
+ issueFindingCount: Array.isArray(node.issueFindings) ? node.issueFindings.length : 0,
431
+ completionEvidenceCount: Array.isArray(node.completionEvidence) ? node.completionEvidence.length : 0,
432
+ };
433
+ }
434
+
435
+ function buildDagGraphSnapshot(dagState = {}) {
436
+ const nodes = Object.values(dagState?.nodes || {})
437
+ .map((node) => cloneDagGraphNode(node))
438
+ .filter((node) => node.nodeId);
439
+ const edges = Array.isArray(dagState?.edges)
440
+ ? dagState.edges
441
+ .map((edge) => ({
442
+ edgeId: String(edge?.edgeId || edge?.id || `${edge?.source || ""}->${edge?.target || ""}`).trim(),
443
+ source: String(edge?.source || "").trim(),
444
+ target: String(edge?.target || "").trim(),
445
+ label: edge?.label || null,
446
+ condition: edge?.condition || null,
447
+ }))
448
+ .filter((edge) => edge.source && edge.target)
449
+ : [];
450
+ return { nodes, edges };
451
+ }
452
+
453
+ function detectDagFindingSource(node = {}, result = {}, errorMessage = "") {
454
+ const nodeType = String(node?.type || "").trim().toLowerCase();
455
+ const reason = String(result?.reason || "").trim().toLowerCase();
456
+ const combined = `${errorMessage} ${result?.output || ""} ${result?.stderr || ""} ${result?.reviewOutput || ""}`.toLowerCase();
457
+
458
+ if (nodeType.includes("model_review") || reason.includes("manual_review") || combined.includes("changes requested")) {
459
+ return "review";
460
+ }
461
+ if (
462
+ nodeType.startsWith("validation.") ||
463
+ combined.includes("validation") ||
464
+ combined.includes("tests red") ||
465
+ combined.includes("test failed")
466
+ ) {
467
+ return "validation";
468
+ }
469
+ if (
470
+ combined.includes("dependency") ||
471
+ combined.includes("module not found") ||
472
+ combined.includes("cannot find module") ||
473
+ combined.includes("api changed") ||
474
+ combined.includes("contract") ||
475
+ combined.includes("schema")
476
+ ) {
477
+ return "workflow_diagnostics";
478
+ }
479
+ return "execution";
480
+ }
481
+
482
+ function detectDagRecommendedAction(node = {}, result = {}, errorMessage = "") {
483
+ const reason = String(result?.reason || "").trim().toLowerCase();
484
+ const combined = `${errorMessage} ${result?.output || ""} ${result?.stderr || ""} ${result?.reviewOutput || ""}`.toLowerCase();
485
+ const source = detectDagFindingSource(node, result, errorMessage);
486
+
487
+ if (
488
+ combined.includes("timeout") ||
489
+ combined.includes("timed out") ||
490
+ combined.includes("temporar") ||
491
+ combined.includes("network") ||
492
+ combined.includes("econnreset") ||
493
+ combined.includes("service unavailable") ||
494
+ combined.includes("rate limit")
495
+ ) {
496
+ return "rerun_same_step";
497
+ }
498
+ if (source === "review") {
499
+ return "spawn_fix_step";
500
+ }
501
+ if (
502
+ source === "workflow_diagnostics" ||
503
+ combined.includes("dependency") ||
504
+ combined.includes("module not found") ||
505
+ combined.includes("cannot find module") ||
506
+ combined.includes("api changed") ||
507
+ combined.includes("schema") ||
508
+ combined.includes("contract")
509
+ ) {
510
+ return "replan_subgraph";
511
+ }
512
+ if (reason === "manual_review_required") {
513
+ return "spawn_fix_step";
514
+ }
515
+ if (source === "validation") {
516
+ return "replan_from_failed";
517
+ }
518
+ return "inspect_failure";
519
+ }
520
+
521
+ function collectDagIssueFindings(node = {}, result = undefined, errorMessage = "") {
522
+ const output = result && typeof result === "object" ? result : {};
523
+ const message = normalizeDagText(
524
+ errorMessage || output?.reviewOutput || output?.stderr || output?.output || output?.reason || "",
525
+ 260,
526
+ );
527
+ const failed = Boolean(errorMessage) || output?.passed === false || output?._failed === true;
528
+ if (!failed || !message) return [];
529
+
530
+ const source = detectDagFindingSource(node, output, message);
531
+ const recommendedAction = detectDagRecommendedAction(node, output, message);
532
+ return [cleanObject({
533
+ nodeId: node?.id || node?.nodeId || null,
534
+ label: node?.label || null,
535
+ source,
536
+ severity: source === "review" ? "high" : (source === "workflow_diagnostics" ? "high" : "medium"),
537
+ summary: message,
538
+ reason: output?.reason || null,
539
+ exitCode: Number.isFinite(Number(output?.exitCode)) ? Number(output.exitCode) : null,
540
+ command: normalizeDagText(output?.command || "", 140) || null,
541
+ recommendedAction,
542
+ suggestedRerun: output?.outputSuggestedRerun || output?.outputDiagnostics?.suggestedRerun || null,
543
+ })];
544
+ }
545
+
546
+ function collectDagCompletionEvidence(node = {}, result = undefined) {
547
+ const output = result && typeof result === "object" ? result : {};
548
+ const passed = output?.passed !== false && output?._failed !== true;
549
+ if (!passed) return [];
550
+
551
+ const evidence = [];
552
+ const summary = normalizeDagText(
553
+ output?.summary || output?.outputHint || output?.narrative || output?.output || output?.reviewPath || "",
554
+ 220,
555
+ );
556
+ if (summary) {
557
+ evidence.push({
558
+ nodeId: node?.id || node?.nodeId || null,
559
+ label: node?.label || null,
560
+ kind: "summary",
561
+ summary,
562
+ });
563
+ }
564
+ const command = normalizeDagText(output?.command || "", 140);
565
+ if (command) {
566
+ evidence.push(cleanObject({
567
+ nodeId: node?.id || node?.nodeId || null,
568
+ label: node?.label || null,
569
+ kind: "command",
570
+ command,
571
+ exitCode: Number.isFinite(Number(output?.exitCode)) ? Number(output.exitCode) : 0,
572
+ }));
573
+ }
574
+ const artifactPath = normalizeDagText(output?.reviewPath || output?.evidenceDir || output?.path || "", 180);
575
+ if (artifactPath) {
576
+ evidence.push({
577
+ nodeId: node?.id || node?.nodeId || null,
578
+ label: node?.label || null,
579
+ kind: "artifact",
580
+ path: artifactPath,
581
+ });
582
+ }
583
+ return evidence;
584
+ }
585
+
586
+ function summarizeDagEvidence(evidence = [], limit = 3) {
587
+ if (!Array.isArray(evidence) || evidence.length === 0) return [];
588
+ return evidence.slice(0, Math.max(1, limit)).map((entry) => {
589
+ if (entry?.summary) return entry.summary;
590
+ if (entry?.command) return `Command: ${entry.command}`;
591
+ if (entry?.path) return `Artifact: ${entry.path}`;
592
+ return normalizeDagText(JSON.stringify(entry), 140);
593
+ }).filter(Boolean);
594
+ }
411
595
  /**
412
596
  * Register a node type handler.
413
597
  * @param {string} type - Node type identifier (e.g., "trigger.task_low", "action.run_agent")
@@ -532,6 +716,43 @@ export function listNodeTypes() {
532
716
  * Runtime context passed through workflow execution.
533
717
  * Accumulates data from each node's output.
534
718
  */
719
+ function collectValidationFailures(detail = {}) {
720
+ const nodeOutputs = detail?.nodeOutputs && typeof detail.nodeOutputs === "object"
721
+ ? detail.nodeOutputs
722
+ : {};
723
+ const dagNodes = detail?.dagState?.nodes && typeof detail.dagState.nodes === "object"
724
+ ? detail.dagState.nodes
725
+ : {};
726
+ return Object.entries(nodeOutputs)
727
+ .map(([nodeId, output]) => {
728
+ if (!output || typeof output !== "object") return null;
729
+ const diagnostic = output.failureDiagnostic && typeof output.failureDiagnostic === "object"
730
+ ? output.failureDiagnostic
731
+ : null;
732
+ const failureKind = String(output.failureKind || diagnostic?.category || "").trim();
733
+ const hasRetryability = typeof output.retryable === "boolean" || typeof diagnostic?.retryable === "boolean";
734
+ if (!failureKind && !diagnostic && !hasRetryability) return null;
735
+ return {
736
+ nodeId,
737
+ nodeType: dagNodes?.[nodeId]?.type || null,
738
+ nodeLabel: dagNodes?.[nodeId]?.label || null,
739
+ failureKind: failureKind || diagnostic?.category || null,
740
+ retryable: typeof output.retryable === "boolean"
741
+ ? output.retryable
742
+ : diagnostic?.retryable === true,
743
+ blocked: output.blocked === true || diagnostic?.blocked === true,
744
+ exitCode: diagnostic?.exitCode ?? output.exitCode ?? null,
745
+ summary:
746
+ diagnostic?.summary ||
747
+ output.outputHint ||
748
+ output.outputDiagnostics?.summary ||
749
+ null,
750
+ detail: diagnostic?.detail || null,
751
+ };
752
+ })
753
+ .filter(Boolean);
754
+ }
755
+
535
756
  export class WorkflowContext {
536
757
  constructor(initialData = {}) {
537
758
  this.id = randomUUID();
@@ -709,7 +930,7 @@ export class WorkflowContext {
709
930
  /** Get a serializable summary of the execution */
710
931
  toJSON(endedAt = Date.now()) {
711
932
  const finishedAt = Number.isFinite(endedAt) ? endedAt : Date.now();
712
- return {
933
+ const detail = {
713
934
  id: this.id,
714
935
  startedAt: this.startedAt,
715
936
  endedAt: finishedAt,
@@ -735,6 +956,12 @@ export class WorkflowContext {
735
956
  }))
736
957
  : [],
737
958
  };
959
+ const validationFailures = collectValidationFailures(detail);
960
+ if (validationFailures.length > 0) {
961
+ detail.validationFailures = validationFailures;
962
+ detail.latestValidationFailure = validationFailures.at(-1) || null;
963
+ }
964
+ return detail;
738
965
  }
739
966
  }
740
967
 
@@ -803,14 +1030,22 @@ export class WorkflowEngine extends EventEmitter {
803
1030
 
804
1031
  _initializeDagState(def, ctx, extra = {}) {
805
1032
  const dependencyMap = new Map();
1033
+ const edges = [];
806
1034
  for (const node of def?.nodes || []) {
807
1035
  dependencyMap.set(node.id, []);
808
1036
  }
809
1037
  for (const edge of def?.edges || []) {
810
- if (!edge?.target || edge.backEdge === true) continue;
1038
+ if (!edge?.source || !edge?.target || edge.backEdge === true) continue;
811
1039
  const deps = dependencyMap.get(edge.target) || [];
812
1040
  deps.push(edge.source);
813
1041
  dependencyMap.set(edge.target, deps);
1042
+ edges.push({
1043
+ edgeId: String(edge.id || `${edge.source}->${edge.target}`),
1044
+ source: edge.source,
1045
+ target: edge.target,
1046
+ label: edge.label || null,
1047
+ condition: edge.condition || null,
1048
+ });
814
1049
  }
815
1050
 
816
1051
  const nowIso = new Date(ctx.startedAt).toISOString();
@@ -823,9 +1058,11 @@ export class WorkflowEngine extends EventEmitter {
823
1058
  parentRunId: extra.parentRunId || ctx.data?._workflowParentRunId || null,
824
1059
  retryOf: extra.retryOf || ctx.data?._retryOf || null,
825
1060
  retryMode: extra.retryMode || null,
1061
+ revisionReason: extra.revisionReason || null,
826
1062
  createdAt: nowIso,
827
1063
  updatedAt: nowIso,
828
1064
  status: WorkflowStatus.RUNNING,
1065
+ revisions: [],
829
1066
  counts: {
830
1067
  total: Array.isArray(def?.nodes) ? def.nodes.length : 0,
831
1068
  pending: Array.isArray(def?.nodes) ? def.nodes.length : 0,
@@ -834,6 +1071,7 @@ export class WorkflowEngine extends EventEmitter {
834
1071
  failed: 0,
835
1072
  skipped: 0,
836
1073
  },
1074
+ edges,
837
1075
  nodes: Object.fromEntries(
838
1076
  (def?.nodes || []).map((node) => [
839
1077
  node.id,
@@ -846,6 +1084,8 @@ export class WorkflowEngine extends EventEmitter {
846
1084
  attempts: 0,
847
1085
  lastError: null,
848
1086
  outputSummary: null,
1087
+ issueFindings: [],
1088
+ completionEvidence: [],
849
1089
  startedAt: null,
850
1090
  endedAt: null,
851
1091
  updatedAt: nowIso,
@@ -860,6 +1100,63 @@ export class WorkflowEngine extends EventEmitter {
860
1100
  return dagState;
861
1101
  }
862
1102
 
1103
+ _recordDagRevision(ctx, revision = {}) {
1104
+ const dagState = ctx?.data?._dagState;
1105
+ if (!dagState || typeof dagState !== "object" || !dagState.nodes || typeof dagState.nodes !== "object") {
1106
+ return null;
1107
+ }
1108
+ if (!Array.isArray(dagState.revisions)) dagState.revisions = [];
1109
+ const nodes = Object.values(dagState.nodes);
1110
+ const counts = {
1111
+ total: nodes.length,
1112
+ pending: nodes.filter((node) => node?.status === NodeStatus.PENDING).length,
1113
+ running: nodes.filter((node) => node?.status === NodeStatus.RUNNING || node?.status === NodeStatus.WAITING).length,
1114
+ completed: nodes.filter((node) => node?.status === NodeStatus.COMPLETED).length,
1115
+ failed: nodes.filter((node) => node?.status === NodeStatus.FAILED).length,
1116
+ skipped: nodes.filter((node) => node?.status === NodeStatus.SKIPPED).length,
1117
+ };
1118
+ const completedNodeIds = nodes.filter((node) => node?.status === NodeStatus.COMPLETED).map((node) => node.nodeId);
1119
+ const failedNodeIds = nodes.filter((node) => node?.status === NodeStatus.FAILED).map((node) => node.nodeId);
1120
+ const pendingNodeIds = nodes
1121
+ .filter((node) => node?.status === NodeStatus.PENDING || node?.status === NodeStatus.WAITING)
1122
+ .map((node) => node.nodeId);
1123
+ const graphBefore = revision.graphBefore && typeof revision.graphBefore === "object"
1124
+ ? revision.graphBefore
1125
+ : (dagState.revisions.length > 0 ? dagState.revisions[dagState.revisions.length - 1]?.graphAfter || null : null);
1126
+ const graphAfter = revision.graphAfter && typeof revision.graphAfter === "object"
1127
+ ? revision.graphAfter
1128
+ : buildDagGraphSnapshot(dagState);
1129
+ const issueFindings = nodes.flatMap((node) => Array.isArray(node?.issueFindings) ? node.issueFindings : []);
1130
+ const completionEvidence = nodes.flatMap((node) => Array.isArray(node?.completionEvidence) ? node.completionEvidence : []);
1131
+ const snapshot = {
1132
+ index: dagState.revisions.length,
1133
+ recordedAt: new Date().toISOString(),
1134
+ reason: revision.reason || "update",
1135
+ sourceRunId: revision.sourceRunId || null,
1136
+ retryMode: dagState.retryMode || null,
1137
+ status: dagState.status || WorkflowStatus.RUNNING,
1138
+ counts,
1139
+ preservedCompletedNodeIds: Array.isArray(revision.preservedCompletedNodeIds)
1140
+ ? [...new Set(revision.preservedCompletedNodeIds.map((id) => String(id || "").trim()).filter(Boolean))]
1141
+ : completedNodeIds,
1142
+ focusNodeIds: Array.isArray(revision.focusNodeIds)
1143
+ ? [...new Set(revision.focusNodeIds.map((id) => String(id || "").trim()).filter(Boolean))]
1144
+ : (failedNodeIds.length ? failedNodeIds : pendingNodeIds),
1145
+ failedNodeIds,
1146
+ pendingNodeIds,
1147
+ issueFindingCount: issueFindings.length,
1148
+ completionEvidenceCount: completionEvidence.length,
1149
+ issueFindingsPreview: issueFindings.slice(0, 6),
1150
+ completionEvidencePreview: completionEvidence.slice(0, 6),
1151
+ graphBefore,
1152
+ graphAfter,
1153
+ edgeCount: Array.isArray(dagState.edges) ? dagState.edges.length : 0,
1154
+ nodeCount: nodes.length,
1155
+ };
1156
+ dagState.revisions.push(snapshot);
1157
+ dagState.updatedAt = snapshot.recordedAt;
1158
+ return snapshot;
1159
+ }
863
1160
  _refreshDagState(ctx, status = null) {
864
1161
  const dagState = ctx?.data?._dagState;
865
1162
  if (!dagState || typeof dagState !== "object" || !dagState.nodes || typeof dagState.nodes !== "object") {
@@ -882,19 +1179,48 @@ export class WorkflowEngine extends EventEmitter {
882
1179
  const pendingNodes = nodes.filter((node) => node?.status === NodeStatus.PENDING || node?.status === NodeStatus.WAITING);
883
1180
  const firstFailed = failedNodes[0] || null;
884
1181
  const firstPending = pendingNodes[0] || null;
1182
+ const issueFindings = nodes.flatMap((node) => Array.isArray(node?.issueFindings) ? node.issueFindings : []);
1183
+ const completionEvidence = nodes.flatMap((node) => Array.isArray(node?.completionEvidence) ? node.completionEvidence : []);
1184
+ const firstFinding = issueFindings[0] || null;
885
1185
 
886
1186
  let recommendedAction = "continue";
887
1187
  let summary = "Workflow is ready to continue.";
888
1188
  if (failedNodes.length > 0) {
889
- recommendedAction = counts.completed > 0 ? "replan_from_failed" : "inspect_failure";
890
- summary = firstFailed?.lastError
891
- ? `Failed at ${firstFailed.label || firstFailed.nodeId}: ${firstFailed.lastError}`
892
- : `Workflow has ${failedNodes.length} failed node(s).`;
1189
+ const preferredAction = firstFinding?.recommendedAction || null;
1190
+ recommendedAction = preferredAction;
1191
+ if (!recommendedAction || (recommendedAction === "inspect_failure" && counts.completed > 0)) {
1192
+ recommendedAction = counts.completed > 0 ? "replan_from_failed" : "inspect_failure";
1193
+ }
1194
+ summary = firstFinding?.summary
1195
+ ? `Failed at ${firstFailed?.label || firstFailed?.nodeId || "a workflow step"}: ${firstFinding.summary}`
1196
+ : (firstFailed?.lastError
1197
+ ? `Failed at ${firstFailed.label || firstFailed.nodeId}: ${firstFailed.lastError}`
1198
+ : `Workflow has ${failedNodes.length} failed node(s).`);
893
1199
  } else if (pendingNodes.length > 0 && counts.completed > 0) {
894
1200
  recommendedAction = "resume_remaining";
895
1201
  summary = `Resume from ${firstPending?.label || firstPending?.nodeId || "the next pending node"}.`;
896
1202
  }
897
1203
 
1204
+ const nextStepGuidance = failedNodes.length > 0
1205
+ ? [
1206
+ recommendedAction === "rerun_same_step"
1207
+ ? "Preserve completed work and rerun the same failed step with the suggested command or fix."
1208
+ : (recommendedAction === "spawn_fix_step"
1209
+ ? "Preserve completed work and insert a targeted fix step before resuming downstream execution."
1210
+ : (recommendedAction === "replan_subgraph"
1211
+ ? "Preserve completed work and replan the impacted downstream subgraph before continuing."
1212
+ : "Preserve completed work and replan from the failed boundary.")),
1213
+ firstFailed?.label ? `Focus next on ${firstFailed.label}.` : null,
1214
+ firstFinding?.suggestedRerun ? `Suggested rerun: ${firstFinding.suggestedRerun}` : null,
1215
+ firstFailed?.lastError ? `Address failure: ${firstFailed.lastError}` : null,
1216
+ ].filter(Boolean).join(" ")
1217
+ : (pendingNodes.length > 0 && counts.completed > 0
1218
+ ? [
1219
+ "Preserve completed work and continue from the next pending node.",
1220
+ firstPending?.label ? `Next step: ${firstPending.label}.` : null,
1221
+ ].filter(Boolean).join(" ")
1222
+ : "Plan is healthy; continue executing the remaining graph.");
1223
+
898
1224
  const issueAdvisor = {
899
1225
  status: dagState.status,
900
1226
  summary,
@@ -902,14 +1228,28 @@ export class WorkflowEngine extends EventEmitter {
902
1228
  failedNodeCount: failedNodes.length,
903
1229
  pendingNodeCount: pendingNodes.length,
904
1230
  completedNodeCount: counts.completed,
905
- suggestedRerun: firstFailed?.suggestedRerun || null,
1231
+ suggestedRerun: firstFailed?.suggestedRerun || firstFinding?.suggestedRerun || null,
1232
+ retryDecisionClass:
1233
+ recommendedAction === "rerun_same_step"
1234
+ ? "rerun_same_step"
1235
+ : (recommendedAction === "spawn_fix_step"
1236
+ ? "spawn_fix_step"
1237
+ : ((recommendedAction === "replan_subgraph" || recommendedAction === "replan_from_failed")
1238
+ ? "replan_entire_subgraph"
1239
+ : "inspect_failure")),
906
1240
  failedNodes: failedNodes.slice(0, 6).map((node) => ({
907
1241
  nodeId: node.nodeId,
908
1242
  label: node.label || null,
909
1243
  error: node.lastError || null,
910
1244
  attempts: node.attempts || 0,
1245
+ issueFindings: Array.isArray(node.issueFindings) ? node.issueFindings.slice(0, 3) : [],
1246
+ completionEvidence: Array.isArray(node.completionEvidence) ? node.completionEvidence.slice(0, 3) : [],
911
1247
  })),
1248
+ issueFindings: issueFindings.slice(0, 8),
1249
+ completionEvidence: completionEvidence.slice(0, 8),
912
1250
  updatedAt: dagState.updatedAt,
1251
+ nextStepGuidance,
1252
+ dagRevisionCount: Array.isArray(dagState.revisions) ? dagState.revisions.length : 0,
913
1253
  };
914
1254
 
915
1255
  ctx.data._issueAdvisor = issueAdvisor;
@@ -920,11 +1260,16 @@ export class WorkflowEngine extends EventEmitter {
920
1260
  ctx.data._plannerFeedback = {
921
1261
  ...existingFeedback,
922
1262
  issueAdvisor,
1263
+ issueAdvisorSummary: [summary, nextStepGuidance].filter(Boolean).join("\n"),
923
1264
  dagStateSummary: {
1265
+ revisionCount: Array.isArray(dagState.revisions) ? dagState.revisions.length : 0,
924
1266
  runId: dagState.runId,
925
1267
  workflowId: dagState.workflowId,
926
1268
  status: dagState.status,
927
1269
  counts,
1270
+ issueFindingCount: issueFindings.length,
1271
+ completionEvidenceCount: completionEvidence.length,
1272
+ graph: buildDagGraphSnapshot(dagState),
928
1273
  },
929
1274
  };
930
1275
  return issueAdvisor;
@@ -989,11 +1334,18 @@ export class WorkflowEngine extends EventEmitter {
989
1334
  } = {}) {
990
1335
  if (!ctx || !node?.id) return;
991
1336
  const timing = ctx.getNodeTiming(node.id) || {};
1337
+ const issueFindings = collectDagIssueFindings(node, result, error ? String(error) : "");
1338
+ const completionEvidence = collectDagCompletionEvidence(node, result);
1339
+ const existingDagNode = ctx.data?._dagState?.nodes?.[node.id] || {};
992
1340
  const nodePatch = cleanObject({
993
1341
  status,
994
1342
  attempts: Number.isFinite(Number(attempt)) ? Number(attempt) : ctx.getRetryCount(node.id),
995
1343
  lastError: error ? String(error) : null,
996
1344
  outputSummary: result !== undefined ? this._summarizeTaskTraceNodeResult(result) : undefined,
1345
+ issueFindings: issueFindings.length > 0 ? issueFindings : (status === NodeStatus.COMPLETED ? [] : (existingDagNode.issueFindings || undefined)),
1346
+ completionEvidence: completionEvidence.length > 0
1347
+ ? [...(Array.isArray(existingDagNode.completionEvidence) ? existingDagNode.completionEvidence : []), ...completionEvidence]
1348
+ : (status === NodeStatus.FAILED ? (existingDagNode.completionEvidence || undefined) : undefined),
997
1349
  startedAt: Number.isFinite(Number(timing.startedAt)) ? new Date(Number(timing.startedAt)).toISOString() : null,
998
1350
  endedAt: Number.isFinite(Number(timing.endedAt)) ? new Date(Number(timing.endedAt)).toISOString() : null,
999
1351
  suggestedRerun:
@@ -1812,6 +2164,7 @@ export class WorkflowEngine extends EventEmitter {
1812
2164
  _parentRunId: runId,
1813
2165
  _rootRunId: originalRootRunId,
1814
2166
  _retryMode: mode,
2167
+ _dagRevisionReason: "retry_replan_subgraph",
1815
2168
  force: true,
1816
2169
  });
1817
2170
  return { retryRunId: ctx.id, mode, originalRunId: runId, ctx };
@@ -1842,8 +2195,11 @@ export class WorkflowEngine extends EventEmitter {
1842
2195
  });
1843
2196
 
1844
2197
  // Pre-populate nodes that already succeeded.
2198
+ const preservedCompletedNodeIds = [];
2199
+ const focusNodeIds = [];
1845
2200
  for (const [nodeId, status] of Object.entries(nodeStatuses)) {
1846
2201
  if (status === NodeStatus.COMPLETED) {
2202
+ preservedCompletedNodeIds.push(nodeId);
1847
2203
  ctx.setNodeStatus(nodeId, NodeStatus.COMPLETED);
1848
2204
  if (nodeOutputs[nodeId] !== undefined) {
1849
2205
  ctx.setNodeOutput(nodeId, nodeOutputs[nodeId]);
@@ -1852,10 +2208,25 @@ export class WorkflowEngine extends EventEmitter {
1852
2208
  status: NodeStatus.COMPLETED,
1853
2209
  result: nodeOutputs[nodeId],
1854
2210
  });
2211
+ } else if (status) {
2212
+ focusNodeIds.push(nodeId);
1855
2213
  }
1856
2214
  // Reset failed / skipped nodes so the DAG will re-run them.
1857
2215
  }
1858
2216
 
2217
+ this._recordDagRevision(ctx, {
2218
+ reason: "retry_resume",
2219
+ sourceRunId: runId,
2220
+ preservedCompletedNodeIds,
2221
+ });
2222
+
2223
+ this._recordDagRevision(ctx, {
2224
+ reason: mode === "from_failed" ? "retry_replan_from_failed" : `retry_${mode}`,
2225
+ sourceRunId: runId,
2226
+ preservedCompletedNodeIds,
2227
+ focusNodeIds,
2228
+ });
2229
+
1859
2230
  const retryRunId = ctx.id;
1860
2231
  this._activeRuns.set(retryRunId, {
1861
2232
  workflowId,
@@ -2062,6 +2433,15 @@ export class WorkflowEngine extends EventEmitter {
2062
2433
  if (issueAdvisor?.recommendedAction === "resume_remaining") {
2063
2434
  mode = "from_failed";
2064
2435
  reason = "issue_advisor.resume_remaining";
2436
+ } else if (issueAdvisor?.recommendedAction === "rerun_same_step") {
2437
+ mode = "from_failed";
2438
+ reason = "issue_advisor.rerun_same_step";
2439
+ } else if (issueAdvisor?.recommendedAction === "spawn_fix_step") {
2440
+ mode = "from_failed";
2441
+ reason = "issue_advisor.spawn_fix_step";
2442
+ } else if (issueAdvisor?.recommendedAction === "replan_subgraph") {
2443
+ mode = "from_scratch";
2444
+ reason = "issue_advisor.replan_subgraph";
2065
2445
  } else if (issueAdvisor?.recommendedAction === "replan_from_failed") {
2066
2446
  mode = "from_scratch";
2067
2447
  reason = "issue_advisor.replan_from_failed";
@@ -2392,6 +2772,7 @@ export class WorkflowEngine extends EventEmitter {
2392
2772
 
2393
2773
  let runs = [...active, ...persisted.filter((run) => !activeRunIds.has(run.runId))];
2394
2774
  if (workflowId) runs = runs.filter((r) => r.workflowId === workflowId);
2775
+ runs = runs.map((run) => this.getRunDetail(run.runId) || run);
2395
2776
  runs.sort((a, b) => Number(b?.startedAt || 0) - Number(a?.startedAt || 0));
2396
2777
  if (hasLimit) {
2397
2778
  return runs.slice(0, Math.floor(normalizedLimit));
@@ -2854,8 +3235,11 @@ export class WorkflowEngine extends EventEmitter {
2854
3235
  // Pre-seed completed nodes from snapshot
2855
3236
  const nodeStatuses = snapshot.nodeStatuses || {};
2856
3237
  const nodeOutputs = snapshot.nodeOutputs || {};
3238
+ const preservedCompletedNodeIds = [];
3239
+ const focusNodeIds = [];
2857
3240
  for (const [nodeId, status] of Object.entries(nodeStatuses)) {
2858
3241
  if (status === "completed") {
3242
+ preservedCompletedNodeIds.push(nodeId);
2859
3243
  ctx.setNodeStatus(nodeId, NodeStatus.COMPLETED);
2860
3244
  if (nodeOutputs[nodeId] !== undefined) {
2861
3245
  ctx.setNodeOutput(nodeId, nodeOutputs[nodeId]);
@@ -2863,6 +3247,13 @@ export class WorkflowEngine extends EventEmitter {
2863
3247
  }
2864
3248
  }
2865
3249
 
3250
+ this._recordDagRevision(ctx, {
3251
+ reason: (opts.mode === "from_failed") ? "retry_replan_from_failed" : `retry_${opts.mode || "snapshot"}`,
3252
+ sourceRunId: snapshotId,
3253
+ preservedCompletedNodeIds,
3254
+ focusNodeIds,
3255
+ });
3256
+
2866
3257
  const retryRunId = ctx.id;
2867
3258
  this._activeRuns.set(retryRunId, {
2868
3259
  workflowId,
@@ -3032,8 +3423,10 @@ export class WorkflowEngine extends EventEmitter {
3032
3423
  // If nodes are already marked COMPLETED in the context (pre-seeded by
3033
3424
  // retryRun), treat them as already executed so the DAG skips them and
3034
3425
  // begins from the first un-completed node.
3426
+ const preservedCompletedNodeIds = [];
3035
3427
  for (const [nodeId, status] of ctx.nodeStatuses) {
3036
3428
  if (status === NodeStatus.COMPLETED) {
3429
+ preservedCompletedNodeIds.push(nodeId);
3037
3430
  executed.add(nodeId);
3038
3431
  }
3039
3432
  }
@@ -3872,6 +4265,8 @@ export class WorkflowEngine extends EventEmitter {
3872
4265
 
3873
4266
  _serializeRunContext(ctx, isRunning = false) {
3874
4267
  const detail = ctx.toJSON(Date.now());
4268
+ if (ctx?.data?._dagState) detail.dagState = ctx.data._dagState;
4269
+ if (ctx?.data?._issueAdvisor) detail.issueAdvisor = ctx.data._issueAdvisor;
3875
4270
  if (isRunning) {
3876
4271
  detail.endedAt = null;
3877
4272
  detail.duration = Math.max(0, Date.now() - Number(ctx?.startedAt || Date.now()));
@@ -3929,6 +4324,8 @@ export class WorkflowEngine extends EventEmitter {
3929
4324
  const retryDecisionReason = detail?.data?._retryDecisionReason || null;
3930
4325
  const issueAdvisorRecommendation = detail?.issueAdvisor?.recommendedAction || null;
3931
4326
  const issueAdvisorSummary = detail?.issueAdvisor?.summary || null;
4327
+ const dagRevisionCount = Array.isArray(detail?.dagState?.revisions) ? detail.dagState.revisions.length : 0;
4328
+ const validationFailures = collectValidationFailures(detail);
3932
4329
 
3933
4330
  return {
3934
4331
  runId,
@@ -3962,6 +4359,13 @@ export class WorkflowEngine extends EventEmitter {
3962
4359
  retryDecisionReason,
3963
4360
  issueAdvisorRecommendation,
3964
4361
  issueAdvisorSummary,
4362
+ dagRevisionCount,
4363
+ ...(validationFailures.length > 0
4364
+ ? {
4365
+ validationFailures,
4366
+ latestValidationFailure: validationFailures.at(-1) || null,
4367
+ }
4368
+ : {}),
3965
4369
  };
3966
4370
  }
3967
4371
 
@@ -4546,3 +4950,4 @@ export function getWorkflow(id, opts) { return getWorkflowEngine(opts).get(id);
4546
4950
  export async function executeWorkflow(id, data, opts) { return getWorkflowEngine(opts).execute(id, data, opts); }
4547
4951
  export async function retryWorkflowRun(runId, retryOpts, engineOpts) { return getWorkflowEngine(engineOpts).retryRun(runId, retryOpts); }
4548
4952
 
4953
+