@osolmaz/pi-workflows 0.10.0 → 0.11.1

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 (135) hide show
  1. package/README.md +32 -14
  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 +212 -11
  6. package/dist/builtins/autoimplement.workflow.js +431 -22
  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/herdr/setup.d.ts +13 -1
  37. package/dist/herdr/setup.js +349 -36
  38. package/dist/herdr/setup.js.map +1 -1
  39. package/dist/host/rpc-bridge.js +4 -21
  40. package/dist/host/rpc-bridge.js.map +1 -1
  41. package/dist/render/graph-render.js +33 -8
  42. package/dist/render/graph-render.js.map +1 -1
  43. package/dist/viewer/cli.d.ts +1 -0
  44. package/dist/viewer/cli.js +21 -10
  45. package/dist/viewer/cli.js.map +1 -1
  46. package/dist/viewer/render.js +36 -2
  47. package/dist/viewer/render.js.map +1 -1
  48. package/dist/workflows/composition.js +15 -1
  49. package/dist/workflows/composition.js.map +1 -1
  50. package/dist/workflows/decision-presentation.d.ts +18 -0
  51. package/dist/workflows/decision-presentation.js +417 -0
  52. package/dist/workflows/decision-presentation.js.map +1 -0
  53. package/dist/workflows/engine.d.ts +2 -1
  54. package/dist/workflows/engine.js +86 -4
  55. package/dist/workflows/engine.js.map +1 -1
  56. package/dist/workflows/human-decision.d.ts +87 -0
  57. package/dist/workflows/human-decision.js +583 -0
  58. package/dist/workflows/human-decision.js.map +1 -0
  59. package/dist/workflows/index.d.ts +3 -1
  60. package/dist/workflows/index.js +2 -0
  61. package/dist/workflows/index.js.map +1 -1
  62. package/dist/workflows/schema.js +15 -0
  63. package/dist/workflows/schema.js.map +1 -1
  64. package/dist/workflows/store.js +7 -0
  65. package/dist/workflows/store.js.map +1 -1
  66. package/dist/workflows/tool-input.d.ts +72 -0
  67. package/dist/workflows/tool-input.js +141 -0
  68. package/dist/workflows/tool-input.js.map +1 -0
  69. package/dist/workflows/types.d.ts +223 -0
  70. package/docs/HUMAN_DECISIONS.md +371 -0
  71. package/docs/HUMAN_DECISION_PRESENTATIONS.md +323 -0
  72. package/docs/MONITOR.md +17 -6
  73. package/docs/WORKFLOW_COMPOSITION.md +13 -6
  74. package/docs/plans/2026-08-19-human-decision-gates-plan.md +296 -0
  75. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +172 -0
  76. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +45 -0
  77. package/docs/plans/2026-08-19-workflow-composition-plan.md +25 -17
  78. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +138 -0
  79. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +104 -0
  80. package/docs/run-bundles.md +23 -2
  81. package/docs/workflows.md +47 -5
  82. package/examples/workflows/approved-plan.workflow.ts +58 -0
  83. package/examples/workflows/autoplan.workflow.ts +1 -0
  84. package/examples/workflows/human-decision.workflow.ts +62 -0
  85. package/herdr-plugin.toml +1 -1
  86. package/package.json +2 -1
  87. package/schemas/decision-presentation-v1.schema.json +83 -0
  88. package/schemas/human-decision-accepted-v1.schema.json +44 -0
  89. package/schemas/human-decision-accepted-v2.schema.json +50 -0
  90. package/schemas/human-decision-answer-attempt-v1.schema.json +37 -0
  91. package/schemas/human-decision-cancellation-v1.schema.json +15 -0
  92. package/schemas/human-decision-continuation-v1.schema.json +16 -0
  93. package/schemas/human-decision-delivery-v1.schema.json +28 -0
  94. package/schemas/human-decision-delivery-v2.schema.json +36 -0
  95. package/schemas/human-decision-receipt-v1.schema.json +33 -0
  96. package/schemas/human-decision-receipt-v2.schema.json +39 -0
  97. package/schemas/human-decision-request-v1.schema.json +59 -0
  98. package/schemas/human-decision-request-v2.schema.json +69 -0
  99. package/schemas/human-decision-resolution-v1.schema.json +27 -0
  100. package/schemas/human-decision-resolution-v2.schema.json +27 -0
  101. package/schemas/human-decision-settlement-v1.schema.json +28 -0
  102. package/skills/autodoc/SKILL.md +43 -0
  103. package/skills/autoimplement/SKILL.md +58 -0
  104. package/skills/autoimplement/agents/openai.yaml +4 -0
  105. package/skills/autoplan/SKILL.md +25 -0
  106. package/skills/monitor/SKILL.md +3 -1
  107. package/skills/pi-workflows/SKILL.md +3 -3
  108. package/src/builtins/autodoc.workflow.ts +325 -0
  109. package/src/builtins/autoimplement.workflow.ts +493 -23
  110. package/src/builtins/{autodevise.workflow.ts → autoplan.workflow.ts} +32 -32
  111. package/src/builtins/catalog.ts +8 -4
  112. package/src/builtins/index.ts +25 -6
  113. package/src/builtins/monitor.workflow.ts +162 -8
  114. package/src/builtins/plan-approval.workflow.ts +190 -0
  115. package/src/builtins/plan-presentation.ts +57 -0
  116. package/src/extension/decision-channels.ts +1533 -0
  117. package/src/extension/index.ts +499 -9
  118. package/src/extension/widget.ts +39 -2
  119. package/src/extension/workflow-tool.ts +5 -81
  120. package/src/herdr/setup.ts +429 -39
  121. package/src/host/rpc-bridge.ts +7 -30
  122. package/src/render/graph-render.ts +40 -3
  123. package/src/viewer/cli.ts +22 -10
  124. package/src/viewer/render.ts +56 -3
  125. package/src/workflows/composition.ts +16 -1
  126. package/src/workflows/decision-presentation.ts +501 -0
  127. package/src/workflows/engine.ts +108 -4
  128. package/src/workflows/human-decision.ts +819 -0
  129. package/src/workflows/index.ts +69 -0
  130. package/src/workflows/schema.ts +17 -0
  131. package/src/workflows/store.ts +8 -0
  132. package/src/workflows/tool-input.ts +202 -0
  133. package/src/workflows/types.ts +265 -0
  134. package/dist/builtins/autodevise.workflow.js.map +0 -1
  135. package/examples/workflows/autodevise.workflow.ts +0 -1
