@agwab/pi-workflow 0.1.1 → 0.2.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 (70) hide show
  1. package/README.md +20 -15
  2. package/agents/researcher.md +17 -7
  3. package/dist/artifact-graph-runtime.js +1 -0
  4. package/dist/compiler.d.ts +2 -0
  5. package/dist/compiler.js +29 -4
  6. package/dist/dynamic-generated-task-runtime.js +4 -3
  7. package/dist/dynamic-runtime-bundle.js +3 -2
  8. package/dist/engine.d.ts +2 -0
  9. package/dist/engine.js +3 -2
  10. package/dist/extension.js +240 -16
  11. package/dist/store.js +1 -0
  12. package/dist/subagent-backend.js +82 -27
  13. package/dist/tool-metadata.d.ts +1 -0
  14. package/dist/tool-metadata.js +13 -1
  15. package/dist/types.d.ts +3 -0
  16. package/dist/workflow-artifact-extension.js +3 -2
  17. package/dist/workflow-artifact-tool.js +84 -4
  18. package/dist/workflow-progress-health.d.ts +37 -0
  19. package/dist/workflow-progress-health.js +296 -0
  20. package/dist/workflow-runtime.d.ts +6 -0
  21. package/dist/workflow-runtime.js +33 -10
  22. package/dist/workflow-view.d.ts +2 -0
  23. package/dist/workflow-view.js +97 -18
  24. package/dist/workflow-web-source-extension.d.ts +43 -0
  25. package/dist/workflow-web-source-extension.js +1194 -0
  26. package/dist/workflow-web-source.d.ts +171 -0
  27. package/dist/workflow-web-source.js +915 -0
  28. package/docs/usage.md +32 -18
  29. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  30. package/node_modules/@agwab/pi-subagent/src/api.ts +245 -132
  31. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +243 -163
  32. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +117 -90
  33. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +728 -475
  34. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +305 -209
  35. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +750 -439
  36. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +422 -268
  37. package/package.json +7 -7
  38. package/skills/workflow-guide/scaffolds/object-tool-fallback/schemas/fetch-control.schema.json +1 -1
  39. package/skills/workflow-guide/scaffolds/object-tool-fallback/spec.json +4 -3
  40. package/src/artifact-graph-runtime.ts +1 -0
  41. package/src/compiler.ts +43 -3
  42. package/src/dynamic-generated-task-runtime.ts +4 -2
  43. package/src/dynamic-runtime-bundle.ts +3 -2
  44. package/src/engine.ts +7 -16
  45. package/src/extension.ts +299 -22
  46. package/src/store.ts +1 -0
  47. package/src/subagent-backend.ts +121 -37
  48. package/src/tool-metadata.ts +22 -1
  49. package/src/types.ts +4 -0
  50. package/src/workflow-artifact-extension.ts +3 -2
  51. package/src/workflow-artifact-tool.ts +96 -4
  52. package/src/workflow-progress-health.ts +461 -0
  53. package/src/workflow-runtime.ts +50 -13
  54. package/src/workflow-view.ts +186 -41
  55. package/src/workflow-web-source-extension.ts +1411 -0
  56. package/src/workflow-web-source.ts +1294 -0
  57. package/workflows/README.md +1 -1
  58. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +552 -44
  59. package/workflows/deep-research/helpers/final-audit-packet.mjs +396 -0
  60. package/workflows/deep-research/helpers/normalize-input-packet.mjs +545 -0
  61. package/workflows/deep-research/helpers/render-executive.mjs +1199 -192
  62. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +624 -0
  63. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +37 -8
  64. package/workflows/deep-research/schemas/deep-research-final-synthesis-control.schema.json +110 -0
  65. package/workflows/deep-research/schemas/deep-research-normalize-claims-control.schema.json +45 -4
  66. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +0 -2
  67. package/workflows/deep-research/spec.json +71 -26
  68. package/workflows/deep-review/helpers/render-review-report.mjs +502 -0
  69. package/workflows/deep-review/schemas/deep-review-render-control.schema.json +50 -0
  70. package/workflows/deep-review/spec.json +22 -1
@@ -17,9 +17,16 @@ export interface WorkflowRuntimeDefaults {
17
17
  thinking?: ThinkingLevel;
18
18
  }
