@osolmaz/pi-workflows 0.9.1 → 0.11.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 (147) hide show
  1. package/README.md +64 -19
  2. package/dist/builtins/autodoc.workflow.d.ts +58 -0
  3. package/dist/builtins/autodoc.workflow.js +266 -0
  4. package/dist/builtins/autodoc.workflow.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +353 -0
  6. package/dist/builtins/autoimplement.workflow.js +961 -0
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -0
  8. package/dist/builtins/autoplan.workflow.d.ts +58 -0
  9. package/dist/builtins/autoplan.workflow.js +190 -0
  10. package/dist/builtins/autoplan.workflow.js.map +1 -0
  11. package/dist/builtins/catalog.js +9 -1
  12. package/dist/builtins/catalog.js.map +1 -1
  13. package/dist/builtins/index.d.ts +6 -0
  14. package/dist/builtins/index.js +7 -0
  15. package/dist/builtins/index.js.map +1 -0
  16. package/dist/builtins/monitor.workflow.d.ts +29 -3
  17. package/dist/builtins/monitor.workflow.js +325 -13
  18. package/dist/builtins/monitor.workflow.js.map +1 -1
  19. package/dist/builtins/plan-approval.workflow.d.ts +83 -0
  20. package/dist/builtins/plan-approval.workflow.js +148 -0
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -0
  22. package/dist/builtins/plan-presentation.d.ts +7 -0
  23. package/dist/builtins/plan-presentation.js +44 -0
  24. package/dist/builtins/plan-presentation.js.map +1 -0
  25. package/dist/extension/decision-channels.d.ts +132 -0
  26. package/dist/extension/decision-channels.js +1082 -0
  27. package/dist/extension/decision-channels.js.map +1 -0
  28. package/dist/extension/index.d.ts +1 -0
  29. package/dist/extension/index.js +426 -11
  30. package/dist/extension/index.js.map +1 -1
  31. package/dist/extension/widget.js +26 -2
  32. package/dist/extension/widget.js.map +1 -1
  33. package/dist/extension/workflow-tool.d.ts +1 -37
  34. package/dist/extension/workflow-tool.js +1 -42
  35. package/dist/extension/workflow-tool.js.map +1 -1
  36. package/dist/host/rpc-bridge.js +4 -21
  37. package/dist/host/rpc-bridge.js.map +1 -1
  38. package/dist/render/graph-render.js +46 -10
  39. package/dist/render/graph-render.js.map +1 -1
  40. package/dist/viewer/render.js +36 -2
  41. package/dist/viewer/render.js.map +1 -1
  42. package/dist/workflows/catalog.d.ts +1 -0
  43. package/dist/workflows/catalog.js +6 -0
  44. package/dist/workflows/catalog.js.map +1 -1
  45. package/dist/workflows/composition.d.ts +45 -0
  46. package/dist/workflows/composition.js +485 -0
  47. package/dist/workflows/composition.js.map +1 -0
  48. package/dist/workflows/decision-presentation.d.ts +18 -0
  49. package/dist/workflows/decision-presentation.js +417 -0
  50. package/dist/workflows/decision-presentation.js.map +1 -0
  51. package/dist/workflows/decision.d.ts +11 -5
  52. package/dist/workflows/decision.js.map +1 -1
  53. package/dist/workflows/definition.d.ts +22 -3
  54. package/dist/workflows/definition.js +46 -3
  55. package/dist/workflows/definition.js.map +1 -1
  56. package/dist/workflows/engine.d.ts +2 -1
  57. package/dist/workflows/engine.js +199 -18
  58. package/dist/workflows/engine.js.map +1 -1
  59. package/dist/workflows/graph.js +8 -6
  60. package/dist/workflows/graph.js.map +1 -1
  61. package/dist/workflows/human-decision.d.ts +87 -0
  62. package/dist/workflows/human-decision.js +583 -0
  63. package/dist/workflows/human-decision.js.map +1 -0
  64. package/dist/workflows/index.d.ts +5 -2
  65. package/dist/workflows/index.js +4 -1
  66. package/dist/workflows/index.js.map +1 -1
  67. package/dist/workflows/loader.d.ts +5 -4
  68. package/dist/workflows/loader.js +118 -18
  69. package/dist/workflows/loader.js.map +1 -1
  70. package/dist/workflows/schema.d.ts +3 -1
  71. package/dist/workflows/schema.js +64 -2
  72. package/dist/workflows/schema.js.map +1 -1
  73. package/dist/workflows/store.js +39 -2
  74. package/dist/workflows/store.js.map +1 -1
  75. package/dist/workflows/tool-input.d.ts +72 -0
  76. package/dist/workflows/tool-input.js +141 -0
  77. package/dist/workflows/tool-input.js.map +1 -0
  78. package/dist/workflows/types.d.ts +300 -2
  79. package/docs/CONTROLLERS.md +1 -1
  80. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  81. package/docs/HUMAN_DECISIONS.md +371 -0
  82. package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
  83. package/docs/MONITOR.md +46 -18
  84. package/docs/WORKFLOW_COMPOSITION.md +333 -0
  85. package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
  86. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
  87. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
  88. package/docs/plans/2026-08-19-workflow-composition-plan.md +308 -0
  89. package/docs/run-bundles.md +45 -10
  90. package/docs/workflows.md +106 -15
  91. package/examples/workflows/approved-plan.workflow.ts +58 -0
  92. package/examples/workflows/autoimplement.workflow.ts +1 -92
  93. package/examples/workflows/autoplan.workflow.ts +1 -0
  94. package/examples/workflows/human-decision.workflow.ts +62 -0
  95. package/herdr-plugin.toml +1 -1
  96. package/package.json +6 -1
  97. package/schemas/decision-presentation-v1.schema.json +83 -0
  98. package/schemas/human-decision-accepted-v1.schema.json +44 -0
  99. package/schemas/human-decision-accepted-v2.schema.json +50 -0
  100. package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
  101. package/schemas/human-decision-cancellation-v1.schema.json +15 -0
  102. package/schemas/human-decision-continuation-v1.schema.json +16 -0
  103. package/schemas/human-decision-delivery-v1.schema.json +28 -0
  104. package/schemas/human-decision-delivery-v2.schema.json +36 -0
  105. package/schemas/human-decision-receipt-v1.schema.json +33 -0
  106. package/schemas/human-decision-receipt-v2.schema.json +39 -0
  107. package/schemas/human-decision-request-v1.schema.json +59 -0
  108. package/schemas/human-decision-request-v2.schema.json +69 -0
  109. package/schemas/human-decision-resolution-v1.schema.json +27 -0
  110. package/schemas/human-decision-resolution-v2.schema.json +27 -0
  111. package/schemas/human-decision-settlement-v1.schema.json +28 -0
  112. package/skills/autodoc/SKILL.md +43 -0
  113. package/skills/autoimplement/SKILL.md +58 -0
  114. package/skills/autoimplement/agents/openai.yaml +4 -0
  115. package/skills/autoplan/SKILL.md +25 -0
  116. package/skills/monitor/SKILL.md +8 -1
  117. package/skills/pi-workflows/SKILL.md +5 -3
  118. package/src/builtins/autodoc.workflow.ts +325 -0
  119. package/src/builtins/autoimplement.workflow.ts +1123 -0
  120. package/src/builtins/autoplan.workflow.ts +231 -0
  121. package/src/builtins/catalog.ts +9 -1
  122. package/src/builtins/index.ts +32 -0
  123. package/src/builtins/monitor.workflow.ts +396 -15
  124. package/src/builtins/plan-approval.workflow.ts +190 -0
  125. package/src/builtins/plan-presentation.ts +57 -0
  126. package/src/extension/decision-channels.ts +1533 -0
  127. package/src/extension/index.ts +499 -9
  128. package/src/extension/widget.ts +39 -2
  129. package/src/extension/workflow-tool.ts +5 -81
  130. package/src/host/rpc-bridge.ts +7 -30
  131. package/src/render/graph-render.ts +54 -5
  132. package/src/viewer/render.ts +56 -3
  133. package/src/workflows/catalog.ts +7 -0
  134. package/src/workflows/composition.ts +642 -0
  135. package/src/workflows/decision-presentation.ts +501 -0
  136. package/src/workflows/decision.ts +12 -5
  137. package/src/workflows/definition.ts +118 -8
  138. package/src/workflows/engine.ts +257 -20
  139. package/src/workflows/graph.ts +8 -6
  140. package/src/workflows/human-decision.ts +819 -0
  141. package/src/workflows/index.ts +89 -0
  142. package/src/workflows/loader.ts +186 -18
  143. package/src/workflows/schema.ts +79 -2
  144. package/src/workflows/store.ts +45 -2
  145. package/src/workflows/tool-input.ts +202 -0
  146. package/src/workflows/types.ts +374 -2
  147. package/examples/workflows/elegant-solution.workflow.ts +0 -95