@@ -7,12 +7,15 @@ import {
7
7
  includedResult,
8
8
  shell,
9
9
  } from "../workflows/definition.js";
10
+ import { digest } from "../workflows/human-decision.js";
10
11
  import type {
11
12
  ShellActionExecution,
12
13
  ShellActionResult,
13
14
  WorkflowNodeContext,
14
15
  } from "../workflows/types.js";
15
- import autodeviseWorkflow, { type AutodeviseInput } from "./autodevise.workflow.js";
16
+ import autodocWorkflow, { type AutodocInput } from "./autodoc.workflow.js";
17
+ import autoplanWorkflow, { type AutoplanInput } from "./autoplan.workflow.js";
18
+ import planApprovalWorkflow, { type PlanApprovalInput } from "./plan-approval.workflow.js";
16
19
 
17
20
  export type AutoimplementInput = {
18
21
  task: string;
@@ -22,6 +25,25 @@ export type AutoimplementInput = {
22
25
  repository?: string;
23
26
  baseBranch?: string;
24
27
  merge?: boolean;
28
+ documents?: string[];
29
+ documentation?: {
30
+ status: "current";
31
+ planDigest: string;
32
+ documents: string[];
33
+ };
34
+ approval?: {
35
+ audience: string;
36
+ maxReplans: number;
37
+ };
38
+ };
39
+
40
+ export type ExistingPlanDiscovery = {
41
+ route: "found" | "blocked";
42
+ plan?: unknown;
43
+ documentation?: "current" | "missing" | "stale";
44
+ documents: string[];
45
+ reason: string;
46
+ evidence: unknown;
25
47
  };
26
48
 
27
49
  type StructuredCommand = {
@@ -65,8 +87,22 @@ export type AutoimplementBlocked = {
65
87
  evidence: unknown;
66
88
  };
67
89
 
90
+ type BlockerChallenge = {
91
+ route: "continue" | "blocked";
92
+ blockingNow: boolean;
93
+ outsideAuthority: boolean;
94
+ canProceed: boolean;
95
+ reason: string;
96
+ nextAction: string;
97
+ alternativesChecked: string[];
98
+ evidence: string[];
99
+ };
100
+
68
101
  const FIVE_MINUTES_MS = 5 * 60_000;
69
102
  const TEN_MINUTES_MS = 10 * 60_000;
103
+ const MAX_BLOCKER_CHALLENGES = 3;
104
+ const MAX_CHALLENGE_ITEMS = 5;
105
+ const MAX_CHALLENGE_TEXT = 500;
70
106
 
71
107
  function requireRecord(value: unknown, label: string): Record<string, unknown> {
72
108
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
@@ -82,6 +118,80 @@ function requireString(value: unknown, label: string): string {
82
118
  return value.trim();
83
119
  }
84
120
 
121
+ function boundedChallengeItems(value: unknown, label: string): string[] {
122
+ if (!Array.isArray(value) || value.length > MAX_CHALLENGE_ITEMS) {
123
+ throw new Error(`${label} must be an array with at most ${MAX_CHALLENGE_ITEMS} items`);
124
+ }
125
+ return value.map((item, index) => {
126
+ const text = requireString(item, `${label}[${index}]`);
127
+ if (text.length > MAX_CHALLENGE_TEXT) {
128
+ throw new Error(`${label}[${index}] must be at most ${MAX_CHALLENGE_TEXT} characters`);
129
+ }
130
+ return text;
131
+ });
132
+ }
133
+
134
+ function parseBlockerChallenge(value: unknown): BlockerChallenge {
135
+ const result = requireRecord(value, "blocker challenge");
136
+ if (result.route !== "continue" && result.route !== "blocked") {
137
+ throw new Error("blocker challenge route must be continue or blocked");
138
+ }
139
+ for (const key of ["blockingNow", "outsideAuthority", "canProceed"] as const) {
140
+ if (typeof result[key] !== "boolean") {
141
+ throw new Error(`blocker challenge ${key} must be a boolean`);
142
+ }
143
+ }
144
+ const blockingNow = result.blockingNow as boolean;
145
+ const outsideAuthority = result.outsideAuthority as boolean;
146
+ const canProceed = result.canProceed as boolean;
147
+ const reason = requireString(result.reason, "blocker challenge reason");
148
+ if (reason.length > MAX_CHALLENGE_TEXT) {
149
+ throw new Error(`blocker challenge reason must be at most ${MAX_CHALLENGE_TEXT} characters`);
150
+ }
151
+ if (typeof result.nextAction !== "string") {
152
+ throw new Error("blocker challenge nextAction must be a string");
153
+ }
154
+ const nextAction = result.nextAction.trim();
155
+ if (nextAction.length > MAX_CHALLENGE_TEXT) {
156
+ throw new Error(
157
+ `blocker challenge nextAction must be at most ${MAX_CHALLENGE_TEXT} characters`,
158
+ );
159
+ }
160
+ const alternativesChecked = boundedChallengeItems(
161
+ result.alternativesChecked,
162
+ "blocker challenge alternativesChecked",
163
+ );
164
+ const evidence = boundedChallengeItems(result.evidence, "blocker challenge evidence");
165
+
166
+ if (result.route === "blocked") {
167
+ if (
168
+ blockingNow !== true ||
169
+ outsideAuthority !== true ||
170
+ canProceed !== false ||
171
+ nextAction.length > 0 ||
172
+ alternativesChecked.length === 0 ||
173
+ evidence.length === 0
174
+ ) {
175
+ throw new Error(
176
+ "blocked challenge requires blockingNow=true, outsideAuthority=true, canProceed=false, an empty nextAction, and concrete alternatives and evidence",
177
+ );
178
+ }
179
+ } else if (canProceed !== true || nextAction.length === 0) {
180
+ throw new Error("continue challenge requires canProceed=true and a practical nextAction");
181
+ }
182
+
183
+ return {
184
+ route: result.route,
185
+ blockingNow,
186
+ outsideAuthority,
187
+ canProceed,
188
+ reason,
189
+ nextAction,
190
+ alternativesChecked,
191
+ evidence,
192
+ };
193
+ }
194
+
85
195
  function parseInput(value: unknown): AutoimplementInput {
86
196
  const input = requireRecord(value, "autoimplement input");
87
197
  const constraints = input.constraints;
@@ -94,6 +204,51 @@ function parseInput(value: unknown): AutoimplementInput {
94
204
  if (input.merge !== undefined && typeof input.merge !== "boolean") {
95
205
  throw new Error("autoimplement merge must be a boolean");
96
206
  }
207
+ const documents = input.documents;
208
+ if (
209
+ documents !== undefined &&
210
+ (!Array.isArray(documents) || documents.some((item) => typeof item !== "string"))
211
+ ) {
212
+ throw new Error("autoimplement documents must be an array of strings");
213
+ }
214
+ let documentation: AutoimplementInput["documentation"];
215
+ if (input.documentation !== undefined) {
216
+ if (input.plan === undefined) {
217
+ throw new Error("autoimplement documentation requires an explicit plan");
218
+ }
219
+ const raw = requireRecord(input.documentation, "autoimplement documentation");
220
+ if (raw.status !== "current") {
221
+ throw new Error("autoimplement documentation status must be current");
222
+ }
223
+ const planDigest = requireString(raw.planDigest, "autoimplement documentation planDigest");
224
+ if (planDigest !== digest(input.plan)) {
225
+ throw new Error("autoimplement documentation planDigest does not match the explicit plan");
226
+ }
227
+ if (!Array.isArray(raw.documents) || raw.documents.some((item) => typeof item !== "string")) {
228
+ throw new Error("autoimplement documentation documents must be an array of strings");
229
+ }
230
+ documentation = {
231
+ status: "current",
232
+ planDigest,
233
+ documents: [...raw.documents] as string[],
234
+ };
235
+ }
236
+ let approval: AutoimplementInput["approval"];
237
+ if (input.approval !== undefined) {
238
+ const raw = requireRecord(input.approval, "autoimplement approval");
239
+ const maxReplans = raw.maxReplans ?? 3;
240
+ if (
241
+ !Number.isInteger(maxReplans) ||
242
+ (maxReplans as number) < 1 ||
243
+ (maxReplans as number) > 20
244
+ ) {
245
+ throw new Error("autoimplement approval maxReplans must be from 1 through 20");
246
+ }
247
+ approval = {
248
+ audience: requireString(raw.audience, "autoimplement approval audience"),
249
+ maxReplans: maxReplans as number,
250
+ };
251
+ }
97
252
  return {
98
253
  task: requireString(input.task, "autoimplement task"),
99
254
  ...(input.plan !== undefined ? { plan: input.plan } : {}),
@@ -106,6 +261,42 @@ function parseInput(value: unknown): AutoimplementInput {
106
261
  ? { baseBranch: requireString(input.baseBranch, "baseBranch") }
107
262
  : {}),
108
263
  merge: input.merge === true,
264
+ ...(documents !== undefined ? { documents: [...documents] as string[] } : {}),
265
+ ...(documentation !== undefined ? { documentation } : {}),
266
+ ...(approval !== undefined ? { approval } : {}),
267
+ };
268
+ }
269
+
270
+ function parseExistingPlan(value: unknown): ExistingPlanDiscovery {
271
+ const result = requireRecord(value, "existing plan discovery");
272
+ if (result.route !== "found" && result.route !== "blocked") {
273
+ throw new Error("existing plan discovery route must be found or blocked");
274
+ }
275
+ if (result.route === "found") {
276
+ if (result.plan === undefined) throw new Error("found plan must include plan");
277
+ if (
278
+ result.documentation !== "current" &&
279
+ result.documentation !== "missing" &&
280
+ result.documentation !== "stale"
281
+ ) {
282
+ throw new Error("found plan documentation must be current, missing, or stale");
283
+ }
284
+ }
285
+ if (
286
+ !Array.isArray(result.documents) ||
287
+ result.documents.some((item) => typeof item !== "string")
288
+ ) {
289
+ throw new Error("existing plan documents must be an array of strings");
290
+ }
291
+ return {
292
+ route: result.route,
293
+ ...(result.plan !== undefined ? { plan: result.plan } : {}),
294
+ ...(result.documentation !== undefined
295
+ ? { documentation: result.documentation as "current" | "missing" | "stale" }
296
+ : {}),
297
+ documents: [...result.documents] as string[],
298
+ reason: requireString(result.reason, "existing plan discovery reason"),
299
+ evidence: result.evidence ?? null,
109
300
  };
110
301
  }
111
302
 
@@ -210,11 +401,80 @@ function latestCiCommand(context: WorkflowNodeContext): StructuredCommand {
210
401
 
211
402
  function currentPlan(context: WorkflowNodeContext): unknown {
212
403
  const adopted = context.outputs.adoptPlan as { plan?: unknown } | undefined;
213
- return adopted?.plan ?? (context.input as AutoimplementInput).plan;
404
+ if (adopted?.plan !== undefined) return adopted.plan;
405
+ const documented = context.outputs.documentation as
406
+ | { exit?: string; output?: { plan?: unknown } }
407
+ | undefined;
408
+ if (documented?.exit === "ready" && documented.output?.plan !== undefined) {
409
+ return documented.output.plan;
410
+ }
411
+ const discovered = context.outputs.findPlan as ExistingPlanDiscovery | undefined;
412
+ if (discovered?.route === "found" && discovered.plan !== undefined) return discovered.plan;
413
+ return (context.input as AutoimplementInput).plan;
414
+ }
415
+
416
+ function currentPlanDigest(context: WorkflowNodeContext): string {
417
+ const adopted = context.outputs.adoptPlan as { planDigest?: unknown } | undefined;
418
+ if (typeof adopted?.planDigest === "string") return adopted.planDigest;
419
+ const documented = context.outputs.documentation as
420
+ | { exit?: string; output?: { planDigest?: unknown } }
421
+ | undefined;
422
+ if (documented?.exit === "ready" && typeof documented.output?.planDigest === "string") {
423
+ return documented.output.planDigest;
424
+ }
425
+ const plan = currentPlan(context);
426
+ if (plan === undefined) throw new Error("autoimplement does not have a selected plan");
427
+ return digest(plan);
428
+ }
429
+
430
+ function blockerChallenges(context: WorkflowNodeContext): BlockerChallenge[] {
431
+ return context.state.steps
432
+ .filter((step) => step.nodeId === "challengeBlocker" && step.outcome === "ok")
433
+ .map((step) => step.output as BlockerChallenge);
434
+ }
435
+
436
+ function latestBlockerClaim(context: WorkflowNodeContext): unknown {
437
+ const ids = [
438
+ "classifyImplementation",
439
+ "classifyVerification",
440
+ "repairReviewCommand",
441
+ "inspectComments",
442
+ "inspectCi",
443
+ "repairCiCommand",
444
+ "assessTrackedCi",
445
+ "classifyCi",
446
+ "finalizeDelivery",
447
+ ];
448
+ for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
449
+ const step = context.state.steps[index];
450
+ if (step && ids.includes(step.nodeId)) {
451
+ return { source: step.nodeId, result: step.output };
452
+ }
453
+ }
454
+ throw new Error("No model-generated blocker claim is available to challenge");
455
+ }
456
+
457
+ function recentWorkflowAttempts(context: WorkflowNodeContext): unknown[] {
458
+ return context.state.steps.slice(-12).map((step) => ({
459
+ nodeId: step.nodeId,
460
+ outcome: step.outcome,
461
+ output: step.output,
462
+ }));
214
463
  }
215
464
 
216
465
  function latestIssue(context: WorkflowNodeContext): unknown {
466
+ const approval = context.outputs.approval as
467
+ | { exit?: string; output?: { instructions?: unknown } }
468
+ | undefined;
469
+ if (approval?.exit === "replan" && typeof approval.output?.instructions === "string") {
470
+ return {
471
+ source: "human-replan",
472
+ instructions: approval.output.instructions,
473
+ priorPlanDigest: currentPlanDigest(context),
474
+ };
475
+ }
217
476
  const ids = [
477
+ "challengeBlocker",
218
478
  "classifyImplementation",
219
479
  "classifyVerification",
220
480
  "triageReview",
@@ -279,6 +539,8 @@ function reviewRounds(context: WorkflowNodeContext): ReviewAssessment[] {
279
539
 
280
540
  function latestBlockedReason(context: WorkflowNodeContext): { reason: string; evidence: unknown } {
281
541
  const candidates = [
542
+ "challengeBlockerGuard",
543
+ "challengeBlocker",
282
544
  "finalizeDelivery",
283
545
  "inspectCi",
284
546
  "assessTrackedCi",
@@ -287,7 +549,11 @@ function latestBlockedReason(context: WorkflowNodeContext): { reason: string; ev
287
549
  "classifyImplementation",
288
550
  "classifyVerification",
289
551
  "triageReview",
552
+ "replanGuard",
290
553
  "adoptPlan",
554
+ "findPlan",
555
+ "documentation",
556
+ "approval",
291
557
  ];
292
558
  for (let index = context.state.steps.length - 1; index >= 0; index -= 1) {
293
559
  const step = context.state.steps[index];
@@ -311,12 +577,48 @@ export const autoimplementWorkflow = defineWorkflow({
311
577
  presentationPrompt:
312
578
  "Summarize what was implemented, the review rounds by severity, the CI result, the PR or merge result, and any remaining limitation. Include exact validation commands.",
313
579
  startAt: "prepare",
314
- maxSteps: 160,
580
+ maxSteps: 240,
315
581
  includes: {
582
+ documentation: includeWorkflow(autodocWorkflow, {
583
+ input: (context): AutodocInput => {
584
+ const request = context.input as AutoimplementInput;
585
+ const discovery = context.outputs.findPlan as ExistingPlanDiscovery | undefined;
586
+ const plan = currentPlan(context);
587
+ if (plan === undefined) throw new Error("autoimplement documentation is missing a plan");
588
+ return {
589
+ task: request.task,
590
+ plan,
591
+ ...(request.repository !== undefined ? { repository: request.repository } : {}),
592
+ documents:
593
+ request.documents ?? request.documentation?.documents ?? discovery?.documents ?? [],
594
+ evidence: latestIssue(context),
595
+ };
596
+ },
597
+ }),
598
+ approval: includeWorkflow(planApprovalWorkflow, {
599
+ input: (context): PlanApprovalInput => {
600
+ const request = context.input as AutoimplementInput;
601
+ if (request.approval === undefined) {
602
+ throw new Error("autoimplement approval was entered without an approval policy");
603
+ }
604
+ const plan = currentPlan(context);
605
+ if (plan === undefined) throw new Error("autoimplement approval is missing a plan");
606
+ const revisions = context.state.steps.filter(
607
+ (step) => step.nodeId === "approval/approve",
608
+ ).length;
609
+ return {
610
+ task: request.task,
611
+ plan,
612
+ planDigest: currentPlanDigest(context),
613
+ audience: request.approval.audience,
614
+ revision: revisions + 1,
615
+ };
616
+ },
617
+ }),
316
618
  redesign: includeWorkflow({
317
- workflow: "autodevise",
318
- contract: autodeviseWorkflow,
319
- input: (context): AutodeviseInput => {
619
+ workflow: "autoplan",
620
+ contract: autoplanWorkflow,
621
+ input: (context): AutoplanInput => {
320
622
  const request = context.input as AutoimplementInput;
321
623
  return {
322
624
  problem: request.task,
@@ -340,25 +642,87 @@ export const autoimplementWorkflow = defineWorkflow({
340
642
  },
341
643
  nodes: {
342
644
  prepare: compute({
343
- run: ({ input }) => ({
344
- route: (input as AutoimplementInput).plan === undefined ? "redesign" : "implement",
345
- }),
645
+ run: ({ input }) => {
646
+ const request = input as AutoimplementInput;
647
+ return {
648
+ route:
649
+ request.plan === undefined
650
+ ? "find"
651
+ : request.documentation?.status === "current"
652
+ ? "ready"
653
+ : "document",
654
+ };
655
+ },
656
+ }),
657
+ findPlan: agent({
658
+ statusDetail: "finding existing plan",
659
+ prompt: ({ input }) => {
660
+ const request = input as AutoimplementInput;
661
+ return [
662
+ "Find the clear plan that has already been selected for this task.",
663
+ "Use the current conversation context and referenced canonical documents.",
664
+ "Do not devise, improve, replace, document, or implement a plan.",
665
+ "Return blocked when no single clear existing plan can be found.",
666
+ "Report whether its canonical documentation is current, missing, or stale.",
667
+ `Task: ${request.task}`,
668
+ `Repository: ${request.repository ?? "current repository"}`,
669
+ `Referenced documents: ${JSON.stringify(request.documents ?? [])}`,
670
+ ].join("\n");
671
+ },
672
+ expectedOutput:
673
+ '{ "route": "found" | "blocked", "plan": {} (required when found), "documentation": "current" | "missing" | "stale" (required when found), "documents": ["canonical file"], "reason": "reason", "evidence": "evidence" }',
674
+ validate: parseExistingPlan,
675
+ }),
676
+ routeFoundPlan: compute({
677
+ run: ({ outputs }) => {
678
+ const discovered = outputs.findPlan as ExistingPlanDiscovery;
679
+ if (discovered.route !== "found" || discovered.plan === undefined) {
680
+ return { route: "blocked", reason: discovered.reason, evidence: discovered.evidence };
681
+ }
682
+ return {
683
+ route: discovered.documentation === "current" ? "ready" : "document",
684
+ plan: discovered.plan,
685
+ reason: discovered.reason,
686
+ evidence: discovered.evidence,
687
+ };
688
+ },
346
689
  }),
347
690
  adoptPlan: compute({
348
691
  run: ({ outputs }) => {
349
- const result = includedResult(autodeviseWorkflow, outputs.redesign);
692
+ const result = includedResult(autoplanWorkflow, outputs.redesign);
350
693
  if (result.exit !== "ready") throw new Error("redesign did not return a ready plan");
351
694
  return {
352
- route: result.output.changed ? "implement" : "blocked",
695
+ route: result.output.changed ? "document" : "blocked",
353
696
  plan: result.output.plan,
354
697
  planDigest: result.output.planDigest,
355
698
  changed: result.output.changed,
356
699
  reason: result.output.changed
357
- ? "The plan changed in response to new evidence."
700
+ ? "The plan changed in response to new evidence and must be documented."
358
701
  : "Redesign returned the same plan for the same unresolved evidence.",
359
702
  };
360
703
  },
361
704
  }),
705
+ maybeApproval: compute({
706
+ run: ({ input }) => ({
707
+ route: (input as AutoimplementInput).approval === undefined ? "implement" : "approve",
708
+ }),
709
+ }),
710
+ replanGuard: compute({
711
+ run: (context) => {
712
+ const request = context.input as AutoimplementInput;
713
+ const limit = request.approval?.maxReplans ?? 3;
714
+ const replans = context.state.steps.filter(
715
+ (step) => step.nodeId === "approval/replan",
716
+ ).length;
717
+ return replans > limit
718
+ ? {
719
+ route: "blocked",
720
+ reason: `Plan approval reached the ${limit}-replan safety limit.`,
721
+ evidence: context.outputs.approval,
722
+ }
723
+ : { route: "redesign", replans, limit };
724
+ },
725
+ }),
362
726
  implement: agent({
363
727
  timeoutMs: 60 * 60_000,
364
728
  statusDetail: "implementing",
@@ -367,6 +731,8 @@ export const autoimplementWorkflow = defineWorkflow({
367
731
  return [
368
732
  `Implement this task end-to-end: ${request.task}`,
369
733
  `Plan: ${JSON.stringify(currentPlan(context))}`,
734
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
735
+ `Constraints: ${JSON.stringify(request.constraints ?? [])}`,
370
736
  "Follow repository instructions and use the most elegant long-term production-ready implementation without unnecessary work.",
371
737
  "If implementation exposes a new design or scope problem, report it precisely instead of forcing the old plan.",
372
738
  "Do not merge yet.",
@@ -394,6 +760,51 @@ export const autoimplementWorkflow = defineWorkflow({
394
760
  "implementation assessment",
395
761
  ),
396
762
  }),
763
+ challengeBlockerGuard: compute({
764
+ run: (context) => {
765
+ const challenges = blockerChallenges(context);
766
+ return challenges.length >= MAX_BLOCKER_CHALLENGES
767
+ ? {
768
+ route: "blocked",
769
+ reason: `Blocker challenge reached the ${MAX_BLOCKER_CHALLENGES}-attempt workflow safety limit.`,
770
+ evidence: { attempts: challenges.length, challenges },
771
+ }
772
+ : {
773
+ route: "challenge",
774
+ attempt: challenges.length + 1,
775
+ limit: MAX_BLOCKER_CHALLENGES,
776
+ };
777
+ },
778
+ }),
779
+ challengeBlocker: agent({
780
+ statusDetail: "challenging blocker claim",
781
+ prompt: (context) => {
782
+ const request = context.input as AutoimplementInput;
783
+ return [
784
+ "Independently challenge the latest claim that autoimplement is blocked.",
785
+ "Are you really blocked?",
786
+ "Is this really a blocker right now?",
787
+ "Can you find a safe way to move forward and finish this?",
788
+ "Are you getting stuck on something trivial, procedural, reversible, or already authorized?",
789
+ "Inspect the task, approved plan, current result, evidence, scope, authority, previous attempts, and viable alternatives.",
790
+ "Distinguish a true external blocker from ordinary rollout work, local implementation work, a design adjustment, a missing verification step, or a reversible operational task.",
791
+ "A local test failure, stale package, packaging or artifact mismatch, rollback preparation, or deployment procedure is not by itself outside authority.",
792
+ "If a safe deployment and rollback path is already authorized, a supported cutover is work to do, not a blocker.",
793
+ "Confirm blocked only when the issue blocks progress now, is outside authority, and has no safe practical path forward.",
794
+ "Return continue with the next practical action when work can proceed. Keep text concise, with at most five alternatives and five evidence items.",
795
+ `Task: ${request.task}`,
796
+ `Approved plan: ${JSON.stringify(currentPlan(context))}`,
797
+ `Current result and claimed blocker: ${JSON.stringify(latestBlockerClaim(context))}`,
798
+ `Authorized scope: ${request.scope ?? request.repository ?? "the current repository and task"}`,
799
+ `Constraints and authority: ${JSON.stringify(request.constraints ?? [])}`,
800
+ `Merge authorized: ${request.merge === true}`,
801
+ `Previous blocker challenges: ${JSON.stringify(blockerChallenges(context))}`,
802
+ `Recent workflow attempts: ${JSON.stringify(recentWorkflowAttempts(context))}`,
803
+ ].join("\n");
804
+ },
805
+ expectedOutput: `{ "route": "continue" | "blocked", "blockingNow": true | false, "outsideAuthority": true | false, "canProceed": true | false, "reason": "concise reason", "nextAction": "practical action or empty when blocked", "alternativesChecked": ["checked alternative"], "evidence": ["concrete evidence"] }`,
806
+ validate: parseBlockerChallenge,
807
+ }),
397
808
  verify: agent({
398
809
  timeoutMs: 45 * 60_000,
399
810
  statusDetail: "verifying",
@@ -728,28 +1139,73 @@ export const autoimplementWorkflow = defineWorkflow({
728
1139
  edges: [
729
1140
  {
730
1141
  from: "prepare",
731
- switch: { on: "$.route", cases: { redesign: "redesign", implement: "implement" } },
1142
+ switch: {
1143
+ on: "$.route",
1144
+ cases: { find: "findPlan", document: "documentation", ready: "maybeApproval" },
1145
+ },
1146
+ },
1147
+ {
1148
+ from: "findPlan",
1149
+ switch: { on: "$.route", cases: { found: "routeFoundPlan", blocked: "blocked" } },
1150
+ },
1151
+ {
1152
+ from: "routeFoundPlan",
1153
+ switch: {
1154
+ on: "$.route",
1155
+ cases: { ready: "maybeApproval", document: "documentation", blocked: "blocked" },
1156
+ },
732
1157
  },
733
1158
  { from: "redesign.ready", to: "adoptPlan" },
734
1159
  { from: "redesign.blocked", to: "blocked" },
735
1160
  {
736
1161
  from: "adoptPlan",
737
- switch: { on: "$.route", cases: { implement: "implement", blocked: "blocked" } },
1162
+ switch: { on: "$.route", cases: { document: "documentation", blocked: "blocked" } },
1163
+ },
1164
+ { from: "documentation.ready", to: "maybeApproval" },
1165
+ { from: "documentation.blocked", to: "blocked" },
1166
+ {
1167
+ from: "maybeApproval",
1168
+ switch: { on: "$.route", cases: { approve: "approval", implement: "implement" } },
1169
+ },
1170
+ { from: "approval.continue", to: "implement" },
1171
+ { from: "approval.stop", to: "blocked" },
1172
+ { from: "approval.replan", to: "replanGuard" },
1173
+ {
1174
+ from: "replanGuard",
1175
+ switch: { on: "$.route", cases: { redesign: "redesign", blocked: "blocked" } },
738
1176
  },
739
1177
  { from: "implement", to: "classifyImplementation" },
740
1178
  {
741
1179
  from: "classifyImplementation",
742
1180
  switch: {
743
1181
  on: "$.route",
744
- cases: { verify: "verify", redesign: "redesign", fix: "fix", blocked: "blocked" },
1182
+ cases: {
1183
+ verify: "verify",
1184
+ redesign: "redesign",
1185
+ fix: "fix",
1186
+ blocked: "challengeBlockerGuard",
1187
+ },
745
1188
  },
746
1189
  },
1190
+ {
1191
+ from: "challengeBlockerGuard",
1192
+ switch: { on: "$.route", cases: { challenge: "challengeBlocker", blocked: "blocked" } },
1193
+ },
1194
+ {
1195
+ from: "challengeBlocker",
1196
+ switch: { on: "$.route", cases: { continue: "redesign", blocked: "blocked" } },
1197
+ },
747
1198
  { from: "verify", to: "classifyVerification" },
748
1199
  {
749
1200
  from: "classifyVerification",
750
1201
  switch: {
751
1202
  on: "$.route",
752
- cases: { publish: "publish", redesign: "redesign", fix: "fix", blocked: "blocked" },
1203
+ cases: {
1204
+ publish: "publish",
1205
+ redesign: "redesign",
1206
+ fix: "fix",
1207
+ blocked: "challengeBlockerGuard",
1208
+ },
753
1209
  },
754
1210
  },
755
1211
  { from: "fix", to: "verify" },
@@ -768,7 +1224,10 @@ export const autoimplementWorkflow = defineWorkflow({
768
1224
  },
769
1225
  {
770
1226
  from: "repairReviewCommand",
771
- switch: { on: "$.route", cases: { retry: "runReview", blocked: "blocked" } },
1227
+ switch: {
1228
+ on: "$.route",
1229
+ cases: { retry: "runReview", blocked: "challengeBlockerGuard" },
1230
+ },
772
1231
  },
773
1232
  {
774
1233
  from: "assessReview",
@@ -795,7 +1254,12 @@ export const autoimplementWorkflow = defineWorkflow({
795
1254
  from: "inspectComments",
796
1255
  switch: {
797
1256
  on: "$.route",
798
- cases: { redesign: "redesign", fix: "fix", ci: "inspectCi", blocked: "blocked" },
1257
+ cases: {
1258
+ redesign: "redesign",
1259
+ fix: "fix",
1260
+ ci: "inspectCi",
1261
+ blocked: "challengeBlockerGuard",
1262
+ },
799
1263
  },
800
1264
  },
801
1265
  {
@@ -806,7 +1270,7 @@ export const autoimplementWorkflow = defineWorkflow({
806
1270
  green: "finalizeDelivery",
807
1271
  failed: "classifyCi",
808
1272
  pending: "trackCi",
809
- unavailable: "blocked",
1273
+ unavailable: "challengeBlockerGuard",
810
1274
  },
811
1275
  },
812
1276
  },
@@ -819,7 +1283,10 @@ export const autoimplementWorkflow = defineWorkflow({
819
1283
  },
820
1284
  {
821
1285
  from: "repairCiCommand",
822
- switch: { on: "$.route", cases: { retry: "trackCi", blocked: "blocked" } },
1286
+ switch: {
1287
+ on: "$.route",
1288
+ cases: { retry: "trackCi", blocked: "challengeBlockerGuard" },
1289
+ },
823
1290
  },
824
1291
  {
825
1292
  from: "assessTrackedCi",
@@ -829,7 +1296,7 @@ export const autoimplementWorkflow = defineWorkflow({
829
1296
  green: "finalizeDelivery",
830
1297
  failed: "classifyCi",
831
1298
  pending: "opportunisticTest",
832
- unavailable: "blocked",
1299
+ unavailable: "challengeBlockerGuard",
833
1300
  },
834
1301
  },
835
1302
  },
@@ -842,13 +1309,16 @@ export const autoimplementWorkflow = defineWorkflow({
842
1309
  redesign: "redesign",
843
1310
  fix: "fix",
844
1311
  unrelated: "finalizeDelivery",
845
- blocked: "blocked",
1312
+ blocked: "challengeBlockerGuard",
846
1313
  },
847
1314
  },
848
1315
  },
849
1316
  {
850
1317
  from: "finalizeDelivery",
851
- switch: { on: "$.status", cases: { completed: "finalize", blocked: "blocked" } },
1318
+ switch: {
1319
+ on: "$.status",
1320
+ cases: { completed: "finalize", blocked: "challengeBlockerGuard" },
1321
+ },
852
1322
  },
853
1323
  ],
854
1324
  });