@osolmaz/pi-workflows 0.11.2 → 0.12.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 (177) hide show
  1. package/README.md +15 -10
  2. package/dist/builtins/autoimplement-command-batches.d.ts +52 -0
  3. package/dist/builtins/autoimplement-command-batches.js +258 -0
  4. package/dist/builtins/autoimplement-command-batches.js.map +1 -0
  5. package/dist/builtins/autoimplement.workflow.d.ts +509 -124
  6. package/dist/builtins/autoimplement.workflow.js +884 -327
  7. package/dist/builtins/autoimplement.workflow.js.map +1 -1
  8. package/dist/builtins/catalog.js +5 -3
  9. package/dist/builtins/catalog.js.map +1 -1
  10. package/dist/builtins/index.d.ts +4 -1
  11. package/dist/builtins/index.js +3 -1
  12. package/dist/builtins/index.js.map +1 -1
  13. package/dist/builtins/monitor.workflow.d.ts +2 -4
  14. package/dist/builtins/monitor.workflow.js +26 -128
  15. package/dist/builtins/monitor.workflow.js.map +1 -1
  16. package/dist/builtins/pi-agent-group.d.ts +72 -0
  17. package/dist/builtins/pi-agent-group.js +1087 -0
  18. package/dist/builtins/pi-agent-group.js.map +1 -0
  19. package/dist/builtins/plan-approval.workflow.d.ts +39 -5
  20. package/dist/builtins/plan-approval.workflow.js +92 -14
  21. package/dist/builtins/plan-approval.workflow.js.map +1 -1
  22. package/dist/builtins/plan-change.workflow.d.ts +301 -0
  23. package/dist/builtins/plan-change.workflow.js +256 -0
  24. package/dist/builtins/plan-change.workflow.js.map +1 -0
  25. package/dist/builtins/plan-presentation.js +2 -2
  26. package/dist/builtins/plan-presentation.js.map +1 -1
  27. package/dist/builtins/sanity-check.workflow.d.ts +85 -0
  28. package/dist/builtins/sanity-check.workflow.js +482 -0
  29. package/dist/builtins/sanity-check.workflow.js.map +1 -0
  30. package/dist/controllers/sqlite.d.ts +90 -2
  31. package/dist/controllers/sqlite.js +380 -5
  32. package/dist/controllers/sqlite.js.map +1 -1
  33. package/dist/extension/decision-channels.d.ts +2 -2
  34. package/dist/extension/decision-channels.js +29 -35
  35. package/dist/extension/decision-channels.js.map +1 -1
  36. package/dist/extension/deferred-turn-coordinator.d.ts +32 -0
  37. package/dist/extension/deferred-turn-coordinator.js +143 -0
  38. package/dist/extension/deferred-turn-coordinator.js.map +1 -0
  39. package/dist/extension/deferred-turn.d.ts +44 -0
  40. package/dist/extension/deferred-turn.js +110 -0
  41. package/dist/extension/deferred-turn.js.map +1 -0
  42. package/dist/extension/index.js +348 -80
  43. package/dist/extension/index.js.map +1 -1
  44. package/dist/extension/session-events.d.ts +2 -2
  45. package/dist/extension/step-message.d.ts +1 -0
  46. package/dist/extension/step-message.js.map +1 -1
  47. package/dist/extension/widget.js +23 -3
  48. package/dist/extension/widget.js.map +1 -1
  49. package/dist/herdr/setup.js +4 -4
  50. package/dist/render/graph-render.js +1 -2
  51. package/dist/render/graph-render.js.map +1 -1
  52. package/dist/viewer/render.js +7 -6
  53. package/dist/viewer/render.js.map +1 -1
  54. package/dist/workflows/catalog.js +7 -2
  55. package/dist/workflows/catalog.js.map +1 -1
  56. package/dist/workflows/command-batch.d.ts +38 -0
  57. package/dist/workflows/command-batch.js +176 -0
  58. package/dist/workflows/command-batch.js.map +1 -0
  59. package/dist/workflows/composition.js +8 -0
  60. package/dist/workflows/composition.js.map +1 -1
  61. package/dist/workflows/decision-presentation.d.ts +1 -1
  62. package/dist/workflows/decision-presentation.js +51 -38
  63. package/dist/workflows/decision-presentation.js.map +1 -1
  64. package/dist/workflows/engine.d.ts +2 -2
  65. package/dist/workflows/engine.js +22 -17
  66. package/dist/workflows/engine.js.map +1 -1
  67. package/dist/workflows/errors.d.ts +13 -0
  68. package/dist/workflows/errors.js +15 -0
  69. package/dist/workflows/errors.js.map +1 -1
  70. package/dist/workflows/human-decision.d.ts +16 -4
  71. package/dist/workflows/human-decision.js +175 -72
  72. package/dist/workflows/human-decision.js.map +1 -1
  73. package/dist/workflows/index.d.ts +3 -2
  74. package/dist/workflows/index.js +2 -1
  75. package/dist/workflows/index.js.map +1 -1
  76. package/dist/workflows/progress.d.ts +1 -0
  77. package/dist/workflows/progress.js +15 -3
  78. package/dist/workflows/progress.js.map +1 -1
  79. package/dist/workflows/schema.js +12 -1
  80. package/dist/workflows/schema.js.map +1 -1
  81. package/dist/workflows/shell.d.ts +4 -0
  82. package/dist/workflows/shell.js +6 -0
  83. package/dist/workflows/shell.js.map +1 -1
  84. package/dist/workflows/store.js +8 -1
  85. package/dist/workflows/store.js.map +1 -1
  86. package/dist/workflows/types.d.ts +37 -48
  87. package/docs/2026-08-18-herdr-piw-plan.md +1 -1
  88. package/docs/2026-08-20-durable-workflow-launch-plan.md +19 -23
  89. package/docs/CONTROLLERS.md +2 -2
  90. package/docs/DEFERRED_TURNS.md +298 -0
  91. package/docs/DESIGN_PHILOSOPHY.md +1 -1
  92. package/docs/HUMAN_DECISIONS.md +30 -40
  93. package/docs/HUMAN_DECISION_PRESENTATIONS.md +17 -27
  94. package/docs/MONITOR.md +7 -13
  95. package/docs/WORKFLOW_COMPOSITION.md +10 -9
  96. package/docs/WORKFLOW_STEP_MESSAGES.md +5 -5
  97. package/docs/WORKFLOW_UPDATES.md +28 -5
  98. package/docs/plans/2026-08-04-controller-runtime-plan.md +1 -1
  99. package/docs/plans/2026-08-05-always-on-workflows-plan.md +1 -1
  100. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +9 -9
  101. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +1 -1
  102. package/docs/plans/2026-08-13-session-addressed-workflow-notifications-plan.md +1 -1
  103. package/docs/plans/2026-08-16-workflow-updates-plan.md +3 -3
  104. package/docs/plans/2026-08-17-bundled-skills-plan.md +6 -6
  105. package/docs/plans/2026-08-19-human-decision-gates-plan.md +5 -5
  106. package/docs/plans/2026-08-19-human-decision-presentations-plan.md +1 -1
  107. package/docs/plans/2026-08-19-provider-compatible-workflow-tool-schema-plan.md +1 -1
  108. package/docs/plans/2026-08-19-workflow-composition-plan.md +6 -6
  109. package/docs/plans/2026-08-20-autoimplement-blocker-challenge-plan.md +3 -3
  110. package/docs/plans/2026-08-20-bounded-command-batches-plan.md +206 -0
  111. package/docs/plans/2026-08-20-herdr-plugin-sync-plan.md +9 -9
  112. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +242 -0
  113. package/docs/plans/2026-08-21-deferred-turn-intents-plan.md +324 -0
  114. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +322 -0
  115. package/docs/plans/2026-08-21-sanity-check-plan.md +283 -0
  116. package/docs/run-bundles.md +8 -6
  117. package/docs/workflows.md +85 -22
  118. package/examples/workflows/approved-plan.workflow.ts +19 -46
  119. package/examples/workflows/command-batch.workflow.ts +50 -0
  120. package/examples/workflows/sanity-check.workflow.ts +1 -0
  121. package/herdr-plugin.toml +3 -3
  122. package/package.json +7 -7
  123. package/schemas/decision-presentation-v1.schema.json +1 -1
  124. package/schemas/human-decision-accepted-v1.schema.json +16 -4
  125. package/schemas/human-decision-answer-attempt-v1.schema.json +1 -1
  126. package/schemas/human-decision-cancellation-v1.schema.json +1 -1
  127. package/schemas/human-decision-continuation-v1.schema.json +11 -2
  128. package/schemas/human-decision-delivery-v1.schema.json +9 -1
  129. package/schemas/human-decision-receipt-v1.schema.json +9 -1
  130. package/schemas/human-decision-request-v1.schema.json +25 -5
  131. package/schemas/human-decision-resolution-v1.schema.json +1 -1
  132. package/schemas/human-decision-settlement-v1.schema.json +1 -1
  133. package/skills/autodoc/SKILL.md +33 -2
  134. package/skills/autoimplement/SKILL.md +93 -12
  135. package/skills/autoplan/SKILL.md +26 -2
  136. package/skills/monitor/SKILL.md +66 -24
  137. package/skills/pi-workflows/SKILL.md +21 -6
  138. package/skills/sanity-check/SKILL.md +44 -0
  139. package/src/builtins/autoimplement-command-batches.ts +364 -0
  140. package/src/builtins/autoimplement.workflow.ts +1075 -370
  141. package/src/builtins/catalog.ts +5 -3
  142. package/src/builtins/index.ts +24 -0
  143. package/src/builtins/monitor.workflow.ts +27 -150
  144. package/src/builtins/pi-agent-group.ts +1407 -0
  145. package/src/builtins/plan-approval.workflow.ts +157 -24
  146. package/src/builtins/plan-change.workflow.ts +321 -0
  147. package/src/builtins/plan-presentation.ts +2 -2
  148. package/src/builtins/sanity-check.workflow.ts +769 -0
  149. package/src/controllers/sqlite.ts +580 -7
  150. package/src/extension/decision-channels.ts +36 -66
  151. package/src/extension/deferred-turn-coordinator.ts +171 -0
  152. package/src/extension/deferred-turn.ts +166 -0
  153. package/src/extension/index.ts +438 -90
  154. package/src/extension/session-events.ts +2 -2
  155. package/src/extension/step-message.ts +1 -0
  156. package/src/extension/widget.ts +24 -5
  157. package/src/herdr/setup.ts +4 -4
  158. package/src/render/graph-render.ts +1 -2
  159. package/src/viewer/render.ts +7 -6
  160. package/src/workflows/catalog.ts +7 -2
  161. package/src/workflows/command-batch.ts +254 -0
  162. package/src/workflows/composition.ts +9 -0
  163. package/src/workflows/decision-presentation.ts +56 -43
  164. package/src/workflows/engine.ts +25 -20
  165. package/src/workflows/errors.ts +24 -0
  166. package/src/workflows/human-decision.ts +218 -101
  167. package/src/workflows/index.ts +20 -11
  168. package/src/workflows/progress.ts +18 -3
  169. package/src/workflows/schema.ts +19 -1
  170. package/src/workflows/shell.ts +10 -0
  171. package/src/workflows/store.ts +8 -1
  172. package/src/workflows/types.ts +43 -59
  173. package/schemas/human-decision-accepted-v2.schema.json +0 -50
  174. package/schemas/human-decision-delivery-v2.schema.json +0 -36
  175. package/schemas/human-decision-receipt-v2.schema.json +0 -39
  176. package/schemas/human-decision-request-v2.schema.json +0 -69
  177. package/schemas/human-decision-resolution-v2.schema.json +0 -27