@@ -297,8 +297,36 @@ function nodeRuntimeSegments(
297
297
  }
298
298
 
299
299
  if (state.waitingOn === nodeId) {
300
- const summary = snapshot.nodes[nodeId]?.summary;
301
- segments.push(summary ? sanitizeText(summary) : "waiting");
300
+ const human = snapshot.nodes[nodeId]?.humanDecision;
301
+ if (human !== undefined) {
302
+ const request =
303
+ state.finalOutput !== null && typeof state.finalOutput === "object"
304
+ ? (state.finalOutput as {
305
+ schema?: unknown;
306
+ audience?: unknown;
307
+ presentationDigest?: unknown;
308
+ presentation?: { summary?: unknown };
309
+ })
310
+ : undefined;
311
+ const requestAudience =
312
+ (request?.schema === "pi-workflows.human-decision-request.v1" ||
313
+ request?.schema === "pi-workflows.human-decision-request.v2") &&
314
+ typeof request.audience === "string"
315
+ ? request.audience
316
+ : human.audience;
317
+ segments.push(
318
+ `human decision · ${sanitizeText(requestAudience)}${typeof request?.presentation?.summary === "string" ? ` · ${sanitizeText(request.presentation.summary)}` : ""} · ${Object.values(
319
+ human.choices,
320
+ )
321
+ .map((choice) => sanitizeText(choice.label))
322
+ .join(
323
+ " / ",
324
+ )}${typeof request?.presentationDigest === "string" ? ` · ${request.presentationDigest.slice(7, 19)}` : ""}`,
325
+ );
326
+ } else {
327
+ const summary = snapshot.nodes[nodeId]?.summary;
328
+ segments.push(summary ? sanitizeText(summary) : "waiting");
329
+ }
302
330
  return segments;
303
331
  }