19
19
 
20
+ export interface WorkflowRuntimeThinkingResolution {
21
+ requested?: ThinkingLevel;
22
+ resolved?: ThinkingLevel;
23
+ reason?: string;
24
+ }
25
+
20
26
  export interface WorkflowRuntimeResolutionInput {
21
27
  model?: string;
22
28
  thinking?: ThinkingLevel;
29
+ thinkingResolution?: WorkflowRuntimeThinkingResolution;
23
30
  }
24
31
 
25
32
  export interface WorkflowRuntimeResolutionContext {
@@ -66,7 +73,7 @@ export async function resolveWorkflowRuntime(
66
73
  const model = await resolveModel(baseModel, context, options);
67
74
  const effectiveThinking =
68
75
  runtime.thinking ?? thinking ?? options.defaults?.thinking;
69
- const resolvedThinking = await resolveThinking(
76
+ const thinkingResolution = await resolveThinking(
70
77
  model,
71
78
  effectiveThinking,
72
79
  context,
@@ -74,7 +81,10 @@ export async function resolveWorkflowRuntime(
74
81
  );
75
82
  return {
76
83
  ...(model ? { model } : {}),
77
- ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
84
+ ...(thinkingResolution?.resolved
85
+ ? { thinking: thinkingResolution.resolved }
86
+ : {}),
87
+ ...(thinkingResolution ? { thinkingResolution } : {}),
78
88
  };
79
89
  }
80
90
 
@@ -208,36 +218,63 @@ async function resolveThinking(
208
218
  requested: ThinkingLevel | undefined,
209
219
  context: WorkflowRuntimeResolutionContext,
210
220
  options: ResolveWorkflowRuntimeOptions,
211
- ): Promise<ThinkingLevel | undefined> {
221
+ ): Promise<WorkflowRuntimeThinkingResolution | undefined> {
212
222
  if (!requested) return undefined;
213
223
  const model = findModelInfo(modelId, options.availableModels ?? []);
214
224
  const supported = getSupportedThinkingLevels(model);
215
- if (supported.includes(requested)) return requested;
225
+ if (supported.includes(requested)) {
226
+ return { requested, resolved: requested };
227
+ }
216
228
 
217
- if (!options.prompt) {
218
- const modelLabel = modelId ?? "selected model";
229
+ if (supported.length === 0) {
219
230
  throw new Error(
220
- `${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}. Supported: ${supported.join(", ") || "none"}`,
231
+ `${modelId ?? "selected model"} does not expose any supported reasoning levels for ${context.taskKey}`,
221
232
  );
222
233
  }
223
234
 
224
- if (supported.length === 0) {
235
+ const downgradeOptions = lowerOrEqualSupportedThinking(requested, supported);
236
+ if (downgradeOptions.length === 0) {
237
+ const modelLabel = modelId ?? "selected model";
225
238
  throw new Error(
226
- `${modelId ?? "selected model"} does not expose any supported reasoning levels for ${context.taskKey}`,
239
+ `${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}, and no lower-or-equal fallback is available. Supported: ${supported.join(", ") || "none"}`,
227
240
  );
228
241
  }
229
242
 
243
+ if (!options.prompt) {
244
+ const resolved = downgradeOptions[downgradeOptions.length - 1]!;
245
+ return {
246
+ requested,
247
+ resolved,
248
+ reason: `requested ${requested} is unsupported by ${modelId ?? "selected model"}; using ${resolved}`,
249
+ };
250
+ }
251
+
230
252
  const selected = await options.prompt.select(
231
- `${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported level.`,
232
- supported,
253
+ `${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported lower-or-equal level.`,
254
+ downgradeOptions,
233
255
  );
234
256
  if (!selected)
235
257
  throw new Error(`Reasoning selection cancelled for ${context.taskKey}`);
236
- if (!isThinkingLevel(selected))
258
+ if (!isThinkingLevel(selected) || !downgradeOptions.includes(selected))
237
259
  throw new Error(
238
260
  `Invalid reasoning selection "${selected}" for ${context.taskKey}`,
239
261
  );
240
- return selected;
262
+ return {
263
+ requested,
264
+ resolved: selected,
265
+ reason: `selected supported reasoning ${selected} for unsupported request ${requested}`,
266
+ };
267
+ }
268
+
269
+ function lowerOrEqualSupportedThinking(
270
+ requested: ThinkingLevel,
271
+ supported: ThinkingLevel[],
272
+ ): ThinkingLevel[] {
273
+ const requestedIndex = THINKING_LEVELS.indexOf(requested);
274
+ if (requestedIndex < 0) return [];
275
+ return THINKING_LEVELS.slice(0, requestedIndex + 1).filter((level) =>
276
+ supported.includes(level),
277
+ );
241
278
  }
242
279
 
243
280
  function findModelInfo(
@@ -9,6 +9,11 @@ import {
9
9
  readIndex,
10
10
  readRunRecord,
11
11
  } from "./store.js";
12
+ import {
13
+ diagnoseWorkflowRunHealth,
14
+ diagnoseWorkflowTaskHealth,
15
+ type WorkflowProgressHealth,
16
+ } from "./workflow-progress-health.js";
12
17
  import {
13
18
  type WorkflowIndexRecord,
14
19
  type WorkflowRunRecord,
@@ -341,7 +346,8 @@ export class WorkflowView implements Component {
341
346
  }
342
347
 
343
348
  private renderDrilldownHeader(width: number): string[] {
344
- const taskSummary = this.mode !== "runs" ? this.detailRun?.taskSummary : undefined;
349
+ const taskSummary =
350
+ this.mode !== "runs" ? this.detailRun?.taskSummary : undefined;
345
351
  const active = taskSummary
346
352
  ? taskSummary.running
347
353
  : this.flows.filter((flow) => flow.status === "running").length;
@@ -371,6 +377,10 @@ export class WorkflowView implements Component {
371
377
 
372
378
  private renderRunsScreen(width: number): string[] {
373
379
  const selected = this.flows[this.selectedFlow];
380
+ const selectedDetail =
381
+ selected && this.detailRun?.runId === selected.runId
382
+ ? this.detailRun
383
+ : undefined;
374
384
  const sideLines = [
375
385
  accent(this.theme, "All runs"),
376
386
  kvRow(this.theme, "total", String(this.flows.length)),
@@ -379,10 +389,19 @@ export class WorkflowView implements Component {
379
389
  "running",
380
390
  String(this.flows.filter((flow) => flow.status === "running").length),
381
391
  ),
392
+ kvRow(
393
+ this.theme,
394
+ "needs action",
395
+ String(
396
+ this.flows.filter((flow) =>
397
+ ["failed", "blocked", "interrupted"].includes(flow.status),
398
+ ).length,
399
+ ),
400
+ ),
382
401
  "",
383
402
  accent(this.theme, "Selected"),
384
403
  ...(selected
385
- ? this.runSummaryLines(selected)
404
+ ? this.runSummaryLines(selected, selectedDetail)
386
405
  : [placeholder(this.theme, "none")]),
387
406
  ];
388
407
  return this.renderTwoPane(
@@ -480,13 +499,14 @@ export class WorkflowView implements Component {
480
499
  run: WorkflowRunRecord,
481
500
  task: WorkflowTaskRunRecord,
482
501
  ): string[] {
502
+ const taskHealth = diagnoseWorkflowTaskHealth(task, run);
483
503
  const lines = [
484
504
  ...boxed(
485
505
  this.theme,
486
506
  "Task Detail",
487
507
  width,
488
508
  [
489
- `${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${muted(this.theme, this.breadcrumbText())}`,
509
+ `${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${healthInline(this.theme, taskHealth)} ${muted(this.theme, this.breadcrumbText())}`,
490
510
  taskMetaLine(this.theme, [
491
511
  ["agent", task.agent],
492
512
  ["stage", task.stageId ?? "(none)"],
@@ -499,6 +519,20 @@ export class WorkflowView implements Component {
499
519
  "",
500
520
  ];
501
521
 
522
+ const healthLines = this.taskHealthLines(taskHealth, width - 4);
523
+ if (healthLines.length > 0) {
524
+ lines.push(
525
+ ...boxed(
526
+ this.theme,
527
+ "Health",
528
+ width,
529
+ healthLines,
530
+ healthColor(taskHealth),
531
+ ),
532
+ "",
533
+ );
534
+ }
535
+
502
536
  const validationLines = this.taskValidationStripLines(task, width - 4);
503
537
  if (validationLines.length > 0) {
504
538
  lines.push(
@@ -578,7 +612,9 @@ export class WorkflowView implements Component {
578
612
  );
579
613
  const statusWidth = Math.max(
580
614
  7,
581
- ...window.rows.map(({ item }) => visibleWidth(statusLabelText(runStatusLabel(item)))),
615
+ ...window.rows.map(({ item }) =>
616
+ visibleWidth(statusLabelText(runStatusLabel(item))),
617
+ ),
582
618
  );
583
619
  for (const { item: flow, index } of window.rows) {
584
620
  const selected = index === this.selectedFlow;
@@ -587,21 +623,19 @@ export class WorkflowView implements Component {
587
623
  const name = flow.name ?? flow.type;
588
624
  const left = `${prefix}${marker} ${selected ? strong(this.theme, name) : name}`;
589
625
  const runIdText = shortId(flow.runId).slice(0, 16).padEnd(16, " ");
590
- const runningText =
591
- flow.taskSummary.running > 0
592
- ? ` ${muted(this.theme, "·")} ${metaLabel(this.theme, "running")} ${metaValue(this.theme, String(flow.taskSummary.running))}`
593
- : "";
626
+ const detailRun =
627
+ this.detailRun?.runId === flow.runId ? this.detailRun : undefined;
628
+ const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
629
+ const healthText =
630
+ health.state === "completed"
631
+ ? ""
632
+ : ` ${muted(this.theme, "·")} ${healthLabel(this.theme, health)}`;
594
633
  const baseRight = `${statusColumn(this.theme, flow.status, runStatusLabel(flow), statusWidth)} ${progressBar(this.theme, flow.taskSummary, 5)} ${metaValue(this.theme, runIdText)}`;
595
634
  const right =
596
635
  width >= 90
597
- ? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${runningText}`
598
- : `${baseRight}${runningText}`;
599
- const line = joinColumns(
600
- left,
601
- right,
602
- width,
603
- 17,
604
- );
636
+ ? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${healthText}`
637
+ : `${baseRight}${healthText}`;
638
+ const line = joinColumns(left, right, width, 17);
605
639
  lines.push(selectedLine(this.theme, line, width, selected, true));
606
640
  }
607
641
  if (window.hiddenAfter > 0)
@@ -667,7 +701,11 @@ export class WorkflowView implements Component {
667
701
  const selected = index === this.selectedTask;
668
702
  const prefix = selected ? accent(this.theme, "› ") : " ";
669
703
  const left = `${prefix}${statusGlyph(this.theme, task.status)} ${selected ? strong(this.theme, task.displayName) : task.displayName}`;
670
- const right = taskListStatusLabel(this.theme, task);
704
+ const right = taskListStatusLabel(
705
+ this.theme,
706
+ task,
707
+ diagnoseWorkflowTaskHealth(task, run),
708
+ );
671
709
  const line = joinColumns(
672
710
  left,
673
711
  metaByStatus(this.theme, task.status, right),
@@ -685,7 +723,6 @@ export class WorkflowView implements Component {
685
723
  : [placeholder(this.theme, " no tasks in selected stage")];
686
724
  }
687
725
 
688
-
689
726
  private taskIdentityLines(
690
727
  run: WorkflowRunRecord,
691
728
  task: WorkflowTaskRunRecord,
@@ -762,6 +799,48 @@ export class WorkflowView implements Component {
762
799
  return lines.map((line) => fit(line, width));
763
800
  }
764
801
 
802
+ private taskHealthLines(
803
+ health: WorkflowProgressHealth,
804
+ width: number,
805
+ ): string[] {
806
+ if (health.state === "completed" || health.state === "pending") return [];
807
+ const lines = [
808
+ `${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
809
+ kvRow(this.theme, "suggested", health.suggestion, healthColor(health)),
810
+ kvRow(this.theme, "why", health.reason),
811
+ ];
812
+ if (health.currentTask?.elapsedMs !== undefined)
813
+ lines.splice(
814
+ 1,
815
+ 0,
816
+ kvRow(
817
+ this.theme,
818
+ "elapsed",
819
+ formatDuration(health.currentTask.elapsedMs),
820
+ ),
821
+ );
822
+ if (health.durationClass)
823
+ lines.push(
824
+ kvRow(this.theme, "duration", `${health.durationClass} expected`),
825
+ );
826
+ if (health.heartbeatAgeMs !== undefined)
827
+ lines.push(
828
+ kvRow(
829
+ this.theme,
830
+ "heartbeat",
831
+ `${formatDuration(health.heartbeatAgeMs)} ago`,
832
+ ),
833
+ );
834
+ if (health.lastActivityAgeMs !== undefined)
835
+ lines.push(
836
+ kvRow(
837
+ this.theme,
838
+ "activity",
839
+ `${formatDuration(health.lastActivityAgeMs)} ago`,
840
+ ),
841
+ );
842
+ return lines.map((line) => fit(line, width));
843
+ }
765
844
 
766
845
  private taskValidationStripLines(
767
846
  task: WorkflowTaskRunRecord,
@@ -770,10 +849,7 @@ export class WorkflowView implements Component {
770
849
  const summary = taskValidationSummary(task);
771
850
  if (!summary) return [];
772
851
  return [
773
- fit(
774
- validationLine(this.theme, summary.status, summary.message),
775
- width,
776
- ),
852
+ fit(validationLine(this.theme, summary.status, summary.message), width),
777
853
  ];
778
854
  }
779
855
 
@@ -790,9 +866,7 @@ export class WorkflowView implements Component {
790
866
  const maxStart = Math.max(0, total - TASK_ARTIFACT_VIEW_LINES);
791
867
  const start = Math.min(this.artifactScrollLine, maxStart);
792
868
  const end =
793
- total === 0
794
- ? 0
795
- : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
869
+ total === 0 ? 0 : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
796
870
  const visible =
797
871
  total === 0
798
872
  ? [
@@ -862,7 +936,6 @@ export class WorkflowView implements Component {
862
936
  this.artifactScrollLine = 0;
863
937
  }
864
938
 
865
-
866
939
  private moveModeSelection(delta: number): void {
867
940
  if (this.mode === "runs") {
868
941
  this.moveRun(delta);
@@ -1005,11 +1078,11 @@ export class WorkflowView implements Component {
1005
1078
 
1006
1079
  private syncSelectedTaskId(tasks?: WorkflowTaskRunRecord[]): void {
1007
1080
  const stageTasks =
1008
- tasks ?? (this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
1081
+ tasks ??
1082
+ (this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
1009
1083
  this.selectedTaskId = stageTasks[this.selectedTask]?.taskId ?? "";
1010
1084
  }
1011
1085
 
1012
-
1013
1086
  private breadcrumbText(): string {
1014
1087
  const parts = ["workflow"];
1015
1088
  const flow = this.flows[this.selectedFlow];
@@ -1025,7 +1098,11 @@ export class WorkflowView implements Component {
1025
1098
  return parts.join(" › ");
1026
1099
  }
1027
1100
 
1028
- private runSummaryLines(flow: WorkflowSummary): string[] {
1101
+ private runSummaryLines(
1102
+ flow: WorkflowSummary,
1103
+ detailRun?: WorkflowRunRecord,
1104
+ ): string[] {
1105
+ const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
1029
1106
  return [
1030
1107
  `${statusGlyph(this.theme, flow.status)} ${strong(this.theme, flow.name ?? flow.type)} ${statusBadge(this.theme, flow.status, runStatusLabel(flow))}`,
1031
1108
  progressBar(this.theme, flow.taskSummary, 8),
@@ -1048,10 +1125,43 @@ export class WorkflowView implements Component {
1048
1125
  ),
1049
1126
  ],
1050
1127
  ]),
1128
+ ...this.runHealthLines(health),
1051
1129
  ];
1052
1130
  }
1053
1131
 
1132
+ private runHealthLines(health: WorkflowProgressHealth): string[] {
1133
+ if (health.state === "completed") return [];
1134
+ const lines = [
1135
+ "",
1136
+ accent(this.theme, "Health"),
1137
+ `${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
1138
+ ];
1139
+ if (health.currentTask?.displayName)
1140
+ lines.push(kvRow(this.theme, "current", health.currentTask.displayName));
1141
+ if (health.lastActivityAgeMs !== undefined)
1142
+ lines.push(
1143
+ kvRow(
1144
+ this.theme,
1145
+ "activity",
1146
+ `${formatDuration(health.lastActivityAgeMs)} ago`,
1147
+ ),
1148
+ );
1149
+ if (health.heartbeatAgeMs !== undefined)
1150
+ lines.push(
1151
+ kvRow(
1152
+ this.theme,
1153
+ "heartbeat",
1154
+ `${formatDuration(health.heartbeatAgeMs)} ago`,
1155
+ ),
1156
+ );
1157
+ lines.push(
1158
+ kvRow(this.theme, "suggested", health.suggestion, healthColor(health)),
1159
+ );
1160
+ return lines;
1161
+ }
1162
+
1054
1163
  private runDetailSummaryLines(run: WorkflowRunRecord): string[] {
1164
+ const health = diagnoseWorkflowRunHealth(run);
1055
1165
  const lines = [
1056
1166
  `${statusGlyph(this.theme, run.status)} ${strong(this.theme, run.name ?? run.type)} ${statusBadge(this.theme, run.status)}`,
1057
1167
  progressBar(this.theme, run.taskSummary, 10),
@@ -1071,6 +1181,7 @@ export class WorkflowView implements Component {
1071
1181
  ["updated", timestampText(run.updatedAt)],
1072
1182
  ]),
1073
1183
  kvRow(this.theme, "run", shortId(run.runId)),
1184
+ ...this.runHealthLines(health),
1074
1185
  ];
1075
1186
  if (run.fanout && run.fanout.length > 0) {
1076
1187
  lines.push("", accent(this.theme, "Fanout"));
@@ -1197,7 +1308,6 @@ function runToSummary(cwd: string, run: WorkflowRunRecord): WorkflowSummary {
1197
1308
  };
1198
1309
  }
1199
1310
 
1200
-
1201
1311
  async function readFileLinesBounded(
1202
1312
  cwd: string,
1203
1313
  projectPath: string | undefined,
@@ -1255,7 +1365,9 @@ function summarizeTasks(tasks: WorkflowTaskRunRecord[]): TaskSummary {
1255
1365
  return summary;
1256
1366
  }
1257
1367
 
1258
- function statusForSummary(summary: TaskSummary): WorkflowRunStatus | TaskRunStatus {
1368
+ function statusForSummary(
1369
+ summary: TaskSummary,
1370
+ ): WorkflowRunStatus | TaskRunStatus {
1259
1371
  if (summary.running > 0) return "running";
1260
1372
  if (summary.blocked > 0) return "blocked";
1261
1373
  if (summary.failed > 0 || summary.interrupted > 0) return "failed";
@@ -1364,7 +1476,10 @@ function statusColumn(
1364
1476
  width: number,
1365
1477
  ): string {
1366
1478
  const normalized = statusLabelText(label);
1367
- return padAnsi(fg(theme, statusColor(status), strong(theme, normalized)), width);
1479
+ return padAnsi(
1480
+ fg(theme, statusColor(status), strong(theme, normalized)),
1481
+ width,
1482
+ );
1368
1483
  }
1369
1484
 
1370
1485
  function statusLabelText(label: string): string {
@@ -1384,9 +1499,10 @@ function progressBar(
1384
1499
  cells: number,
1385
1500
  ): string {
1386
1501
  const safeCells = Math.max(1, cells);
1387
- const visibleProgress = summary.running > 0
1388
- ? summary.completed + summary.running
1389
- : summary.completed;
1502
+ const visibleProgress =
1503
+ summary.running > 0
1504
+ ? summary.completed + summary.running
1505
+ : summary.completed;
1390
1506
  const filled =
1391
1507
  summary.total <= 0
1392
1508
  ? 0
@@ -1420,9 +1536,31 @@ function statusText(status: WorkflowRunStatus | TaskRunStatus): string {
1420
1536
  return status;
1421
1537
  }
1422
1538
 
1539
+ function healthColor(health: WorkflowProgressHealth): string {
1540
+ return health.tone;
1541
+ }
1542
+
1543
+ function healthGlyph(theme: Theme, health: WorkflowProgressHealth): string {
1544
+ if (health.tone === "success") return success(theme, "✓");
1545
+ if (health.tone === "warning") return warning(theme, "●");
1546
+ if (health.tone === "error") return errorText(theme, "●");
1547
+ if (health.tone === "dim") return muted(theme, "•");
1548
+ return accent(theme, "●");
1549
+ }
1550
+
1551
+ function healthLabel(theme: Theme, health: WorkflowProgressHealth): string {
1552
+ return fg(theme, healthColor(health), strong(theme, health.label));
1553
+ }
1554
+
1555
+ function healthInline(theme: Theme, health: WorkflowProgressHealth): string {
1556
+ if (health.state === "completed" || health.state === "pending") return "";
1557
+ return `${healthGlyph(theme, health)} ${healthLabel(theme, health)}`;
1558
+ }
1559
+
1423
1560
  function taskListStatusLabel(
1424
1561
  theme: Theme,
1425
1562
  task: WorkflowTaskRunRecord,
1563
+ health: WorkflowProgressHealth,
1426
1564
  ): string {
1427
1565
  const validation = taskValidationSummary(task);
1428
1566
  const label =
@@ -1432,8 +1570,14 @@ function taskListStatusLabel(
1432
1570
  ? "valid"
1433
1571
  : task.status === "completed"
1434
1572
  ? "done"
1435
- : statusText(task.status);
1436
- return fg(theme, statusColor(task.status), strong(theme, label));
1573
+ : task.status === "running"
1574
+ ? health.label
1575
+ : statusText(task.status);
1576
+ const suffix =
1577
+ task.status === "running" && health.currentTask?.elapsedMs !== undefined
1578
+ ? ` ${muted(theme, "·")} ${metaValue(theme, formatDuration(health.currentTask.elapsedMs))}`
1579
+ : "";
1580
+ return `${fg(theme, task.status === "running" ? healthColor(health) : statusColor(task.status), strong(theme, label))}${suffix}`;
1437
1581
  }
1438
1582
 
1439
1583
  function compactStatusLabel(
@@ -1452,7 +1596,6 @@ function shortId(runId: string): string {
1452
1596
  return runId.replace(/^workflow_/, "workflow_").slice(0, 24);
1453
1597
  }
1454
1598
 
1455
-
1456
1599
  function visibleWindow<T>(
1457
1600
  items: T[],
1458
1601
  selectedIndex: number,
@@ -1518,7 +1661,7 @@ function taskValidationSummary(
1518
1661
  const issueMessage =
1519
1662
  typeof issue === "string"
1520
1663
  ? issue
1521
- : issue?.message ?? issue?.path ?? issue?.code ?? "";
1664
+ : (issue?.message ?? issue?.path ?? issue?.code ?? "");
1522
1665
  const message = validation.message ?? validation.reason ?? issueMessage;
1523
1666
  if (status === "valid" && !message) return undefined;
1524
1667
  return { status, message };
@@ -1726,7 +1869,10 @@ function truncateToWidth(text: string, width: number): string {
1726
1869
  if (safeWidth === 0) return "";
1727
1870
  if (visibleWidth(text) <= safeWidth) return text;
1728
1871
 
1729
- const hasAnsi = text.includes("\u001b[") || text.includes("\u001b]") || text.includes("\u001b_");
1872
+ const hasAnsi =
1873
+ text.includes("\u001b[") ||
1874
+ text.includes("\u001b]") ||
1875
+ text.includes("\u001b_");
1730
1876
  const ellipsis = "…";
1731
1877
  const ellipsisWidth = visibleWidth(ellipsis);
1732
1878
  const limit = Math.max(0, safeWidth - ellipsisWidth);
@@ -1905,7 +2051,6 @@ function pathText(theme: Theme, projectPath: string): string {
1905
2051
  return `${dim(theme, projectPath.slice(0, lastSlash + 1))}${metaValue(theme, projectPath.slice(lastSlash + 1))}`;
1906
2052
  }
1907
2053
 
1908
-
1909
2054
  function navHint(theme: Theme, text: string): string {
1910
2055
  return text
1911
2056
  .split(" · ")