@osolmaz/pi-workflows 0.10.0 → 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 (125) hide show
  1. package/README.md +23 -12
  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 +210 -11
  6. package/dist/builtins/autoimplement.workflow.js +245 -13
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/{autodevise.workflow.d.ts → autoplan.workflow.d.ts} +9 -9
  9. package/dist/builtins/{autodevise.workflow.js → autoplan.workflow.js} +20 -20
  10. package/dist/builtins/autoplan.workflow.js.map +1 -0
  11. package/dist/builtins/catalog.js +8 -4
  12. package/dist/builtins/catalog.js.map +1 -1
  13. package/dist/builtins/index.d.ts +6 -3
  14. package/dist/builtins/index.js +5 -2
  15. package/dist/builtins/index.js.map +1 -1
  16. package/dist/builtins/monitor.workflow.d.ts +4 -0
  17. package/dist/builtins/monitor.workflow.js +134 -9
  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 +33 -8
  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/composition.js +15 -1
  43. package/dist/workflows/composition.js.map +1 -1
  44. package/dist/workflows/decision-presentation.d.ts +18 -0
  45. package/dist/workflows/decision-presentation.js +417 -0
  46. package/dist/workflows/decision-presentation.js.map +1 -0
  47. package/dist/workflows/engine.d.ts +2 -1
  48. package/dist/workflows/engine.js +86 -4
  49. package/dist/workflows/engine.js.map +1 -1
  50. package/dist/workflows/human-decision.d.ts +87 -0
  51. package/dist/workflows/human-decision.js +583 -0
  52. package/dist/workflows/human-decision.js.map +1 -0
  53. package/dist/workflows/index.d.ts +3 -1
  54. package/dist/workflows/index.js +2 -0
  55. package/dist/workflows/index.js.map +1 -1
  56. package/dist/workflows/schema.js +15 -0
  57. package/dist/workflows/schema.js.map +1 -1
  58. package/dist/workflows/store.js +7 -0
  59. package/dist/workflows/store.js.map +1 -1
  60. package/dist/workflows/tool-input.d.ts +72 -0
  61. package/dist/workflows/tool-input.js +141 -0
  62. package/dist/workflows/tool-input.js.map +1 -0
  63. package/dist/workflows/types.d.ts +223 -0
  64. package/docs/HUMAN_DECISIONS.md +371 -0
  65. package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
  66. package/docs/MONITOR.md +17 -6
  67. package/docs/WORKFLOW_COMPOSITION.md +13 -6
  68. package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
  69. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
  70. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
  71. package/docs/plans/2026-08-19-workflow-composition-plan.md +25 -17
  72. package/docs/run-bundles.md +23 -2
  73. package/docs/workflows.md +43 -5
  74. package/examples/workflows/approved-plan.workflow.ts +58 -0
  75. package/examples/workflows/autoplan.workflow.ts +1 -0
  76. package/examples/workflows/human-decision.workflow.ts +62 -0
  77. package/herdr-plugin.toml +1 -1
  78. package/package.json +2 -1
  79. package/schemas/decision-presentation-v1.schema.json +83 -0
  80. package/schemas/human-decision-accepted-v1.schema.json +44 -0
  81. package/schemas/human-decision-accepted-v2.schema.json +50 -0
  82. package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
  83. package/schemas/human-decision-cancellation-v1.schema.json +15 -0
  84. package/schemas/human-decision-continuation-v1.schema.json +16 -0
  85. package/schemas/human-decision-delivery-v1.schema.json +28 -0
  86. package/schemas/human-decision-delivery-v2.schema.json +36 -0
  87. package/schemas/human-decision-receipt-v1.schema.json +33 -0
  88. package/schemas/human-decision-receipt-v2.schema.json +39 -0
  89. package/schemas/human-decision-request-v1.schema.json +59 -0
  90. package/schemas/human-decision-request-v2.schema.json +69 -0
  91. package/schemas/human-decision-resolution-v1.schema.json +27 -0
  92. package/schemas/human-decision-resolution-v2.schema.json +27 -0
  93. package/schemas/human-decision-settlement-v1.schema.json +28 -0
  94. package/skills/autodoc/SKILL.md +43 -0
  95. package/skills/autoimplement/SKILL.md +58 -0
  96. package/skills/autoimplement/agents/openai.yaml +4 -0
  97. package/skills/autoplan/SKILL.md +25 -0
  98. package/skills/monitor/SKILL.md +3 -1
  99. package/skills/pi-workflows/SKILL.md +3 -3
  100. package/src/builtins/autodoc.workflow.ts +325 -0
  101. package/src/builtins/autoimplement.workflow.ts +281 -14
  102. package/src/builtins/{autodevise.workflow.ts → autoplan.workflow.ts} +32 -32
  103. package/src/builtins/catalog.ts +8 -4
  104. package/src/builtins/index.ts +25 -6
  105. package/src/builtins/monitor.workflow.ts +162 -8
  106. package/src/builtins/plan-approval.workflow.ts +190 -0
  107. package/src/builtins/plan-presentation.ts +57 -0
  108. package/src/extension/decision-channels.ts +1533 -0
  109. package/src/extension/index.ts +499 -9
  110. package/src/extension/widget.ts +39 -2
  111. package/src/extension/workflow-tool.ts +5 -81
  112. package/src/host/rpc-bridge.ts +7 -30
  113. package/src/render/graph-render.ts +40 -3
  114. package/src/viewer/render.ts +56 -3
  115. package/src/workflows/composition.ts +16 -1
  116. package/src/workflows/decision-presentation.ts +501 -0
  117. package/src/workflows/engine.ts +108 -4
  118. package/src/workflows/human-decision.ts +819 -0
  119. package/src/workflows/index.ts +69 -0
  120. package/src/workflows/schema.ts +17 -0
  121. package/src/workflows/store.ts +8 -0
  122. package/src/workflows/tool-input.ts +202 -0
  123. package/src/workflows/types.ts +265 -0
  124. package/dist/builtins/autodevise.workflow.js.map +0 -1
  125. package/examples/workflows/autodevise.workflow.ts +0 -1