304
332
 
@@ -313,6 +341,15 @@ function nodeRuntimeSegments(
313
341
  );
314
342
  return segments;
315
343
  }
344
+ const human = snapshot.nodes[nodeId]?.humanDecision;
345
+ if (
346
+ human !== undefined &&
347
+ state.humanDecision !== undefined &&
348
+ state.humanDecision.nodeId === nodeId
349
+ ) {
350
+ const selected = human.choices[state.humanDecision.response.choice];
351
+ if (selected !== undefined) segments.push(`human: ${sanitizeText(selected.label)}`);
352
+ }
316
353
  if (Number.isFinite(result.durationMs)) {
317
354
  segments.push(formatDuration(result.durationMs));
318
355
  }
@@ -1,81 +1,5 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
- import { Type, type TSchema } from "typebox";
3
-
4
- const noExtraProperties = { additionalProperties: false } as const;
5
-
6
- export const WorkflowToolParameters: TSchema = Type.Union([
7
- Type.Object(
8
- {
9
- action: StringEnum(["list"] as const),
10
- offset: Type.Optional(Type.Integer({ minimum: 0, description: "Workflow list offset" })),
11
- },
12
- noExtraProperties,
13
- ),
14
- Type.Object(
15
- {
16
- action: StringEnum(["start"] as const),
17
- workflow: Type.String({ description: "Discovered workflow name or workflow file path" }),
18
- input: Type.Optional(Type.Unknown({ description: "Structured workflow input" })),
19
- },
20
- noExtraProperties,
21
- ),
22
- Type.Object(
23
- {
24
- action: StringEnum(["status"] as const),
25
- runId: Type.Optional(Type.String({ description: "Run id; omit for the active run" })),
26
- },
27
- noExtraProperties,
28
- ),
29
- Type.Object({ action: StringEnum(["pause"] as const) }, noExtraProperties),
30
- Type.Object({ action: StringEnum(["resume"] as const) }, noExtraProperties),
31
- Type.Object({ action: StringEnum(["cancel"] as const) }, noExtraProperties),
32
- Type.Object(
33
- {
34
- action: StringEnum(["answer"] as const),
35
- input: Type.Unknown({ description: "Checkpoint answer" }),
36
- runId: Type.Optional(Type.String({ description: "Waiting run id" })),
37
- },
38
- noExtraProperties,
39
- ),
40
- Type.Object(
41
- {
42
- action: StringEnum(["update"] as const),
43
- step: Type.String({ description: "Active step id" }),
44
- attempt: Type.String({ description: "Active attempt id" }),
45
- update: Type.Object(
46
- {
47
- type: Type.String(),
48
- key: Type.String(),
49
- data: Type.Record(Type.String(), Type.Unknown()),
50
- },
51
- noExtraProperties,
52
- ),
53
- },
54
- noExtraProperties,
55
- ),
56
- Type.Object(
57
- {
58
- action: StringEnum(["submit"] as const),
59
- step: Type.String({ description: "Step id from the workflow step contract" }),
60
- attempt: Type.String({ description: "Attempt id from the workflow step contract" }),
61
- output: Type.Unknown({ description: "Step output matching the expected shape" }),
62
- },
63
- noExtraProperties,
64
- ),
65
- ]);
66
-
67
- export type WorkflowToolInput =
68
- | { action: "list"; offset?: number }
69
- | { action: "start"; workflow: string; input?: unknown }
70
- | { action: "status"; runId?: string }
71
- | { action: "pause" }
72
- | { action: "resume" }
73
- | { action: "cancel" }
74
- | { action: "answer"; input: unknown; runId?: string }
75
- | {
76
- action: "update";
77
- step: string;
78
- attempt: string;
79
- update: { type: string; key: string; data: Record<string, unknown> };
80
- }
81
- | { action: "submit"; step: string; attempt: string; output: unknown };
1
+ export {
2
+ parseWorkflowToolInput,
3
+ WorkflowToolParameters,
4
+ type WorkflowToolInput,
5
+ } from "../workflows/tool-input.js";
@@ -1,6 +1,8 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { Type } from "typebox";
2
+ import {
3
+ parseWorkflowSubmissionInput,
4
+ WorkflowSubmissionToolParameters,
5
+ } from "../workflows/tool-input.js";
4
6
 
