@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
@@ -0,0 +1,296 @@
1
+ const ACTIVE_ACTIVITY_MS = 2 * 60_000;
2
+ const LONG_TAIL_ELAPSED_MS = 8 * 60_000;
3
+ const STALL_BY_DURATION = {
4
+ short: 5 * 60_000,
5
+ medium: 10 * 60_000,
6
+ long: 20 * 60_000,
7
+ };
8
+ const STUCK_BY_DURATION = {
9
+ short: 15 * 60_000,
10
+ medium: 30 * 60_000,
11
+ long: 60 * 60_000,
12
+ };
13
+ export function diagnoseWorkflowRunHealth(run, options = {}) {
14
+ const nowMs = options.nowMs ?? Date.now();
15
+ const runningTask = currentRunningTask(run.tasks ?? []);
16
+ if (runningTask)
17
+ return diagnoseWorkflowTaskHealth(runningTask, run, { nowMs });
18
+ const problem = (run.tasks ?? []).find((task) => isProblemStatus(task.status));
19
+ if (problem)
20
+ return problemRunHealth(problem, nowMs);
21
+ if (isProblemStatus(run.status))
22
+ return problemWorkflowHealth(run.status);
23
+ if (run.status === "completed")
24
+ return completedWorkflowHealth();
25
+ return waitingWorkflowHealth(run, nowMs);
26
+ }
27
+ export function diagnoseWorkflowTaskHealth(task, run, options = {}) {
28
+ const nowMs = options.nowMs ?? Date.now();
29
+ if (task.status !== "running")
30
+ return terminalTaskHealth(task, nowMs);
31
+ return runningTaskHealth(runningContext(task, run, nowMs));
32
+ }
33
+ export function classifyWorkflowTaskDuration(task) {
34
+ const text = [
35
+ task.stageId,
36
+ task.displayName,
37
+ task.specId,
38
+ task.kind,
39
+ task.statusDetail,
40
+ ]
41
+ .filter(Boolean)
42
+ .join(" ")
43
+ .toLowerCase();
44
+ if (/\b(render|helper|support|schema|partition|gate)\b/.test(text))
45
+ return "short";
46
+ if (/\b(research|audit|synthesis|review|verify|verifier|normalize|plan|impact|spec)\b/.test(text))
47
+ return "long";
48
+ const maxRuntimeMs = task.runtime?.maxRuntimeMs;
49
+ if (maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs)) {
50
+ if (maxRuntimeMs <= 5 * 60_000)
51
+ return "short";
52
+ if (maxRuntimeMs >= 60 * 60_000)
53
+ return "long";
54
+ }
55
+ return "medium";
56
+ }
57
+ function currentRunningTask(tasks) {
58
+ return tasks
59
+ .filter((task) => task.status === "running")
60
+ .sort((left, right) => (parseTime(left.startedAt) ?? Number.POSITIVE_INFINITY) -
61
+ (parseTime(right.startedAt) ?? Number.POSITIVE_INFINITY))[0];
62
+ }
63
+ function completedWorkflowHealth() {
64
+ return {
65
+ state: "completed",
66
+ label: "completed",
67
+ summary: "run completed",
68
+ tone: "success",
69
+ suggestion: "review",
70
+ reason: "all tasks reached a terminal successful state",
71
+ };
72
+ }
73
+ function problemWorkflowHealth(status) {
74
+ return {
75
+ state: "needs-action",
76
+ label: "needs action",
77
+ summary: `run ${status}`,
78
+ tone: "error",
79
+ suggestion: "inspect",
80
+ reason: `workflow status is ${status}`,
81
+ };
82
+ }
83
+ function problemRunHealth(task, nowMs) {
84
+ return {
85
+ state: "needs-action",
86
+ label: "needs action",
87
+ summary: `${task.displayName ?? task.taskId ?? "task"} needs attention`,
88
+ tone: "error",
89
+ suggestion: "inspect",
90
+ reason: task.lastMessage ?? task.statusDetail ?? "task did not complete",
91
+ currentTask: taskSummary(task, nowMs),
92
+ };
93
+ }
94
+ function waitingWorkflowHealth(run, nowMs) {
95
+ const hasPending = run.taskSummary.pending > 0;
96
+ return {
97
+ state: hasPending ? "pending" : "active",
98
+ label: hasPending ? "pending" : "active",
99
+ summary: hasPending
100
+ ? "waiting for the next schedulable task"
101
+ : "run is active",
102
+ tone: hasPending ? "dim" : "accent",
103
+ suggestion: "wait",
104
+ reason: hasPending
105
+ ? "no task is currently running"
106
+ : "workflow is still in progress",
107
+ lastActivityAt: run.updatedAt,
108
+ lastActivityAgeMs: ageMs(run.updatedAt, nowMs),
109
+ };
110
+ }
111
+ function runningTaskHealth(context) {
112
+ return (runtimeExceededHealth(context) ??
113
+ staleRunningHealth(context) ??
114
+ longTailHealth(context) ??
115
+ activeRunningHealth(context));
116
+ }
117
+ function runtimeExceededHealth(context) {
118
+ const maxRuntimeMs = context.task.runtime?.maxRuntimeMs;
119
+ const hasElapsed = context.elapsedMs !== undefined;
120
+ const hasBudget = maxRuntimeMs !== undefined && Number.isFinite(maxRuntimeMs);
121
+ if (!hasElapsed || !hasBudget)
122
+ return undefined;
123
+ if (maxRuntimeMs <= 0 || context.elapsedMs <= maxRuntimeMs)
124
+ return undefined;
125
+ return runningHealth(context, {
126
+ state: "likely-stuck",
127
+ label: "runtime exceeded",
128
+ summary: "task exceeded its runtime budget",
129
+ tone: "error",
130
+ suggestion: "resume",
131
+ reason: "elapsed time is past runtime.maxRuntimeMs",
132
+ });
133
+ }
134
+ function staleRunningHealth(context) {
135
+ if (context.staleMs >= STUCK_BY_DURATION[context.durationClass] &&
136
+ !context.hasBackendSignal) {
137
+ return runningHealth(context, {
138
+ state: "likely-stuck",
139
+ label: "likely stuck",
140
+ summary: "no fresh backend or activity signal",
141
+ tone: "error",
142
+ suggestion: "resume",
143
+ reason: "running task has no backend signal and activity is stale",
144
+ });
145
+ }
146
+ if (context.staleMs < STALL_BY_DURATION[context.durationClass])
147
+ return undefined;
148
+ return runningHealth(context, {
149
+ state: "stalled",
150
+ label: "possibly stalled",
151
+ summary: "no recent visible progress",
152
+ tone: "warning",
153
+ suggestion: "inspect",
154
+ reason: context.hasBackendSignal
155
+ ? "backend signal exists, but activity is stale"
156
+ : "activity is stale",
157
+ });
158
+ }
159
+ function longTailHealth(context) {
160
+ const isLongTail = context.durationClass === "long" &&
161
+ context.elapsedMs !== undefined &&
162
+ context.elapsedMs >= LONG_TAIL_ELAPSED_MS;
163
+ if (!isLongTail)
164
+ return undefined;
165
+ return runningHealth(context, {
166
+ state: "long-tail",
167
+ label: "long-tail active",
168
+ summary: "slow task with fresh liveness signals",
169
+ tone: "accent",
170
+ suggestion: "wait",
171
+ reason: context.staleMs <= ACTIVE_ACTIVITY_MS
172
+ ? "liveness signal is fresh"
173
+ : "long-running stage is still within the stale threshold",
174
+ });
175
+ }
176
+ function activeRunningHealth(context) {
177
+ return runningHealth(context, {
178
+ state: "active",
179
+ label: "active",
180
+ summary: "task is running",
181
+ tone: "accent",
182
+ suggestion: "wait",
183
+ reason: context.staleMs <= ACTIVE_ACTIVITY_MS
184
+ ? "liveness signal is fresh"
185
+ : "activity remains within the expected window",
186
+ });
187
+ }
188
+ function runningContext(task, run, nowMs) {
189
+ const durationClass = classifyWorkflowTaskDuration(task);
190
+ const startedAtMs = parseTime(task.startedAt);
191
+ const heartbeatAt = parseHeartbeatAt(task.lastMessage);
192
+ const activityAt = latestIso([heartbeatAt, run?.updatedAt, task.startedAt]);
193
+ const lastActivityAgeMs = ageMs(activityAt, nowMs);
194
+ return {
195
+ task,
196
+ nowMs,
197
+ durationClass,
198
+ elapsedMs: startedAtMs === undefined ? undefined : Math.max(0, nowMs - startedAtMs),
199
+ activityAt,
200
+ lastActivityAgeMs,
201
+ heartbeatAt,
202
+ heartbeatAgeMs: ageMs(heartbeatAt, nowMs),
203
+ hasBackendSignal: Boolean(task.backendHandle || task.pid || heartbeatAt),
204
+ staleMs: lastActivityAgeMs ?? Number.POSITIVE_INFINITY,
205
+ };
206
+ }
207
+ function terminalTaskHealth(task, nowMs) {
208
+ if (task.status === "completed" || task.status === "skipped") {
209
+ return {
210
+ state: "completed",
211
+ label: task.status === "skipped" ? "skipped" : "completed",
212
+ summary: task.status === "skipped" ? "task skipped" : "task completed",
213
+ tone: "success",
214
+ suggestion: "review",
215
+ reason: task.statusDetail,
216
+ currentTask: taskSummary(task, nowMs),
217
+ };
218
+ }
219
+ if (task.status === "pending") {
220
+ return {
221
+ state: "pending",
222
+ label: "pending",
223
+ summary: "waiting for dependencies or scheduler",
224
+ tone: "dim",
225
+ suggestion: "wait",
226
+ reason: task.statusDetail,
227
+ currentTask: taskSummary(task, nowMs),
228
+ };
229
+ }
230
+ return {
231
+ state: "needs-action",
232
+ label: "needs action",
233
+ summary: `${task.status} task needs attention`,
234
+ tone: "error",
235
+ suggestion: "inspect",
236
+ reason: task.lastMessage ?? task.statusDetail,
237
+ currentTask: taskSummary(task, nowMs),
238
+ };
239
+ }
240
+ function runningHealth(context, health) {
241
+ return {
242
+ ...health,
243
+ durationClass: context.durationClass,
244
+ currentTask: taskSummary(context.task, context.nowMs),
245
+ lastActivityAt: context.activityAt,
246
+ lastActivityAgeMs: context.lastActivityAgeMs,
247
+ heartbeatAt: context.heartbeatAt,
248
+ heartbeatAgeMs: context.heartbeatAgeMs,
249
+ };
250
+ }
251
+ function taskSummary(task, nowMs) {
252
+ const startedAtMs = parseTime(task.startedAt);
253
+ return {
254
+ taskId: task.taskId,
255
+ displayName: task.displayName,
256
+ stageId: task.stageId,
257
+ status: task.status,
258
+ ...(startedAtMs === undefined
259
+ ? {}
260
+ : { elapsedMs: Math.max(0, nowMs - startedAtMs) }),
261
+ };
262
+ }
263
+ function isProblemStatus(status) {
264
+ return (status === "failed" || status === "blocked" || status === "interrupted");
265
+ }
266
+ function parseHeartbeatAt(message) {
267
+ if (!message)
268
+ return undefined;
269
+ const match = /heartbeat\s+(\d{4}-\d{2}-\d{2}T\S+?Z)/i.exec(message);
270
+ if (!match)
271
+ return undefined;
272
+ const value = match[1];
273
+ return parseTime(value) === undefined ? undefined : value;
274
+ }
275
+ function latestIso(values) {
276
+ let latest;
277
+ let latestMs = Number.NEGATIVE_INFINITY;
278
+ for (const value of values) {
279
+ const time = parseTime(value);
280
+ if (time === undefined || time <= latestMs)
281
+ continue;
282
+ latest = value;
283
+ latestMs = time;
284
+ }
285
+ return latest;
286
+ }
287
+ function ageMs(value, nowMs) {
288
+ const time = parseTime(value);
289
+ return time === undefined ? undefined : Math.max(0, nowMs - time);
290
+ }
291
+ function parseTime(value) {
292
+ if (!value)
293
+ return undefined;
294
+ const parsed = Date.parse(value);
295
+ return Number.isFinite(parsed) ? parsed : undefined;
296
+ }
@@ -11,9 +11,15 @@ export interface WorkflowRuntimeDefaults {
11
11
  model?: string;
12
12
  thinking?: ThinkingLevel;
13
13
  }