@@ -20,8 +20,10 @@ import type {
20
20
  WorkflowProgressData,
21
21
  } from "../workflows/types.js";
22
22
  import { validateProgressData } from "../workflows/updates.js";
23
- import autodeviseWorkflow from "./autodevise.workflow.js";
23
+ import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
24
24
  import autoimplementWorkflow, { type AutoimplementInput } from "./autoimplement.workflow.js";
25
+ import autoplanWorkflow from "./autoplan.workflow.js";
26
+ import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
25
27
 
26
28
  const MIN_INTERVAL_MINUTES = 1;
27
29
  const MAX_INTERVAL_MINUTES = 24 * 60;
@@ -45,6 +47,10 @@ export type MonitorRepairPolicy = {
45
47
  repository?: string;
46
48
  baseBranch?: string;
47
49
  merge?: boolean;
50
+ approval?: {
51
+ audience: string;
52
+ maxReplans: number;
53
+ };
48
54
  };
49
55
 
50
56
  export type MonitorInput = {
@@ -163,6 +169,22 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
163
169
  if (raw.merge !== undefined && typeof raw.merge !== "boolean") {
164
170
  throw new Error("repair merge must be a boolean");
165
171
  }
172
+ let approval: MonitorRepairPolicy["approval"];
173
+ if (raw.approval !== undefined) {
174
+ const value = requireRecord(raw.approval, "repair approval");
175
+ const maxReplans = value.maxReplans ?? 3;
176
+ if (
177
+ !Number.isInteger(maxReplans) ||
178
+ (maxReplans as number) < 1 ||
179
+ (maxReplans as number) > 20
180
+ ) {
181
+ throw new Error("repair approval maxReplans must be from 1 through 20");
182
+ }
183
+ approval = {
184
+ audience: requireBoundedString(value.audience, "repair approval audience", 200),
185
+ maxReplans: maxReplans as number,
186
+ };
187
+ }
166
188
  repair = {
167
189
  authorized: true,
168
190
  ...(raw.scope !== undefined
@@ -176,6 +198,7 @@ export function prepareMonitorInput(input: unknown): MonitorConfig {
176
198
  ? { baseBranch: requireBoundedString(raw.baseBranch, "repair base branch", 256) }
177
199
  : {}),
178
200
  ...(raw.merge !== undefined ? { merge: raw.merge !== false } : {}),
201
+ ...(approval !== undefined ? { approval } : {}),
179
202
  };
180
203
  }
181
204
  return {
@@ -297,9 +320,56 @@ function repeatedRepairWithoutProgress(context: WorkflowNodeContext): boolean {
297
320
  return false;
298
321
  }
299
322
 
323
+ function currentRepairPlan(outputs: Record<string, unknown>): {
324
+ plan: unknown;
325
+ planDigest: string;
326
+ documents: string[];
327
+ } {
328
+ const documentation = outputs.documentation as
329
+ | {
330
+ exit?: string;
331
+ output?: {
332
+ plan?: unknown;
333
+ planDigest?: string;
334
+ documentation?: { files?: string[] };
335
+ };
336
+ }
337
+ | undefined;
338
+ if (
339
+ documentation?.exit === "ready" &&
340
+ documentation.output?.plan !== undefined &&
341
+ typeof documentation.output.planDigest === "string"
342
+ ) {
343
+ return {
344
+ plan: documentation.output.plan,
345
+ planDigest: documentation.output.planDigest,
346
+ documents: documentation.output.documentation?.files ?? [],
347
+ };
348
+ }
349
+ const design = includedResult(autoplanWorkflow, outputs.initialDesign);
350
+ if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
351
+ return { plan: design.output.plan, planDigest: design.output.planDigest, documents: [] };
352
+ }
353
+
354
+ function latestReplanInstructions(outputs: Record<string, unknown>): string | undefined {
355
+ const approval = outputs.approval as
356
+ | { exit?: string; output?: { instructions?: unknown } }
357
+ | undefined;
358
+ return approval?.exit === "replan" && typeof approval.output?.instructions === "string"
359
+ ? approval.output.instructions
360
+ : undefined;
361
+ }
362
+
300
363
  function repairBlockedReason(outputs: Record<string, unknown>): string {
301
364
  const guard = outputs.repairGuard as { reason?: string; route?: string } | undefined;
302
365
  if (guard?.route === "blocked" && guard.reason !== undefined) return guard.reason;
366
+ const replan = outputs.replanGuard as { reason?: string; route?: string } | undefined;
367
+ if (replan?.route === "blocked" && replan.reason !== undefined) return replan.reason;
368
+ const approval = outputs.approval as { exit?: string; output?: { status?: string } } | undefined;
369
+ if (approval?.exit === "stop") return "The operator stopped the proposed repair.";
370
+ const documentation = outputs.documentation as
371
+ | { exit?: string; output?: { reason?: string } }
372
+ | undefined;
303
373
  const implementation = outputs.implementation as
304
374
  | { exit?: string; output?: { reason?: string } }
305
375
  | undefined;
@@ -308,6 +378,7 @@ function repairBlockedReason(outputs: Record<string, unknown>): string {
308
378
  | undefined;
309
379
  return (
310
380
  implementation?.output?.reason ??
381
+ documentation?.output?.reason ??
311
382
  design?.output?.reason ??
312
383
  "The repair did not produce new verified progress."
313
384
  );
@@ -357,19 +428,66 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
357
428
  maxSteps: 200_000,
358
429
  includes: {
359
430
  initialDesign: includeWorkflow({
360
- workflow: "autodevise",
361
- contract: autodeviseWorkflow,
431
+ workflow: "autoplan",
432
+ contract: autoplanWorkflow,
362
433
  input: ({ outputs }) => {
363
434
  const config = configFrom(outputs);
364
435
  const repair = (outputs.check as MonitorCheck).repair;
365
436
  if (repair === undefined) throw new Error("monitor repair details are missing");
437
+ const prior = outputs.initialDesign as
438
+ | { exit?: string; output?: { plan?: unknown } }
439
+ | undefined;
440
+ const instructions = latestReplanInstructions(outputs);
366
441
  return {
367
442
  problem: repair.problem,
368
443
  ...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
369
444
  ...(config.repair?.constraints !== undefined
370
445
  ? { constraints: config.repair.constraints }
371
446
  : {}),
372
- newEvidence: repair.evidence,
447
+ ...(prior?.exit === "ready" && prior.output?.plan !== undefined
448
+ ? { previousPlan: prior.output.plan }
449
+ : {}),
450
+ newEvidence:
451
+ instructions === undefined
452
+ ? repair.evidence
453
+ : { observedEvidence: repair.evidence, operatorInstructions: instructions },
454
+ };
455
+ },
456
+ }),
457
+ documentation: includeWorkflow(autodocWorkflow, {
458
+ input: ({ outputs }): AutodocInput => {
459
+ const config = configFrom(outputs);
460
+ const repair = (outputs.check as MonitorCheck).repair;
461
+ const design = includedResult(autoplanWorkflow, outputs.initialDesign);
462
+ if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
463
+ if (repair === undefined) throw new Error("monitor repair details are missing");
464
+ return {
465
+ task: repair.problem,
466
+ plan: design.output.plan,
467
+ ...(config.repair?.repository !== undefined
468
+ ? { repository: config.repair.repository }
469
+ : {}),
470
+ evidence: latestReplanInstructions(outputs) ?? repair.evidence,
471
+ };
472
+ },
473
+ }),
474
+ approval: includeWorkflow(planApprovalWorkflow, {
475
+ input: ({ outputs, state }): PlanApprovalInput => {
476
+ const config = configFrom(outputs);
477
+ const repair = (outputs.check as MonitorCheck).repair;
478
+ const approval = config.repair?.approval;
479
+ if (repair === undefined || approval === undefined) {
480
+ throw new Error("monitor approval was entered without an approval policy");
481
+ }
482
+ const current = currentRepairPlan(outputs);
483
+ const revision =
484
+ state.steps.filter((step) => step.nodeId === "approval/approve").length + 1;
485
+ return {
486
+ task: repair.problem,
487
+ plan: current.plan,
488
+ planDigest: current.planDigest,
489
+ audience: approval.audience,
490
+ revision,
373
491
  };
374
492
  },
375
493
  }),
@@ -379,12 +497,16 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
379
497
  input: ({ outputs }) => {
380
498
  const config = configFrom(outputs);
381
499
  const repair = (outputs.check as MonitorCheck).repair;
382
- const design = includedResult(autodeviseWorkflow, outputs.initialDesign);
383
- if (design.exit !== "ready") throw new Error("monitor design did not return a ready plan");
500
+ const documented = currentRepairPlan(outputs);
384
501
  if (repair === undefined) throw new Error("monitor repair details are missing");
385
502
  const request: AutoimplementInput = {
386
503
  task: repair.problem,
387
- plan: design.output.plan,
504
+ plan: documented.plan,
505
+ documentation: {
506
+ status: "current",
507
+ planDigest: documented.planDigest,
508
+ documents: documented.documents,
509
+ },
388
510
  ...(config.repair?.scope !== undefined ? { scope: config.repair.scope } : {}),
389
511
  ...(config.repair?.constraints !== undefined
390
512
  ? { constraints: config.repair.constraints }
@@ -481,6 +603,25 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
481
603
  }
482
604
  : { route: "repair", reason: "The issue is new or has changed evidence." },
483
605
  }),
606
+ approvalRoute: compute({
607
+ run: ({ outputs }) => ({
608
+ route: configFrom(outputs).repair?.approval === undefined ? "implement" : "approve",
609
+ }),
610
+ }),
611
+ replanGuard: compute({
612
+ run: (context) => {
613
+ const limit = configFrom(context.outputs).repair?.approval?.maxReplans ?? 3;
614
+ const replans = context.state.steps.filter(
615
+ (step) => step.nodeId === "approval/replan",
616
+ ).length;
617
+ return replans > limit
618
+ ? {
619
+ route: "blocked",
620
+ reason: `Monitor repair reached the ${limit}-replan safety limit.`,
621
+ }
622
+ : { route: "design", replans, limit };
623
+ },
624
+ }),
484
625
  repairBlocked: compute({
485
626
  run: (context) => ({
486
627
  reason: repairBlockedReason(context.outputs),
@@ -568,8 +709,21 @@ const monitorWorkflow: WorkflowDefinition = defineWorkflow({
568
709
  from: "repairGuard",
569
710
  switch: { on: "$.route", cases: { repair: "initialDesign", blocked: "repairBlocked" } },
570
711
  },
571
- { from: "initialDesign.ready", to: "implementation" },
712
+ { from: "initialDesign.ready", to: "documentation" },
572
713
  { from: "initialDesign.blocked", to: "repairBlocked" },
714
+ { from: "documentation.ready", to: "approvalRoute" },
715
+ { from: "documentation.blocked", to: "repairBlocked" },
716
+ {
717
+ from: "approvalRoute",
718
+ switch: { on: "$.route", cases: { approve: "approval", implement: "implementation" } },
719
+ },
720
+ { from: "approval.continue", to: "implementation" },
721
+ { from: "approval.stop", to: "repairBlocked" },
722
+ { from: "approval.replan", to: "replanGuard" },
723
+ {
724
+ from: "replanGuard",
725
+ switch: { on: "$.route", cases: { design: "initialDesign", blocked: "repairBlocked" } },
726
+ },
573
727
  { from: "implementation.completed", to: "check" },
574
728
  { from: "implementation.blocked", to: "repairBlocked" },
575
729
  { from: "repairBlocked", to: "repairReport" },
@@ -0,0 +1,190 @@
1
+ import { compute, defineWorkflow } from "../workflows/definition.js";
2
+ import {
3
+ choice,
4
+ defineHumanChoices,
5
+ humanDecision,
6
+ humanDecisionEdge,
7
+ textInput,
8
+ } from "../workflows/human-decision.js";
9
+ import type { HumanDecisionReceipt, HumanDecisionResponse } from "../workflows/types.js";
10
+ import { presentPlan } from "./plan-presentation.js";
11
+
12
+ export type PlanApprovalInput = {
13
+ task: string;
14
+ plan: unknown;
15
+ planDigest: string;
16
+ audience: string;
17
+ revision?: number;
18
+ };
19
+
20
+ export type PlanApprovalContinue = {
21
+ status: "continue";
22
+ plan: unknown;
23
+ planDigest: string;
24
+ decision: HumanDecisionReceipt;
25
+ };
26
+
27
+ export type PlanApprovalStop = {
28
+ status: "stop";
29
+ planDigest: string;
30
+ decision: HumanDecisionReceipt;
31
+ };
32
+
33
+ export type PlanApprovalReplan = {
34
+ status: "replan";
35
+ plan: unknown;
36
+ planDigest: string;
37
+ instructions: string;
38
+ decision: HumanDecisionReceipt;
39
+ };
40
+
41
+ const planChoices = defineHumanChoices({
42
+ continue: choice({ label: "Yes, continue" }),
43
+ stop: choice({ label: "No, stop" }),
44
+ replan: choice({
45
+ label: "Replan",
46
+ input: textInput({
47
+ name: "instructions",
48
+ prompt: "What should change?",
49
+ minLength: 1,
50
+ maxLength: 4_000,
51
+ }),
52
+ }),
53
+ });
54
+
55
+ function requireRecord(value: unknown, label: string): Record<string, unknown> {
56
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
57
+ throw new Error(`${label} must be an object`);
58
+ }
59
+ return value as Record<string, unknown>;
60
+ }
61
+
62
+ function requireString(value: unknown, label: string): string {
63
+ if (typeof value !== "string" || value.trim().length === 0) {
64
+ throw new Error(`${label} must be a non-empty string`);
65
+ }
66
+ return value.trim();
67
+ }
68
+
69
+ function parseInput(value: unknown): PlanApprovalInput {
70
+ const input = requireRecord(value, "plan approval input");
71
+ const revision = input.revision;
72
+ if (revision !== undefined && (!Number.isInteger(revision) || (revision as number) < 1)) {
73
+ throw new Error("plan approval revision must be a positive integer");
74
+ }
75
+ if (input.plan === undefined) throw new Error("plan approval requires a plan");
76
+ return {
77
+ task: requireString(input.task, "plan approval task"),
78
+ plan: input.plan,
79
+ planDigest: requireString(input.planDigest, "plan approval digest"),
80
+ audience: requireString(input.audience, "plan approval audience"),
81
+ ...(revision !== undefined ? { revision: revision as number } : {}),
82
+ };
83
+ }
84
+
85
+ function acceptedDecision(value: HumanDecisionReceipt | undefined): HumanDecisionReceipt {
86
+ if (value === undefined)
87
+ throw new Error("plan approval continuation is missing its accepted decision");
88
+ return value;
89
+ }
90
+
91
+ function gateResponse(value: unknown): HumanDecisionResponse {
92
+ return requireRecord(value, "plan approval response") as HumanDecisionResponse;
93
+ }
94
+
95
+ export const planApprovalWorkflow = defineWorkflow({
96
+ source: import.meta.url,
97
+ contractId: "pi-workflows.plan-approval.v1",
98
+ name: "plan-approval",
99
+ input: parseInput,
100
+ title: ({ input }) => `plan approval: ${input.task.slice(0, 60)}`,
101
+ startAt: "approve",
102
+ maxSteps: 8,
103
+ exits: {
104
+ continue: {
105
+ from: "continued",
106
+ validate: (value: unknown): PlanApprovalContinue => value as PlanApprovalContinue,
107
+ },
108
+ stop: {
109
+ from: "stopped",
110
+ validate: (value: unknown): PlanApprovalStop => value as PlanApprovalStop,
111
+ },
112
+ replan: {
113
+ from: "replan",
114
+ validate: (value: unknown): PlanApprovalReplan => value as PlanApprovalReplan,
115
+ },
116
+ },
117
+ nodes: {
118
+ approve: humanDecision({
119
+ audience: ({ input }) => (input as PlanApprovalInput).audience,
120
+ choices: planChoices,
121
+ request: ({ input }) => {
122
+ const request = input as PlanApprovalInput;
123
+ const revision = request.revision ?? 1;
124
+ return {
125
+ title: "Approve the implementation plan",
126
+ subject: {
127
+ task: request.task,
128
+ plan: request.plan,
129
+ planDigest: request.planDigest,
130
+ revision,
131
+ },
132
+ presentation: presentPlan({
133
+ task: request.task,
134
+ plan: request.plan,
135
+ planDigest: request.planDigest,
136
+ revision,
137
+ }),
138
+ revision,
139
+ };
140
+ },
141
+ }),
142
+ continued: compute({
143
+ run: ({ input, state }) => {
144
+ const request = input as PlanApprovalInput;
145
+ return {
146
+ status: "continue",
147
+ plan: request.plan,
148
+ planDigest: request.planDigest,
149
+ decision: acceptedDecision(state.humanDecision),
150
+ } satisfies PlanApprovalContinue;
151
+ },
152
+ }),
153
+ stopped: compute({
154
+ run: ({ input, state }) => {
155
+ const request = input as PlanApprovalInput;
156
+ return {
157
+ status: "stop",
158
+ planDigest: request.planDigest,
159
+ decision: acceptedDecision(state.humanDecision),
160
+ } satisfies PlanApprovalStop;
161
+ },
162
+ }),
163
+ replan: compute({
164
+ run: ({ input, outputs, state }) => {
165
+ const request = input as PlanApprovalInput;
166
+ const response = gateResponse(outputs.approve);
167
+ const instructions = response.input?.instructions;
168
+ if (typeof instructions !== "string" || instructions.length === 0) {
169
+ throw new Error("replan answer is missing exact instructions");
170
+ }
171
+ return {
172
+ status: "replan",
173
+ plan: request.plan,
174
+ planDigest: request.planDigest,
175
+ instructions,
176
+ decision: acceptedDecision(state.humanDecision),
177
+ } satisfies PlanApprovalReplan;
178
+ },
179
+ }),
180
+ },
181
+ edges: [
182
+ humanDecisionEdge({
183
+ from: "approve",
184
+ choices: planChoices,
185
+ cases: { continue: "continued", stop: "stopped", replan: "replan" },
186
+ }),
187
+ ],
188
+ });
189
+
190
+ export default planApprovalWorkflow;
@@ -0,0 +1,57 @@
1
+ import {
2
+ legacyDecisionPresentation,
3
+ normalizeDecisionPresentation,
4
+ } from "../workflows/decision-presentation.js";
5
+ import type { DecisionPresentation } from "../workflows/types.js";
6
+
7
+ export function presentPlan(input: {
8
+ task: string;
9
+ plan: unknown;
10
+ planDigest: string;
11
+ revision: number;
12
+ }): DecisionPresentation {
13
+ const plan = legacyDecisionPresentation(input.plan);
14
+ const summary =
15
+ readableSummary(planSummary(input.plan)) ??
16
+ readableSummary(`Review the implementation plan for ${input.task}.`)!;
17
+ return normalizeDecisionPresentation({
18
+ schema: "pi-workflows.decision-presentation.v1",
19
+ summary,
20
+ blocks: [
21
+ { kind: "section", title: "Task" },
22
+ { kind: "paragraph", text: input.task },
23
+ { kind: "section", title: "Plan" },
24
+ ...plan.blocks.filter(
25
+ (block, index) =>
26
+ !(index === 0 && block.kind === "section" && block.title === "Decision details"),
27
+ ),
28
+ { kind: "section", title: "Plan identity" },
29
+ {
30
+ kind: "fields",
31
+ items: [
32
+ { label: "Revision", value: String(input.revision) },
33
+ { label: "Digest", value: input.planDigest },
34
+ ],
35
+ },
36
+ ],
37
+ });
38
+ }
39
+
40
+ function readableSummary(value: string | undefined): string | undefined {
41
+ if (value === undefined) return undefined;
42
+ const text = [...value]
43
+ .map((character) => {
44
+ const code = character.codePointAt(0) ?? 0;
45
+ return code <= 31 || code === 127 ? " " : character;
46
+ })
47
+ .join("")
48
+ .replaceAll(/\s+/gu, " ")
49
+ .trim();
50
+ return text.length === 0 ? undefined : text;
51
+ }
52
+
53
+ function planSummary(plan: unknown): string | undefined {
54
+ if (plan === null || typeof plan !== "object" || Array.isArray(plan)) return undefined;
55
+ const summary = (plan as Record<string, unknown>).summary;
56
+ return typeof summary === "string" && summary.trim().length > 0 ? summary : undefined;
57
+ }