5
7
  export const RPC_SUBMISSION_PREFIX = "PI_WORKFLOWS_STEP_SUBMISSION ";
6
8
 
@@ -19,34 +21,9 @@ export default function piWorkflowsRpcBridge(pi: ExtensionAPI) {
19
21
  "Only call this tool when a workflow step contract in the conversation asks you to.",
20
22
  "Pass the exact step and attempt ids from the contract.",
21
23
  ].join(" "),
22
- parameters: Type.Union([
23
- Type.Object(
24
- {
25
- action: StringEnum(["update"] as const),
26
- step: Type.String(),
27
- attempt: Type.String(),
28
- update: Type.Object(
29
- {
30
- type: Type.String(),
31
- key: Type.String(),
32
- data: Type.Record(Type.String(), Type.Unknown()),
33
- },
34
- { additionalProperties: false },
35
- ),
36
- },
37
- { additionalProperties: false },
38
- ),
39
- Type.Object(
40
- {
41
- action: StringEnum(["submit"] as const),
42
- step: Type.String({ description: "Step id from the workflow step contract" }),
43
- attempt: Type.String({ description: "Attempt id from the workflow step contract" }),
44
- output: Type.Unknown({ description: "Step output matching the expected shape" }),
45
- },
46
- { additionalProperties: false },
47
- ),
48
- ]),
49
- async execute(toolCallId, params) {
24
+ parameters: WorkflowSubmissionToolParameters,
25
+ async execute(toolCallId, rawParams) {
26
+ const params = parseWorkflowSubmissionInput(rawParams);
50
27
  process.stderr.write(
51
28
  `${RPC_SUBMISSION_PREFIX}${JSON.stringify({ ...params, idempotencyKey: toolCallId })}\n`,
52
29
  );
@@ -216,7 +216,7 @@ function cardMetrics(view: GraphView): CardMetrics {
216
216
  measure(`${nodeTypeBadge("checkpoint")} ${STATUS_GLYPHS[status]} ${STATUS_LABELS[status]}`);
217
217
  }
218
218
  for (const [nodeId, node] of Object.entries(snapshot.nodes)) {
219
- measure(sanitizeText(nodeId));
219
+ measure(sanitizeText(hierarchicalNodeLabel(nodeId, node)));
220
220
  measure(nodeTypeBadge(node.nodeType, node.actionExecution));
221
221
  const labels = nodeBranchLabels(view, nodeId);
222
222
  branchRows = Math.max(branchRows, labels.length);
@@ -305,25 +305,63 @@ function renderCellText(
305
305
  const durationMs = Date.parse(attempt.finishedAt) - Date.parse(attempt.startedAt);
306
306
  elapsed = formatDuration(durationMs);
307
307
  }
308
+ const human = node?.humanDecision;
309
+ const waitingSchema =
310
+ state.waitingOn === nodeId &&
311
+ state.finalOutput !== null &&
312
+ typeof state.finalOutput === "object"
313
+ ? (state.finalOutput as { schema?: unknown }).schema
314
+ : undefined;
315
+ const waitingRequest =
316
+ waitingSchema === "pi-workflows.human-decision-request.v1" ||
317
+ waitingSchema === "pi-workflows.human-decision-request.v2"
318
+ ? (state.finalOutput as {
319
+ audience?: unknown;
320
+ presentationDigest?: unknown;
321
+ presentation?: { summary?: unknown };
322
+ })
323
+ : undefined;
324
+ const selected =
325
+ human !== undefined &&
326
+ state.humanDecision !== undefined &&
327
+ state.humanDecision.nodeId === nodeId
328
+ ? human.choices[state.humanDecision.response.choice]
329
+ : undefined;
330
+ const humanDetail =
331
+ human === undefined
332
+ ? undefined
333
+ : state.waitingOn === nodeId
334
+ ? `human decision · ${sanitizeText(typeof waitingRequest?.audience === "string" ? waitingRequest.audience : human.audience)}${typeof waitingRequest?.presentation?.summary === "string" ? ` · ${sanitizeText(waitingRequest.presentation.summary)}` : ""} · ${Object.values(
335
+ human.choices,
336
+ )
337
+ .map((choice) => sanitizeText(choice.label))
338
+ .join(
339
+ " / ",
340
+ )}${typeof waitingRequest?.presentationDigest === "string" ? ` · ${waitingRequest.presentationDigest.slice(7, 19)}` : ""}`
341
+ : selected !== undefined
342
+ ? `human: ${sanitizeText(selected.label)}`
343
+ : undefined;
308
344
  const detail =
309
- atLatestStep && state.currentNode === nodeId && state.statusDetail
345
+ humanDetail ??
346
+ (atLatestStep && state.currentNode === nodeId && state.statusDetail
310
347
  ? sanitizeText(state.statusDetail)
311
348
  : node?.statusDetail
312
349
  ? sanitizeText(node.statusDetail)
313
350
  : node?.summary
314
351
  ? sanitizeText(node.summary)
315
- : "";
352
+ : "");
316
353
  const branchLines = Array.from({ length: metrics.branchRows }, (_, index) =>
317
354
  labels[index] ? `◇ ${labels[index]}` : "",
318
355
  );
319
356
  const count = atLatestStep && state.currentNode === nodeId ? Math.max(attempts, 1) : attempts;
320
357
  const timing =
321
358
  attempt || count > 0 ? `${count} attempt${count === 1 ? "" : "s"} · ${elapsed}` : "not visited";
322
- const text = `${nodeId} [${nodeType}] ${timing}`;
359
+ const displayNodeId = hierarchicalNodeLabel(nodeId, node);
360
+ const text = `${displayNodeId} [${nodeType}] ${timing}`;
323
361
  return {
324
362
  cell,
325
363
  text,
326
- nodeId: sanitizeText(nodeId),
364
+ nodeId: sanitizeText(displayNodeId),
327
365
  nodeType,
328
366
  typeBadge: node ? nodeTypeBadge(node.nodeType, node.actionExecution) : "? unknown",
329
367
  status,
@@ -337,6 +375,17 @@ function renderCellText(
337
375
  };
338
376
  }
339
377
 
378
+ function hierarchicalNodeLabel(
379
+ nodeId: string,
380
+ node: WorkflowDefinitionSnapshot["nodes"][string] | undefined,
381
+ ): string {
382
+ if (node?.mountPath === undefined || node.localNodeId === undefined) return nodeId;
383
+ const path = node.mountPath.join(" › ");
384
+ if (node.includeTransition === "entry") return `${path} · enter`;
385
+ if (node.includeTransition === "exit") return `${path} · ${node.localNodeId} exit`;
386
+ return `${path} › ${node.localNodeId}`;
387
+ }
388
+
340
389
  type RankGeometry = {
341
390
  cells: RenderedCell[];
342
391
  centers: number[];
@@ -2,13 +2,22 @@ import { ansi, fitWidth, sanitizeText } from "../render/ansi.js";
2
2
  import { formatDuration, runElapsedMs } from "../render/format.js";
3
3
  import { renderGraphLines } from "../render/graph-render.js";
4
4
  import { decodeValueWith } from "../workflows/artifacts.js";
5
+ import {
6
+ decisionDocumentSegments,
7
+ decisionPresentationFingerprint,
8
+ humanDecisionChannelRequest,
9
+ } from "../workflows/decision-presentation.js";
5
10
  import {
6
11
  formatProgressLine,
7
12
  progressRecordsFromTrace,
8
13
  progressTracksFromRecords,
9
14
  } from "../workflows/progress.js";
10
15
  import type { LoadedRunBundle } from "../workflows/store.js";
11
- import type { WorkflowRunStatus, WorkflowStepRecord } from "../workflows/types.js";
16
+ import type {
17
+ HumanDecisionRequest,
18
+ WorkflowRunStatus,
19
+ WorkflowStepRecord,
20
+ } from "../workflows/types.js";
12
21
 
13
22
  export { formatDuration, runElapsedMs };
14
23
 
@@ -132,10 +141,31 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
132
141
  suffix = ansi.cyan(` running ${formatDuration(now.getTime() - startedAt)}${detail}`);
133
142
  } else if (state.waitingOn === nodeId) {
134
143
  glyph = ansi.yellow("⏸");
135
- suffix = ansi.yellow(" waiting");
144
+ const human = bundle.snapshot?.nodes[nodeId]?.humanDecision;
145
+ const request = humanDecisionRequest(state.finalOutput);
146
+ const requestAudience = request?.audience ?? human?.audience;
147
+ suffix = ansi.yellow(
148
+ human === undefined
149
+ ? " waiting"
150
+ : ` waiting for human · ${sanitizeText(requestAudience ?? "operator")}${request?.schema === "pi-workflows.human-decision-request.v2" ? ` · ${sanitizeText(request.presentation.summary)}` : ""} · ${Object.values(
151
+ human.choices,
152
+ )
153
+ .map((choice) => sanitizeText(choice.label))
154
+ .join(
155
+ " / ",
156
+ )}${request?.schema === "pi-workflows.human-decision-request.v2" ? ` · ${request.presentationDigest.slice(7, 19)}` : ""}`,
157
+ );
136
158
  } else if (result) {
137
159
  glyph = result.outcome === "ok" ? ansi.green("✓") : ansi.red("✗");
138
- suffix = ansi.dim(` ${formatDuration(result.durationMs)}`);
160
+ const human = bundle.snapshot?.nodes[nodeId]?.humanDecision;
161
+ const accepted = state.humanDecision;
162
+ const selected =
163
+ human !== undefined && accepted !== undefined && accepted.nodeId === nodeId
164
+ ? human.choices[accepted.response.choice]
165
+ : undefined;
166
+ suffix = ansi.dim(
167
+ ` ${formatDuration(result.durationMs)}${selected === undefined ? "" : ` · human: ${sanitizeText(selected.label)}`}`,
168
+ );
139
169
  }
140
170
  return fitWidth(` ${glyph} ${nodeId} ${ansi.dim(`[${nodeType}]`)}${suffix}`, width);
141
171
  }
@@ -143,6 +173,20 @@ function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number,
143
173
  /** Pretty-printed JSON body of the selected step for the inspector pane. */
144
174
  function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
145
175
  const lines: string[] = [];
176
+ const request = step.error === undefined ? humanDecisionRequest(step.output) : null;
177
+ if (request !== null) {
178
+ const channelRequest = humanDecisionChannelRequest(request);
179
+ for (const segment of decisionDocumentSegments(channelRequest)) {
180
+ for (const raw of segment.text.split("\n")) {
181
+ lines.push(fitWidth(` ${sanitizeText(raw)}`, width));
182
+ }
183
+ lines.push("");
184
+ }
185
+ lines.push(
186
+ fitWidth(ansi.dim(` decision ${decisionPresentationFingerprint(channelRequest)}`), width),
187
+ );
188
+ return lines;
189
+ }
146
190
  const body = step.error !== undefined ? step.error : withArtifactPlaceholders(step.output);
147
191
  const rendered =
148
192
  typeof body === "string" && step.error !== undefined ? body : JSON.stringify(body, null, 2);
@@ -163,6 +207,15 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
163
207
  return lines;
164
208
  }
165
209
 
210
+ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
211
+ if (value === null || typeof value !== "object") return null;
212
+ const schema = (value as { schema?: unknown }).schema;
213
+ return schema === "pi-workflows.human-decision-request.v1" ||
214
+ schema === "pi-workflows.human-decision-request.v2"
215
+ ? (value as HumanDecisionRequest)
216
+ : null;
217
+ }
218
+
166
219
  /**
167
220
  * Full-run detail view: header, graph pane, step timeline, inspector.
168
221
  * `scroll` shifts the viewport down over the full body; `selectedStepIndex`
@@ -83,6 +83,13 @@ export class BuiltinWorkflowCatalog {
83
83
  return this.byName.get(name);
84
84
  }
85
85
 
86
+ sourceForDefinition(definition: WorkflowDefinition): WorkflowSource | undefined {
87
+ const entry = this.list().find((candidate) => candidate.definition === definition);
88
+ return entry === undefined
89
+ ? undefined
90
+ : { kind: "builtin", id: entry.id, revision: entry.revision };
91
+ }
92
+
86
93
  resolve(
87
94
  source: WorkflowSource,
88
95
  runId = `builtin:${source.kind === "builtin" ? source.id : "unknown"}`,