@@ -52,9 +52,10 @@ export type WorkflowProgressData = {
52
52
  export type WorkflowNodeCommon = {
53
53
  /**
54
54
  * Per-node timeout or a callback that derives it from the run context.
55
- * Falls back to the engine default (15 minutes).
55
+ * Null disables the wall-clock deadline. Omission falls back to the engine
56
+ * default (15 minutes).
56
57
  */
57
- timeoutMs?: number | ((context: WorkflowNodeContext) => MaybePromise<number>);
58
+ timeoutMs?: number | null | ((context: WorkflowNodeContext) => MaybePromise<number | null>);
58
59
  /** Short human-readable label shown in the viewer while the node runs. */
59
60
  statusDetail?: string;
60
61
  };
@@ -182,31 +183,26 @@ export type DecisionPresentation = {
182
183
  summary: string;
183
184
  blocks: DecisionPresentationBlock[];
184
185
  };
185
- export type LegacyHumanDecisionPrompt = {
186
- title: string;
187
- body: unknown;
188
- subject?: never;
189
- presentation?: never;
190
- revision?: never;
191
- /** Optional absolute expiry. An expired request cannot accept an answer. */
192
- expiresAt?: string;
193
- };
194
- export type PresentedHumanDecisionPrompt<TSubject = unknown> = {
186
+ export type HumanDecisionPrompt<TSubject = unknown> = {
195
187
  title: string;
196
188
  subject: TSubject;
197
189
  presentation: DecisionPresentation;
198
- body?: never;
199
190
  /** Positive revision of the decision subject and presentation. Defaults to 1. */
200
191
  revision?: number;
201
192
  /** Optional absolute expiry. An expired request cannot accept an answer. */
202
193
  expiresAt?: string;
203
194
  };
204
- export type HumanDecisionPrompt = LegacyHumanDecisionPrompt | PresentedHumanDecisionPrompt;
205
195
  export type HumanDecisionAudience = string | ((context: WorkflowNodeContext) => MaybePromise<string>);
196
+ export type HumanDecisionTimeout = {
197
+ afterMs: number;
198
+ response: HumanDecisionResponse;
199
+ };
200
+ export type HumanDecisionTimeoutPolicy = HumanDecisionTimeout | ((context: WorkflowNodeContext) => MaybePromise<HumanDecisionTimeout | undefined>);
206
201
  export type HumanDecisionNodeContract = {
207
202
  audience: HumanDecisionAudience;
208
203
  choices: HumanDecisionChoiceMap;
209
204
  request: (context: WorkflowNodeContext) => MaybePromise<HumanDecisionPrompt>;
205
+ onTimeout?: HumanDecisionTimeoutPolicy;
210
206
  };
211
207
  type HumanDecisionRequestCommon = {
212
208
  decisionId: string;
@@ -220,23 +216,19 @@ type HumanDecisionRequestCommon = {
220
216
  choices: HumanDecisionChoiceMap;
221
217
  createdAt: string;
222
218
  expiresAt?: string;
219
+ defaultResponse?: HumanDecisionResponse;
223
220
  };
224
- export type HumanDecisionRequestV1 = HumanDecisionRequestCommon & {
221
+ export type HumanDecisionRequest = HumanDecisionRequestCommon & {
225
222
  schema: "pi-workflows.human-decision-request.v1";
226
- body: unknown;
227
- };
228
- export type HumanDecisionRequestV2 = HumanDecisionRequestCommon & {
229
- schema: "pi-workflows.human-decision-request.v2";
230
223
  subject: unknown;
231
224
  presentation: DecisionPresentation;
232
225
  revision: number;
233
226
  subjectDigest: string;
234
227
  presentationDigest: string;
235
228
  };
236
- export type HumanDecisionRequest = HumanDecisionRequestV1 | HumanDecisionRequestV2;
237
229
  /**
238
230
  * Complete operator-facing request passed to a decision channel. It excludes
239
- * the canonical subject and legacy body by design.
231
+ * the canonical subject by design.
240
232
  */
241
233
  export type HumanDecisionChannelRequest = HumanDecisionRequestCommon & {
242
234
  schema: "pi-workflows.human-decision-channel-request.v1";
@@ -260,60 +252,55 @@ export type HumanDecisionSubmission = HumanDecisionResponse & {
260
252
  source: HumanDecisionAnswerSource;
261
253
  idempotencyKey: string;
262
254
  };
263
- type AcceptedHumanDecisionCommon = {
255
+ type ResolvedHumanDecisionCommon = {
264
256
  decisionId: string;
265
257
  requestDigest: string;
266
258
  response: HumanDecisionResponse;
267
- source: HumanDecisionAnswerSource;
268
- idempotencyKey: string;
259
+ provenance: "human" | "timeout";
269
260
  acceptedAt: string;
270
261
  answerDigest: string;
271
262
  };
272
- export type AcceptedHumanDecisionV1 = AcceptedHumanDecisionCommon & {
263
+ type AcceptedHumanDecisionCommon = ResolvedHumanDecisionCommon & {
264
+ provenance: "human";
265
+ source: HumanDecisionAnswerSource;
266
+ idempotencyKey: string;
267
+ };
268
+ type DefaultedHumanDecisionCommon = ResolvedHumanDecisionCommon & {
269
+ provenance: "timeout";
270
+ };
271
+ export type AcceptedHumanDecision = AcceptedHumanDecisionCommon & {
273
272
  schema: "pi-workflows.human-decision-accepted.v1";
273
+ subjectDigest: string;
274
+ presentationDigest: string;
275
+ revision: number;
274
276
  };
275
- export type AcceptedHumanDecisionV2 = AcceptedHumanDecisionCommon & {
276
- schema: "pi-workflows.human-decision-accepted.v2";
277
+ export type DefaultedHumanDecision = DefaultedHumanDecisionCommon & {
278
+ schema: "pi-workflows.human-decision-accepted.v1";
277
279
  subjectDigest: string;
278
280
  presentationDigest: string;
279
281
  revision: number;
280
282
  };
281
- export type AcceptedHumanDecision = AcceptedHumanDecisionV1 | AcceptedHumanDecisionV2;
283
+ export type ResolvedHumanDecision = AcceptedHumanDecision | DefaultedHumanDecision;
282
284
  type HumanDecisionReceiptCommon = {
283
285
  decisionId: string;
284
286
  requestDigest: string;
285
287
  nodeId: string;
286
288
  response: HumanDecisionResponse;
289
+ provenance: "human" | "timeout";
287
290
  acceptedAt: string;
288
291
  answerDigest: string;
289
292
  };
290
- export type HumanDecisionReceiptV1 = HumanDecisionReceiptCommon & {
293
+ export type HumanDecisionReceipt = HumanDecisionReceiptCommon & {
291
294
  schema: "pi-workflows.human-decision-receipt.v1";
292
- };
293
- export type HumanDecisionReceiptV2 = HumanDecisionReceiptCommon & {
294
- schema: "pi-workflows.human-decision-receipt.v2";
295
295
  subjectDigest: string;
296
296
  presentationDigest: string;
297
297
  revision: number;
298
298
  };
299
- export type HumanDecisionReceipt = HumanDecisionReceiptV1 | HumanDecisionReceiptV2;
300
- export type HumanDecisionDeliveryRecordV1 = {
299
+ export type HumanDecisionDeliveryRecord = {
301
300
  schema: "pi-workflows.human-decision-delivery.v1";
302
301
  attemptId: string;
303
302
  decisionId: string;
304
303
  requestDigest: string;
305
- channel: string;
306
- state: "intent" | "confirmed" | "failed" | "unknown";
307
- createdAt: string;
308
- finishedAt?: string;
309
- messageCount?: number;
310
- errorCode?: string;
311
- };
312
- export type HumanDecisionDeliveryRecordV2 = {
313
- schema: "pi-workflows.human-decision-delivery.v2";
314
- attemptId: string;
315
- decisionId: string;
316
- requestDigest: string;
317
304
  presentationDigest: string;
318
305
  channel: string;
319
306
  phase: "intent" | "part" | "complete";
@@ -327,7 +314,6 @@ export type HumanDecisionDeliveryRecordV2 = {
327
314
  messageCount?: number;
328
315
  errorCode?: string;
329
316
  };
330
- export type HumanDecisionDeliveryRecord = HumanDecisionDeliveryRecordV1 | HumanDecisionDeliveryRecordV2;
331
317
  export type HumanDecisionSettlementRecord = {
332
318
  schema: "pi-workflows.human-decision-settlement.v1";
333
319
  attemptId: string;
@@ -350,6 +336,7 @@ export type HumanDecisionContinuationRecord = {
350
336
  schema: "pi-workflows.human-decision-continuation.v1";
351
337
  decisionId: string;
352
338
  requestDigest: string;
339
+ provenance: "human" | "timeout";
353
340
  parentRunId: string;
354
341
  runId: string;
355
342
  createdAt: string;
@@ -586,7 +573,7 @@ export type WorkflowRunState = {
586
573
  };
587
574
  export type WorkflowNodeSnapshot = {
588
575
  nodeType: WorkflowNodeDefinition["nodeType"];
589
- timeoutMs?: number;
576
+ timeoutMs?: number | null;
590
577
  statusDetail?: string;
591
578
  summary?: string;
592
579
  expectedOutput?: string;
@@ -598,6 +585,8 @@ export type WorkflowNodeSnapshot = {
598
585
  audience: string;
599
586
  dynamicAudience?: boolean;
600
587
  choices: HumanDecisionChoiceMap;
588
+ onTimeout?: HumanDecisionTimeout;
589
+ dynamicTimeout?: boolean;
601
590
  };
602
591
  };
603
592
  export type WorkflowDefinitionSnapshot = {
@@ -8,7 +8,7 @@ date: 2026-08-18
8
8
 
9
9
  ## Purpose
10
10
 
11
- When Pi Workflows runs inside Herdr, show a shortcut that opens the current run in `piw`. The integration must belong to Pi Workflows and ship as one package, repository, version, and release. The same repository must also qualify for Herdr's public plugin marketplace.
11
+ When pi-workflows runs inside Herdr, show a shortcut that opens the current run in `piw`. The integration must belong to pi-workflows and ship as one package, repository, version, and release. The same repository must also qualify for Herdr's public plugin marketplace.
12
12
 
13
13
  ## Requirements
14
14
 
@@ -6,17 +6,19 @@ date: 2026-08-20
6
6
 
7
7
  # Make deferred workflow launches durable
8
8
 
9
+ The launch queue and activation contract in this plan remain current. [Deferred workflow turns](DEFERRED_TURNS.md) replaces the launch-specific failure-notification mechanism with the general successor-turn intent contract.
10
+
9
11
  ## Goal
10
12
 
11
13
  A successful `workflow start` call must create a real queued workflow before it returns. The model
12
- must receive the final run ID, and Pi Workflows must start that run only after the current agent turn
14
+ must receive the final run ID, and pi-workflows must start that run only after the current agent turn
13
15
  settles.
14
16
 
15
- If startup then fails, Pi Workflows must save the failure and start one new model turn with an
17
+ If startup then fails, pi-workflows must save the failure and start one new model turn with an
16
18
  actionable error. The model can correct the request and call `workflow start` again. A failed launch
17
19
  must release the session reservation so the corrected run can start.
18
20
 
19
- This change stays inside Pi Workflows. It uses the existing project-scoped SQLite controller store
21
+ This change stays inside pi-workflows. It uses the existing project-scoped SQLite controller store
20
22
  and documented Pi extension APIs. It does not change Pi, add a service, or add another database.
21
23
 
22
24
  ## Current failure
@@ -39,7 +41,7 @@ the workflow had started.
39
41
 
40
42
  Use a durable prepared run and one model-visible failure follow-up.
41
43
 
42
- During the `workflow start` tool call, Pi Workflows will:
44
+ During the `workflow start` tool call, pi-workflows will:
43
45
 
44
46
  1. Resolve and validate the workflow and all available start conditions.
45
47
  2. Allocate the final run ID.
@@ -47,7 +49,7 @@ During the `workflow start` tool call, Pi Workflows will:
47
49
  4. Reserve the initiating Pi session.
48
50
  5. Return the run ID and say that the workflow is queued.
49
51
 
50
- After the current agent turn settles, Pi Workflows will:
52
+ After the current agent turn settles, pi-workflows will:
51
53
 
52
54
  1. Claim the queued record.
53
55
  2. Change it to `starting`.
@@ -55,22 +57,21 @@ After the current agent turn settles, Pi Workflows will:
55
57
  4. Change the record to `running`.
56
58
  5. Release the engine to run the first node.
57
59
 
58
- If startup fails, Pi Workflows will:
60
+ If startup fails, pi-workflows will:
59
61
 
60
62
  1. Change the record to `failed`.
61
63
  2. Save a bounded safe error.
62
64
  3. Release the session reservation.
63
- 4. Queue one durable failure notification for the owning Pi session.
64
- 5. Send that notification through `pi.sendMessage` with `triggerTurn: true` and
65
- `deliverAs: "followUp"`.
65
+ 4. Create one eligible deferred-turn intent for the owning Pi session.
66
+ 5. Resolve that intent with one factual follow-up after settlement.
66
67
 
67
68
  The new model turn will contain the failed run ID and an actionable error. The model can fix the
68
69
  workflow reference, input, source, or local condition and call `workflow start` again. Each retry is
69
- an explicit model action with a new run ID. Pi Workflows does not perform a blind automatic retry.
70
+ an explicit model action with a new run ID. pi-workflows does not perform a blind automatic retry.
70
71
 
71
72
  ## Alpha compatibility contract
72
73
 
73
- Pi Workflows is in alpha. Change the current storage and tool contracts in place.
74
+ pi-workflows is in alpha. Change the current storage and tool contracts in place.
74
75
 
75
76
  - Keep `pi-workflows.controller-store.v1`.
76
77
  - Keep existing run-bundle schema identifiers.
@@ -80,7 +81,7 @@ Pi Workflows is in alpha. Change the current storage and tool contracts in place
80
81
  - Change the SQLite table definitions and TypeScript types directly.
81
82
  - Remove the superseded status values and launch path in the same change.
82
83
 
83
- An existing controller store with the old alpha table layout is incompatible. On open, Pi Workflows
84
+ An existing controller store with the old alpha table layout is incompatible. On open, pi-workflows
84
85
  must verify the required table columns and status contract. If the layout is old, it must stop with a
85
86
  clear instruction to preserve any needed run evidence and reset the project-scoped controller store.
86
87
  It must not silently reinterpret or delete old state.
@@ -150,10 +151,7 @@ Inspect the error and call workflow start again only after you correct the cause
150
151
  The message does not contain raw workflow input, prompt text, credentials, request headers, or a
151
152
  stack trace.
152
153
 
153
- The failure notification has a deterministic ID derived from the run ID. Pi Workflows records its
154
- delivery in the existing notification outbox. Before a delivery retry, it checks the native Pi
155
- session for that notification ID. This prevents a duplicate after a crash between session append and
156
- outbox acknowledgement.
154
+ The turn intent has a deterministic ID derived from the session, run, and launch event. Pi Workflows records it in `workflow_turn_intents`. Before a delivery retry, it checks the Pi session branch for the intent ID. This prevents a duplicate after a crash between session append and intent resolution.
157
155
 
158
156
  A failed launch releases the one-workflow session reservation before it sends the follow-up. The
159
157
  model can therefore call `workflow start` during the new turn. If that launch also fails, the same
@@ -217,9 +215,7 @@ and `running` state its meaning directly.
217
215
  Clear private queue input when the launch becomes `running`, `failed`, or `cancelled`. The normal run
218
216
  bundle owns input after the engine starts.
219
217
 
220
- Extend the existing `workflow_notifications` table in place so a notification can be run-level.
221
- Add `launch_failure` to its kind contract and allow node and attempt identity to be absent for a
222
- run-level notification. Reuse its delivery claim, lease, and delivered timestamp.
218
+ Keep `workflow_notifications` limited to passive `progress` and `final` reports. A launch failure creates an eligible row in `workflow_turn_intents`; it does not add a run-level notification kind.
223
219
 
224
220
  The project-scoped controller store remains private local state. Tests must verify restrictive file
225
221
  and directory permissions.
@@ -384,8 +380,8 @@ Add or update these tests:
384
380
  10. Cancellation before claim, during `starting`, and before executor release.
385
381
  11. Source change between preparation and activation.
386
382
  12. Safe error redaction and byte bounds.
387
- 13. One durable launch-failure notification.
388
- 14. Crash before session append, after append, and before outbox acknowledgement.
383
+ 13. One durable launch-failure turn intent.
384
+ 14. Crash before session append, after append, and before intent resolution.
389
385
  15. Failed reservation release followed by a corrected model start.
390
386
  16. Repeated model correction attempts with one active reservation at a time.
391
387
  17. Status for every launch and run state.
@@ -411,7 +407,7 @@ or external service.
411
407
  - The start tool never reports success without a committed queued record and final run ID.
412
408
  - The model can correct synchronous start errors in the same turn.
413
409
  - A deferred startup failure always becomes durable before notification.
414
- - One failure notification starts one new model turn.
410
+ - One launch-failure intent starts one new model turn.
415
411
  - The model can correct the cause and start a new run.
416
412
  - Failed and cancelled launches release the session reservation.
417
413
  - No two runs activate for one reservation.
@@ -433,7 +429,7 @@ npx slophammer-ts@latest check . --only ts.dependency-boundaries-required
433
429
  git diff --check
434
430
  ```
435
431
 
436
- Run Pi Reviewer against the base branch and fix all valid P0 and P1 findings before release work.
432
+ Run pi-reviewer against the base branch and fix all valid P0 and P1 findings before release work.
437
433
 
438
434
  Package publication and OnurPi adoption are separate tasks. Do not edit an installed `node_modules`
439
435
  copy as the implementation source.
@@ -1,10 +1,10 @@
1
1
  # Controller runtime specification
2
2
 
3
- Pi Workflows runs finite TypeScript graphs. A graph starts with an input, follows explicit edges, and ends with a result or checkpoint. This works well for one bounded task.
3
+ pi-workflows runs finite TypeScript graphs. A graph starts with an input, follows explicit edges, and ends with a result or checkpoint. This works well for one bounded task.
4
4
 
5
5
  Long-running automation has a different job. It must keep comparing a requested state with the current state of another system. Events can arrive more than once, processes can stop between an external request and its local receipt, and the external state can change while work is running.
6
6
 
7
- This specification adds a Kubernetes-style controller runtime to Pi Workflows. The controller runtime sits beside the graph engine. Controllers manage durable resources, while workflows remain finite jobs that a controller can start and observe.
7
+ This specification adds a Kubernetes-style controller runtime to pi-workflows. The controller runtime sits beside the graph engine. Controllers manage durable resources, while workflows remain finite jobs that a controller can start and observe.
8
8
 
9
9
  The design follows the Kubernetes [controller pattern](https://kubernetes.io/docs/concepts/architecture/controller/), its [`spec` and `status` split](https://kubernetes.io/docs/concepts/overview/working-with-objects/), and the [idempotent reconciliation guidance](https://book.kubebuilder.io/reference/good-practices).
10
10
 
@@ -0,0 +1,298 @@
1
+ # Deferred workflow turns
2
+
3
+ This specification defines how Pi Workflows schedules one successor agent turn after a workflow event stops or strands the current turn. It covers cancellation, timeout, terminal failure, launch failure, controller interruption, and claim loss.
4
+
5
+ The implementation plan is [Guarantee one successor turn after workflow interruption](plans/2026-08-21-deferred-turn-intents-plan.md).
6
+
7
+ ## Terms
8
+
9
+ - **Source event:** The workflow event that creates the need for another turn.
10
+ - **Turn intent:** The durable obligation to send one successor turn.
11
+ - **Natural successor:** The next workflow agent prompt or result presentation.
12
+ - **Fallback:** A factual model-facing message sent when no natural successor remains.
13
+ - **Resolution:** The recorded fact that one message path satisfied the intent.
14
+ - **Target session:** The Pi session that owns the successor turn.
15
+
16
+ ## Core rule
17
+
18
+ Each eligible source event creates at most one turn intent. Exactly one of these message paths can resolve it:
19
+
20
+ 1. a workflow agent prompt;
21
+ 2. a completed or waiting result presentation;
22
+ 3. a factual fallback.
23
+
24
+ All three paths claim the same intent before sending. A resolved intent cannot start another turn.
25
+
26
+ Cancellation and process termination remain immediate. Pi Workflows does not keep the old assistant turn alive and does not wait for the successor before stopping active work.
27
+
28
+ ## Intent lifecycle
29
+
30
+ An intent has two stored states:
31
+
32
+ | State | Condition | Meaning |
33
+ | -------- | ------------------------------------------------------------- | --------------------------------------------------------- |
34
+ | Pending | `resolvedAt` is null | No message path has resolved the intent. |
35
+ | Resolved | `resolvedAt`, `resolution`, and `resolutionMessageId` are set | One message path was sent or found in the session branch. |
36
+
37
+ Pending intents have separate fallback eligibility:
38
+
39
+ | Eligibility | Condition | Meaning |
40
+ | ----------- | -------------------- | ---------------------------------------------------------------- |
41
+ | Ineligible | `eligibleAt` is null | A natural workflow prompt or presentation can still arrive. |
42
+ | Eligible | `eligibleAt` is set | Durable state shows that no immediate natural successor remains. |
43
+
44
+ A claim is temporary coordination state. A claim does not resolve an intent. An expired claim can be acquired again.
45
+
46
+ Valid resolutions are:
47
+
48
+ ```text
49
+ workflowPrompt | presentation | fallback
50
+ ```
51
+
52
+ Resolution records message delivery into the Pi session or the presence of the same message in the session branch. It does not prove that a model turn started or completed.
53
+
54
+ ## Source events
55
+
56
+ Valid causes are:
57
+
58
+ ```text
59
+ agentCancelled | timedOut | failed | launchFailed | controllerInterrupted | claimLost
60
+ ```
61
+
62
+ The event policy is:
63
+
64
+ | Event | Intent | Fallback rule |
65
+ | -------------------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- |
66
+ | Agent calls `workflow cancel` during an active workflow turn | Create before `ctx.abort()` when storage is available. | Make eligible after durable terminal cancellation. |
67
+ | Agent step times out | Create before `ctx.abort()` when storage is available. | Keep ineligible while a recovery prompt can arrive. Make eligible on terminal timeout. |
68
+ | Active workflow turn ends in terminal failure | Create or reuse the abort intent. | Make eligible after durable terminal failure. |
69
+ | Workflow reports started, then crashes before its first prompt | Create after durable failure. | Create as eligible. |
70
+ | Queued launch activation fails | Create after the queue row is durably failed. | Create as eligible. |
71
+ | Controller interrupts an active workflow turn | Create before the turn abort when possible. | Durable terminal or handoff state decides eligibility. |
72
+ | Active workflow turn loses its queue claim | Create before the turn abort when possible. | Keep ineligible while a new owner can continue. |
73
+ | Completed or waiting result has a presentation | Do not create a new intent. | Resolve an existing intent through presentation. |
74
+ | Direct `/workflow cancel` | Do not create. | No automatic model turn. |
75
+ | Workflow pause | Do not create. | No automatic model turn. |
76
+ | User Escape or held workflow | Do not create. | No automatic model turn. |
77
+ | Session shutdown | Do not create. | A closing session cannot start another turn. |
78
+
79
+ A terminal failure after a successful start tool result is eligible even when it did not first call `ctx.abort()`. This rule covers asynchronous runtime validation and startup failures that the user can see in the UI but the model cannot see in its current context.
80
+
81
+ ## Stable identity
82
+
83
+ `intentId` is a deterministic digest of:
84
+
85
+ - target session ID;
86
+ - run ID;
87
+ - source event ID;
88
+ - node ID or `$launch`;
89
+ - attempt ID when known;
90
+ - cause.
91
+
92
+ The same source event must always produce the same ID. Reusing an ID with different immutable facts is an error.
93
+
94
+ `sourceEventId` identifies the source transition independently of delivery retries. Terminal handling must reuse an earlier abort event instead of creating a second terminal event for the same interruption.
95
+
96
+ IDs use the existing controller-store key limit of 512 characters. The digest representation must be stable across processes and extension restarts.
97
+
98
+ ## Stored record
99
+
100
+ `workflow_turn_intents` lives in the existing controller SQLite database.
101
+
102
+ | Column | Null | Meaning |
103
+ | --------------------------- | ---- | -------------------------------------------- |
104
+ | `intent_id` | No | Deterministic primary key. |
105
+ | `source_event_id` | No | Stable source transition identity. |
106
+ | `run_id` | No | Related workflow run. |
107
+ | `workflow_ref` | No | Workflow identity used in messages. |
108
+ | `target_session_id` | No | Session that can receive the successor. |
109
+ | `cause` | No | Closed cause value. |
110
+ | `node_id` | Yes | Source node when known. |
111
+ | `attempt_id` | Yes | Source attempt when known. |
112
+ | `fallback_facts_json` | No | Bounded factual payload. |
113
+ | `requested_at` | No | Absolute ISO 8601 creation time. |
114
+ | `eligible_at` | Yes | Absolute ISO 8601 fallback eligibility time. |
115
+ | `resolved_at` | Yes | Absolute ISO 8601 resolution time. |
116
+ | `resolution` | Yes | Closed resolution value. |
117
+ | `resolution_message_id` | Yes | Stable message identity. |
118
+ | `delivery_claim_token` | Yes | Current claimant. |
119
+ | `delivery_claim_expires_at` | Yes | Claim expiry as epoch milliseconds. |
120
+
121
+ The table requires these indexes:
122
+
123
+ - unresolved intents by run and target session;
124
+ - unresolved eligible intents by target session, eligibility time, and intent ID.
125
+
126
+ A resolved row has all three resolution fields. A pending row has none of them. A row cannot change immutable identity or source fields after creation.
127
+
128
+ ## Fallback facts
129
+
130
+ `fallback_facts_json` uses this versioned camelCase object:
131
+
132
+ ```json
133
+ {
134
+ "schema": "pi-workflows.deferred-turn-facts.v1",
135
+ "workflowName": "autoimplement",
136
+ "runId": "20260821T081731Z-autoimplement-407480dd",
137
+ "observedState": "failed",
138
+ "cause": "failed",
139
+ "nodeId": "$launch",
140
+ "attemptId": null,
141
+ "reason": "scope must be a non-empty string",
142
+ "handoff": false
143
+ }
144
+ ```
145
+
146
+ Rules:
147
+
148
+ - `schema` is required and has the exact value shown above.
149
+ - `workflowName`, `runId`, `observedState`, and `cause` are required strings.
150
+ - `nodeId`, `attemptId`, and `reason` are strings or null.
151
+ - `handoff` is a required boolean.
152
+ - `reason` is safe diagnostic text with at most 8,192 characters.
153
+ - The serialized object is at most 64 KiB.
154
+ - Unknown fields are rejected during alpha.
155
+ - The object must not contain credentials, raw environment values, or unbounded command output.
156
+
157
+ `handoff: true` means another runner can continue. A handoff message must not describe the run as terminal unless separate durable state proves it.
158
+
159
+ ## Store operations
160
+
161
+ The controller store provides these internal operations:
162
+
163
+ - `ensureWorkflowTurnIntent`
164
+ - `getWorkflowTurnIntent`
165
+ - `claimWorkflowTurnIntentForRun`
166
+ - `claimEligibleWorkflowTurnIntentsForSession`
167
+ - `makeWorkflowTurnIntentEligible`
168
+ - `resolveWorkflowTurnIntent`
169
+ - `releaseWorkflowTurnIntentClaim`
170
+ - a bounded diagnostic list operation
171
+
172
+ `ensureWorkflowTurnIntent` is idempotent when all immutable fields match. It fails on an identity collision.
173
+
174
+ Claim operations use a caller-supplied token and lease duration. Resolution requires the matching live claim token. Conditional updates ensure that natural delivery and fallback cannot both resolve the same intent.
175
+
176
+ List operations require a positive bounded limit and deterministic ordering.
177
+
178
+ ## Abort ordering
179
+
180
+ For an eligible active-turn abort, Pi Workflows performs these steps in order:
181
+
182
+ 1. Record abort provenance on the active run.
183
+ 2. Build the source event and intent ID.
184
+ 3. Attempt to persist the intent.
185
+ 4. Record the intent ID in system-abort bookkeeping.
186
+ 5. Call `ctx.abort()`.
187
+
188
+ The persistence attempt is synchronous because cancellation immediately crosses the turn boundary. A store failure does not prevent `ctx.abort()`. Terminal handling retries the same intent ID. If retry also fails, Pi Workflows reports that it could not preserve the successor-turn guarantee.
189
+
190
+ ## Natural delivery
191
+
192
+ All extension-owned workflow prompts and result presentations pass through one `DeferredTurnCoordinator`.
193
+
194
+ If no intent exists, the coordinator preserves current message content and delivery options.
195
+
196
+ If a pending intent exists, the coordinator:
197
+
198
+ 1. waits until the old turn has settled or the session is verified idle;
199
+ 2. claims the intent for the run and session;
200
+ 3. adds `turnIntentId` to the message details;
201
+ 4. sends the normal prompt or presentation;
202
+ 5. resolves the intent with the matching resolution and message ID;
203
+ 6. releases the claim if sending fails.
204
+
205
+ A prompt generated during abort handling must not be sent into the aborting turn. The coordinator releases it after `agent_settled`.
206
+
207
+ ## Fallback delivery
208
+
209
+ Fallback synchronization runs:
210
+
211
+ - after `agent_settled` and system-abort cleanup;
212
+ - when the target session starts;
213
+ - during the existing periodic synchronization pass while the session is idle.
214
+
215
+ The fallback custom message uses this details object:
216
+
217
+ ```json
218
+ {
219
+ "schema": "pi-workflows.deferred-turn-message.v1",
220
+ "turnIntentId": "deferred-turn:...",
221
+ "runId": "20260821T081731Z-autoimplement-407480dd",
222
+ "cause": "failed"
223
+ }
224
+ ```
225
+
226
+ The message type is `pi-workflows-deferred-turn`. Delivery uses:
227
+
228
+ ```ts
229
+ {
230
+ deliverAs: "followUp",
231
+ triggerTurn: true,
232
+ }
233
+ ```
234
+
235
+ The visible content reports observed facts and asks the agent to inspect durable state before it decides whether an authorized correction is needed. It does not claim that recovery occurred, resume the old run, or retry work.
236
+
237
+ Fallback delivery is disabled during session shutdown and while a user-interrupted workflow is held.
238
+
239
+ ## Delivery recovery
240
+
241
+ Every resolving message includes the intent ID in its custom-message details.
242
+
243
+ Before sending, the coordinator scans the current session branch for that ID. If the message is already present, the coordinator resolves the intent without sending again. This repairs a crash or SQLite failure that occurs after `sendMessage` succeeds but before resolution is stored.
244
+
245
+ Store leases prevent concurrent processes from sending the same resolution. Branch identity handles the remaining send-before-resolution window.
246
+
247
+ ## Claim transfer
248
+
249
+ Claim loss stops the old runner's work and all fenced run-bundle writes. The intent stays pending and fallback-ineligible while another runner can resume the run.
250
+
251
+ The new runner's next workflow prompt can resolve the intent. If durable state later proves a terminal outcome with no natural successor, terminal handling makes the intent eligible for fallback to its target session.
252
+
253
+ Time alone does not prove terminal failure.
254
+
255
+ ## Launch notifications
256
+
257
+ Workflow-authored `progress` and `final` notifications remain passive. They do not trigger model turns.
258
+
259
+ The `launch_failure` notification kind is removed. Queued launch failure creates an eligible turn intent instead.
260
+
261
+ Pending `launch_failure` rows from the earlier alpha contract are incompatible. Pi Workflows must stop with a clear controller-store reset instruction. It must not reinterpret, migrate, or silently delete those rows.
262
+
263
+ ## Availability limits
264
+
265
+ Pi Workflows can guarantee only the facts under its control:
266
+
267
+ - the intent was stored;
268
+ - a claimant acquired it;
269
+ - a custom message was sent or found in the session branch;
270
+ - the local intent was resolved.
271
+
272
+ Pi Workflows cannot guarantee model start or completion with the current Pi API. It also cannot deliver after permanent loss of the process, target session, controller store, machine, or model provider.
273
+
274
+ Undelivered intents remain pending. The implementation does not add a service to process them outside a live Pi session.
275
+
276
+ ## Compatibility
277
+
278
+ This is an alpha hard cutover.
279
+
280
+ - Keep `pi-workflows.controller-store.v1`.
281
+ - Add `workflow_turn_intents` to the existing database.
282
+ - Remove the launch-trigger runtime path.
283
+ - Add no v2 schema, compatibility reader, dual write, alias, or feature flag.
284
+ - Keep historical terminal run bundles readable because their contract does not change.
285
+
286
+ ## Conformance
287
+
288
+ An implementation conforms when:
289
+
290
+ - one eligible source event produces at most one intent;
291
+ - one intent produces at most one successor message;
292
+ - an agent self-cancel receives one fallback after settlement;
293
+ - an asynchronous crash after a successful start result receives one fallback after settlement;
294
+ - a natural recovery prompt or presentation suppresses fallback by resolving the same intent;
295
+ - direct cancellation, pause, Escape, user hold, and shutdown produce no automatic turn;
296
+ - claim transfer permits natural resolution by the new owner and prevents stale writes;
297
+ - polling, restart, lease expiry, and send-before-resolution failure do not duplicate turns;
298
+ - passive workflow notifications keep their current behavior.
@@ -1,6 +1,6 @@
1
1
  # Design philosophy
2
2
 
3
- Pi Workflows should stay small and easy to combine. A small set of clear, general parts should support many kinds of work.
3
+ pi-workflows should stay small and easy to combine. A small set of clear, general parts should support many kinds of work.
4
4
 
5
5
  ## Primary rule
6
6