14
+ export interface WorkflowRuntimeThinkingResolution {
15
+ requested?: ThinkingLevel;
16
+ resolved?: ThinkingLevel;
17
+ reason?: string;
18
+ }
14
19
  export interface WorkflowRuntimeResolutionInput {
15
20
  model?: string;
16
21
  thinking?: ThinkingLevel;
22
+ thinkingResolution?: WorkflowRuntimeThinkingResolution;
17
23
  }
18
24
  export interface WorkflowRuntimeResolutionContext {
19
25
  taskKey: string;
@@ -15,10 +15,13 @@ export async function resolveWorkflowRuntime(runtime, context, options) {
15
15
  : { baseModel: undefined, thinking: undefined };
16
16
  const model = await resolveModel(baseModel, context, options);
17
17
  const effectiveThinking = runtime.thinking ?? thinking ?? options.defaults?.thinking;
18
- const resolvedThinking = await resolveThinking(model, effectiveThinking, context, options);
18
+ const thinkingResolution = await resolveThinking(model, effectiveThinking, context, options);
19
19
  return {
20
20
  ...(model ? { model } : {}),
21
- ...(resolvedThinking ? { thinking: resolvedThinking } : {}),
21
+ ...(thinkingResolution?.resolved
22
+ ? { thinking: thinkingResolution.resolved }
23
+ : {}),
24
+ ...(thinkingResolution ? { thinkingResolution } : {}),
22
25
  };
23
26
  }
24
27
  export function splitKnownThinkingSuffix(model) {
@@ -101,21 +104,41 @@ async function resolveThinking(modelId, requested, context, options) {
101
104
  return undefined;
102
105
  const model = findModelInfo(modelId, options.availableModels ?? []);
103
106
  const supported = getSupportedThinkingLevels(model);
104
- if (supported.includes(requested))
105
- return requested;
106
- if (!options.prompt) {
107
- const modelLabel = modelId ?? "selected model";
108
- throw new Error(`${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}. Supported: ${supported.join(", ") || "none"}`);
107
+ if (supported.includes(requested)) {
108
+ return { requested, resolved: requested };
109
109
  }
110
110
  if (supported.length === 0) {
111
111
  throw new Error(`${modelId ?? "selected model"} does not expose any supported reasoning levels for ${context.taskKey}`);
112
112
  }
113
- const selected = await options.prompt.select(`${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported level.`, supported);
113
+ const downgradeOptions = lowerOrEqualSupportedThinking(requested, supported);
114
+ if (downgradeOptions.length === 0) {
115
+ const modelLabel = modelId ?? "selected model";
116
+ throw new Error(`${modelLabel} does not support reasoning level "${requested}" for ${context.taskKey}, and no lower-or-equal fallback is available. Supported: ${supported.join(", ") || "none"}`);
117
+ }
118
+ if (!options.prompt) {
119
+ const resolved = downgradeOptions[downgradeOptions.length - 1];
120
+ return {
121
+ requested,
122
+ resolved,
123
+ reason: `requested ${requested} is unsupported by ${modelId ?? "selected model"}; using ${resolved}`,
124
+ };
125
+ }
126
+ const selected = await options.prompt.select(`${modelId ?? "Selected model"} does not support reasoning "${requested}" for ${context.taskKey}. Choose a supported lower-or-equal level.`, downgradeOptions);
114
127
  if (!selected)
115
128
  throw new Error(`Reasoning selection cancelled for ${context.taskKey}`);
116
- if (!isThinkingLevel(selected))
129
+ if (!isThinkingLevel(selected) || !downgradeOptions.includes(selected))
117
130
  throw new Error(`Invalid reasoning selection "${selected}" for ${context.taskKey}`);
118
- return selected;
131
+ return {
132
+ requested,
133
+ resolved: selected,
134
+ reason: `selected supported reasoning ${selected} for unsupported request ${requested}`,
135
+ };
136
+ }
137
+ function lowerOrEqualSupportedThinking(requested, supported) {
138
+ const requestedIndex = THINKING_LEVELS.indexOf(requested);
139
+ if (requestedIndex < 0)
140
+ return [];
141
+ return THINKING_LEVELS.slice(0, requestedIndex + 1).filter((level) => supported.includes(level));
119
142
  }
120
143
  function findModelInfo(modelId, available) {
121
144
  if (!modelId)
@@ -62,6 +62,7 @@ export declare class WorkflowView implements Component {
62
62
  private taskIdentityLines;
63
63
  private taskOverviewLines;
64
64
  private taskTimelineLines;
65
+ private taskHealthLines;
65
66
  private taskValidationStripLines;
66
67
  private taskArtifactViewerLines;
67
68
  private currentArtifactSourceLines;
@@ -83,6 +84,7 @@ export declare class WorkflowView implements Component {
83
84
  private syncSelectedTaskId;
84
85
  private breadcrumbText;
85
86
  private runSummaryLines;
87
+ private runHealthLines;
86
88
  private runDetailSummaryLines;
87
89
  private stageContextLines;
88
90
  private footerText;
@@ -1,6 +1,7 @@
1
1
  // @ts-nocheck
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { workflowRunPath, fromProjectPath, listRunRecords, readIndex, readRunRecord, } from "./store.js";
4
+ import { diagnoseWorkflowRunHealth, diagnoseWorkflowTaskHealth, } from "./workflow-progress-health.js";
4
5
  import { WORKFLOW_RUN_TYPE, } from "./types.js";
5
6
  const REFRESH_INTERVAL_MS = 1_000;
6
7
  const MAX_LIST_ROWS = 18;
@@ -277,14 +278,18 @@ export class WorkflowView {
277
278
  }
278
279
  renderRunsScreen(width) {
279
280
  const selected = this.flows[this.selectedFlow];
281
+ const selectedDetail = selected && this.detailRun?.runId === selected.runId
282
+ ? this.detailRun
283
+ : undefined;
280
284
  const sideLines = [
281
285
  accent(this.theme, "All runs"),
282
286
  kvRow(this.theme, "total", String(this.flows.length)),
283
287
  kvRow(this.theme, "running", String(this.flows.filter((flow) => flow.status === "running").length)),
288
+ kvRow(this.theme, "needs action", String(this.flows.filter((flow) => ["failed", "blocked", "interrupted"].includes(flow.status)).length)),
284
289
  "",
285
290
  accent(this.theme, "Selected"),
286
291
  ...(selected
287
- ? this.runSummaryLines(selected)
292
+ ? this.runSummaryLines(selected, selectedDetail)
288
293
  : [placeholder(this.theme, "none")]),
289
294
  ];
290
295
  return this.renderTwoPane(width, "Filters / Summary", sideLines, "Runs", this.runLines(Math.max(1, this.mainPaneBodyWidth(width))), 32);
@@ -331,9 +336,10 @@ export class WorkflowView {
331
336
  return width - leftWidth - 5;
332
337
  }
333
338
  renderTaskDetail(width, run, task) {
339
+ const taskHealth = diagnoseWorkflowTaskHealth(task, run);
334
340
  const lines = [
335
341
  ...boxed(this.theme, "Task Detail", width, [
336
- `${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${muted(this.theme, this.breadcrumbText())}`,
342
+ `${statusGlyph(this.theme, task.status)} ${strong(this.theme, task.displayName)} ${statusBadge(this.theme, task.status)} ${healthInline(this.theme, taskHealth)} ${muted(this.theme, this.breadcrumbText())}`,
337
343
  taskMetaLine(this.theme, [
338
344
  ["agent", task.agent],
339
345
  ["stage", task.stageId ?? "(none)"],
@@ -343,6 +349,10 @@ export class WorkflowView {
343
349
  ], statusColor(task.status)),
344
350
  "",
345
351
  ];
352
+ const healthLines = this.taskHealthLines(taskHealth, width - 4);
353
+ if (healthLines.length > 0) {
354
+ lines.push(...boxed(this.theme, "Health", width, healthLines, healthColor(taskHealth)), "");
355
+ }
346
356
  const validationLines = this.taskValidationStripLines(task, width - 4);
347
357
  if (validationLines.length > 0) {
348
358
  lines.push(...boxed(this.theme, "Validation", width, validationLines, task.outputValidation?.status === "invalid" ||
@@ -388,13 +398,15 @@ export class WorkflowView {
388
398
  const name = flow.name ?? flow.type;
389
399
  const left = `${prefix}${marker} ${selected ? strong(this.theme, name) : name}`;
390
400
  const runIdText = shortId(flow.runId).slice(0, 16).padEnd(16, " ");
391
- const runningText = flow.taskSummary.running > 0
392
- ? ` ${muted(this.theme, "·")} ${metaLabel(this.theme, "running")} ${metaValue(this.theme, String(flow.taskSummary.running))}`
393
- : "";
401
+ const detailRun = this.detailRun?.runId === flow.runId ? this.detailRun : undefined;
402
+ const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
403
+ const healthText = health.state === "completed"
404
+ ? ""
405
+ : ` ${muted(this.theme, "·")} ${healthLabel(this.theme, health)}`;
394
406
  const baseRight = `${statusColumn(this.theme, flow.status, runStatusLabel(flow), statusWidth)} ${progressBar(this.theme, flow.taskSummary, 5)} ${metaValue(this.theme, runIdText)}`;
395
407
  const right = width >= 90
396
- ? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${runningText}`
397
- : `${baseRight}${runningText}`;
408
+ ? `${baseRight} ${muted(this.theme, "·")} ${metaLabel(this.theme, "start")} ${metaValue(this.theme, timestampText(flow.createdAt))}${healthText}`
409
+ : `${baseRight}${healthText}`;
398
410
  const line = joinColumns(left, right, width, 17);
399
411
  lines.push(selectedLine(this.theme, line, width, selected, true));
400
412
  }
@@ -437,7 +449,7 @@ export class WorkflowView {
437
449
  const selected = index === this.selectedTask;
438
450
  const prefix = selected ? accent(this.theme, "› ") : " ";
439
451
  const left = `${prefix}${statusGlyph(this.theme, task.status)} ${selected ? strong(this.theme, task.displayName) : task.displayName}`;
440
- const right = taskListStatusLabel(this.theme, task);
452
+ const right = taskListStatusLabel(this.theme, task, diagnoseWorkflowTaskHealth(task, run));
441
453
  const line = joinColumns(left, metaByStatus(this.theme, task.status, right), width, Math.max(22, Math.floor(width * 0.45)));
442
454
  lines.push(selectedLine(this.theme, line, width, selected, true));
443
455
  }
@@ -487,6 +499,24 @@ export class WorkflowView {
487
499
  : "warning"));
488
500
  return lines.map((line) => fit(line, width));
489
501
  }
502
+ taskHealthLines(health, width) {
503
+ if (health.state === "completed" || health.state === "pending")
504
+ return [];
505
+ const lines = [
506
+ `${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
507
+ kvRow(this.theme, "suggested", health.suggestion, healthColor(health)),
508
+ kvRow(this.theme, "why", health.reason),
509
+ ];
510
+ if (health.currentTask?.elapsedMs !== undefined)
511
+ lines.splice(1, 0, kvRow(this.theme, "elapsed", formatDuration(health.currentTask.elapsedMs)));
512
+ if (health.durationClass)
513
+ lines.push(kvRow(this.theme, "duration", `${health.durationClass} expected`));
514
+ if (health.heartbeatAgeMs !== undefined)
515
+ lines.push(kvRow(this.theme, "heartbeat", `${formatDuration(health.heartbeatAgeMs)} ago`));
516
+ if (health.lastActivityAgeMs !== undefined)
517
+ lines.push(kvRow(this.theme, "activity", `${formatDuration(health.lastActivityAgeMs)} ago`));
518
+ return lines.map((line) => fit(line, width));
519
+ }
490
520
  taskValidationStripLines(task, width) {
491
521
  const summary = taskValidationSummary(task);
492
522
  if (!summary)
@@ -502,9 +532,7 @@ export class WorkflowView {
502
532
  const total = sourceLines.length;
503
533
  const maxStart = Math.max(0, total - TASK_ARTIFACT_VIEW_LINES);
504
534
  const start = Math.min(this.artifactScrollLine, maxStart);
505
- const end = total === 0
506
- ? 0
507
- : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
535
+ const end = total === 0 ? 0 : Math.min(total, start + TASK_ARTIFACT_VIEW_LINES);
508
536
  const visible = total === 0
509
537
  ? [
510
538
  this.taskArtifactView === "output"
@@ -686,7 +714,8 @@ export class WorkflowView {
686
714
  return this.tasksForSelectedStage(this.detailRun)[this.selectedTask];
687
715
  }
688
716
  syncSelectedTaskId(tasks) {
689
- const stageTasks = tasks ?? (this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
717
+ const stageTasks = tasks ??
718
+ (this.detailRun ? this.tasksForSelectedStage(this.detailRun) : []);
690
719
  this.selectedTaskId = stageTasks[this.selectedTask]?.taskId ?? "";
691
720
  }
692
721
  breadcrumbText() {
@@ -704,7 +733,8 @@ export class WorkflowView {
704
733
  parts.push(task.displayName);
705
734
  return parts.join(" › ");
706
735
  }
707
- runSummaryLines(flow) {
736
+ runSummaryLines(flow, detailRun) {
737
+ const health = diagnoseWorkflowRunHealth(detailRun ?? flow);
708
738
  return [
709
739
  `${statusGlyph(this.theme, flow.status)} ${strong(this.theme, flow.name ?? flow.type)} ${statusBadge(this.theme, flow.status, runStatusLabel(flow))}`,
710
740
  progressBar(this.theme, flow.taskSummary, 8),
@@ -723,9 +753,28 @@ export class WorkflowView {
723
753
  elapsedText(flow.createdAt, flow.updatedAt, flow.status === "running"),
724
754
  ],
725
755
  ]),
756
+ ...this.runHealthLines(health),
757
+ ];
758
+ }
759
+ runHealthLines(health) {
760
+ if (health.state === "completed")
761
+ return [];
762
+ const lines = [
763
+ "",
764
+ accent(this.theme, "Health"),
765
+ `${healthGlyph(this.theme, health)} ${healthLabel(this.theme, health)} ${muted(this.theme, health.summary)}`,
726
766
  ];
767
+ if (health.currentTask?.displayName)
768
+ lines.push(kvRow(this.theme, "current", health.currentTask.displayName));
769
+ if (health.lastActivityAgeMs !== undefined)
770
+ lines.push(kvRow(this.theme, "activity", `${formatDuration(health.lastActivityAgeMs)} ago`));
771
+ if (health.heartbeatAgeMs !== undefined)
772
+ lines.push(kvRow(this.theme, "heartbeat", `${formatDuration(health.heartbeatAgeMs)} ago`));
773
+ lines.push(kvRow(this.theme, "suggested", health.suggestion, healthColor(health)));
774
+ return lines;
727
775
  }
728
776
  runDetailSummaryLines(run) {
777
+ const health = diagnoseWorkflowRunHealth(run);
729
778
  const lines = [
730
779
  `${statusGlyph(this.theme, run.status)} ${strong(this.theme, run.name ?? run.type)} ${statusBadge(this.theme, run.status)}`,
731
780
  progressBar(this.theme, run.taskSummary, 10),
@@ -745,6 +794,7 @@ export class WorkflowView {
745
794
  ["updated", timestampText(run.updatedAt)],
746
795
  ]),
747
796
  kvRow(this.theme, "run", shortId(run.runId)),
797
+ ...this.runHealthLines(health),
748
798
  ];
749
799
  if (run.fanout && run.fanout.length > 0) {
750
800
  lines.push("", accent(this.theme, "Fanout"));
@@ -1034,7 +1084,29 @@ function statusColor(status) {
1034
1084
  function statusText(status) {
1035
1085
  return status;
1036
1086
  }
1037
- function taskListStatusLabel(theme, task) {
1087
+ function healthColor(health) {
1088
+ return health.tone;
1089
+ }
1090
+ function healthGlyph(theme, health) {
1091
+ if (health.tone === "success")
1092
+ return success(theme, "✓");
1093
+ if (health.tone === "warning")
1094
+ return warning(theme, "●");
1095
+ if (health.tone === "error")
1096
+ return errorText(theme, "●");
1097
+ if (health.tone === "dim")
1098
+ return muted(theme, "•");
1099
+ return accent(theme, "●");
1100
+ }
1101
+ function healthLabel(theme, health) {
1102
+ return fg(theme, healthColor(health), strong(theme, health.label));
1103
+ }
1104
+ function healthInline(theme, health) {
1105
+ if (health.state === "completed" || health.state === "pending")
1106
+ return "";
1107
+ return `${healthGlyph(theme, health)} ${healthLabel(theme, health)}`;
1108
+ }
1109
+ function taskListStatusLabel(theme, task, health) {
1038
1110
  const validation = taskValidationSummary(task);
1039
1111
  const label = validation?.status === "invalid"
1040
1112
  ? "invalid output"
@@ -1042,8 +1114,13 @@ function taskListStatusLabel(theme, task) {
1042
1114
  ? "valid"
1043
1115
  : task.status === "completed"
1044
1116
  ? "done"
1045
- : statusText(task.status);
1046
- return fg(theme, statusColor(task.status), strong(theme, label));
1117
+ : task.status === "running"
1118
+ ? health.label
1119
+ : statusText(task.status);
1120
+ const suffix = task.status === "running" && health.currentTask?.elapsedMs !== undefined
1121
+ ? ` ${muted(theme, "·")} ${metaValue(theme, formatDuration(health.currentTask.elapsedMs))}`
1122
+ : "";
1123
+ return `${fg(theme, task.status === "running" ? healthColor(health) : statusColor(task.status), strong(theme, label))}${suffix}`;
1047
1124
  }
1048
1125
  function compactStatusLabel(theme, status) {
1049
1126
  const label = status === "completed" ? "done" : statusText(status);
@@ -1107,7 +1184,7 @@ function taskValidationSummary(task) {
1107
1184
  : undefined;
1108
1185
  const issueMessage = typeof issue === "string"
1109
1186
  ? issue
1110
- : issue?.message ?? issue?.path ?? issue?.code ?? "";
1187
+ : (issue?.message ?? issue?.path ?? issue?.code ?? "");
1111
1188
  const message = validation.message ?? validation.reason ?? issueMessage;
1112
1189
  if (status === "valid" && !message)
1113
1190
  return undefined;
@@ -1263,7 +1340,9 @@ function truncateToWidth(text, width) {
1263
1340
  return "";
1264
1341
  if (visibleWidth(text) <= safeWidth)
1265
1342
  return text;
1266
- const hasAnsi = text.includes("\u001b[") || text.includes("\u001b]") || text.includes("\u001b_");
1343
+ const hasAnsi = text.includes("\u001b[") ||
1344
+ text.includes("\u001b]") ||
1345
+ text.includes("\u001b_");
1267
1346
  const ellipsis = "…";
1268
1347
  const ellipsisWidth = visibleWidth(ellipsis);
1269
1348
  const limit = Math.max(0, safeWidth - ellipsisWidth);
@@ -0,0 +1,43 @@
1
+ import { type WorkflowWebSecurityPolicy, type WorkflowWebSourceCacheConfig, type WorkflowWebSourcePolicy } from "./workflow-web-source.js";
2
+ export declare const WORKFLOW_WEB_SOURCE_LAUNCH_CONFIG_SCHEMA: "workflow-web-source-launch-config-v1";
3
+ export interface WorkflowWebProviderLaunchConfig {
4
+ kind: "pi-web-access" | "extension" | "none";
5
+ extensionPath?: string;
6
+ }
7
+ export interface WorkflowWebSourceLaunchConfig extends WorkflowWebSourceCacheConfig {
8
+ schema: typeof WORKFLOW_WEB_SOURCE_LAUNCH_CONFIG_SCHEMA;
9
+ workflowName?: string;
10
+ stageId?: string;
11
+ taskKey?: string;
12
+ cwd: string;
13
+ provider: WorkflowWebProviderLaunchConfig;
14
+ webSourcePolicy?: Partial<WorkflowWebSourcePolicy>;
15
+ securityPolicy?: Partial<WorkflowWebSecurityPolicy>;
16
+ exposeLegacyTools?: boolean;
17
+ }
18
+ export interface WorkflowWebSourceExtensionWrapperOptions {
19
+ wrapperPath: string;
20
+ importPath: string;
21
+ providerExtensionPath?: string;
22
+ config: WorkflowWebSourceLaunchConfig;
23
+ }
24
+ type ToolResult = {
25
+ content?: Array<Record<string, unknown>>;
26
+ details?: Record<string, unknown>;
27
+ [key: string]: unknown;
28
+ };
29
+ type ToolSpec = {
30
+ name?: string;
31
+ execute?: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: unknown) => Promise<ToolResult>;
32
+ [key: string]: unknown;
33
+ };
34
+ type PiLike = Record<string | symbol, unknown> & {
35
+ registerTool(tool: ToolSpec): void;
36
+ appendEntry?(type: string, data: unknown): void;
37
+ };
38
+ type ProviderExtension = (pi: PiLike) => void;
39
+ export declare function registerWorkflowWebSourceExtension(pi: PiLike, config: WorkflowWebSourceLaunchConfig, providerExtension?: ProviderExtension): void;
40
+ export declare function buildWorkflowWebSourceExtensionWrapper(options: Omit<WorkflowWebSourceExtensionWrapperOptions, "wrapperPath">): string;
41
+ export declare function writeWorkflowWebSourceExtensionWrapper(options: WorkflowWebSourceExtensionWrapperOptions): Promise<string>;
42
+ export declare function workflowWebSourceModuleImportPath(modulePath: string): string;
43
+ export {};