@osolmaz/pi-workflows 0.2.0 → 0.4.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 (152) hide show
  1. package/README.md +72 -5
  2. package/dist/builtins/catalog.d.ts +2 -0
  3. package/dist/builtins/catalog.js +22 -0
  4. package/dist/builtins/catalog.js.map +1 -0
  5. package/dist/builtins/monitor.workflow.d.ts +71 -0
  6. package/dist/builtins/monitor.workflow.js +250 -0
  7. package/dist/builtins/monitor.workflow.js.map +1 -0
  8. package/dist/controllers/conditions.d.ts +6 -0
  9. package/dist/controllers/conditions.js +68 -0
  10. package/dist/controllers/conditions.js.map +1 -0
  11. package/dist/controllers/definition.d.ts +6 -0
  12. package/dist/controllers/definition.js +45 -0
  13. package/dist/controllers/definition.js.map +1 -0
  14. package/dist/controllers/effects.d.ts +14 -0
  15. package/dist/controllers/effects.js +104 -0
  16. package/dist/controllers/effects.js.map +1 -0
  17. package/dist/controllers/errors.d.ts +12 -0
  18. package/dist/controllers/errors.js +25 -0
  19. package/dist/controllers/errors.js.map +1 -0
  20. package/dist/controllers/index.d.ts +13 -0
  21. package/dist/controllers/index.js +13 -0
  22. package/dist/controllers/index.js.map +1 -0
  23. package/dist/controllers/json.d.ts +5 -0
  24. package/dist/controllers/json.js +57 -0
  25. package/dist/controllers/json.js.map +1 -0
  26. package/dist/controllers/loader.d.ts +23 -0
  27. package/dist/controllers/loader.js +74 -0
  28. package/dist/controllers/loader.js.map +1 -0
  29. package/dist/controllers/manager.d.ts +58 -0
  30. package/dist/controllers/manager.js +399 -0
  31. package/dist/controllers/manager.js.map +1 -0
  32. package/dist/controllers/results.d.ts +5 -0
  33. package/dist/controllers/results.js +32 -0
  34. package/dist/controllers/results.js.map +1 -0
  35. package/dist/controllers/sqlite.d.ts +235 -0
  36. package/dist/controllers/sqlite.js +1087 -0
  37. package/dist/controllers/sqlite.js.map +1 -0
  38. package/dist/controllers/store.d.ts +112 -0
  39. package/dist/controllers/store.js +32 -0
  40. package/dist/controllers/store.js.map +1 -0
  41. package/dist/controllers/types.d.ts +159 -0
  42. package/dist/controllers/types.js +2 -0
  43. package/dist/controllers/types.js.map +1 -0
  44. package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
  45. package/dist/controllers/workflow-engine-scheduler.js +95 -0
  46. package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
  47. package/dist/controllers/workflows.d.ts +27 -0
  48. package/dist/controllers/workflows.js +109 -0
  49. package/dist/controllers/workflows.js.map +1 -0
  50. package/dist/extension/controller-host.d.ts +47 -0
  51. package/dist/extension/controller-host.js +110 -0
  52. package/dist/extension/controller-host.js.map +1 -0
  53. package/dist/extension/executor.d.ts +3 -0
  54. package/dist/extension/executor.js +12 -2
  55. package/dist/extension/executor.js.map +1 -1
  56. package/dist/extension/index.d.ts +7 -0
  57. package/dist/extension/index.js +1118 -106
  58. package/dist/extension/index.js.map +1 -1
  59. package/dist/extension/recorder.d.ts +6 -0
  60. package/dist/extension/recorder.js +20 -8
  61. package/dist/extension/recorder.js.map +1 -1
  62. package/dist/extension/workflow-tool.d.ts +28 -0
  63. package/dist/extension/workflow-tool.js +33 -0
  64. package/dist/extension/workflow-tool.js.map +1 -0
  65. package/dist/host/processes.d.ts +24 -0
  66. package/dist/host/processes.js +114 -0
  67. package/dist/host/processes.js.map +1 -0
  68. package/dist/host/rpc-bridge.d.ts +9 -0
  69. package/dist/host/rpc-bridge.js +39 -0
  70. package/dist/host/rpc-bridge.js.map +1 -0
  71. package/dist/host/rpc-executor.d.ts +38 -0
  72. package/dist/host/rpc-executor.js +254 -0
  73. package/dist/host/rpc-executor.js.map +1 -0
  74. package/dist/host/runner.d.ts +50 -0
  75. package/dist/host/runner.js +379 -0
  76. package/dist/host/runner.js.map +1 -0
  77. package/dist/viewer/cli.d.ts +7 -3
  78. package/dist/viewer/cli.js +150 -19
  79. package/dist/viewer/cli.js.map +1 -1
  80. package/dist/workflows/catalog.d.ts +43 -0
  81. package/dist/workflows/catalog.js +79 -0
  82. package/dist/workflows/catalog.js.map +1 -0
  83. package/dist/workflows/engine.d.ts +37 -2
  84. package/dist/workflows/engine.js +297 -29
  85. package/dist/workflows/engine.js.map +1 -1
  86. package/dist/workflows/errors.d.ts +23 -0
  87. package/dist/workflows/errors.js +38 -0
  88. package/dist/workflows/errors.js.map +1 -1
  89. package/dist/workflows/graph.js +0 -5
  90. package/dist/workflows/graph.js.map +1 -1
  91. package/dist/workflows/index.d.ts +2 -2
  92. package/dist/workflows/index.js +1 -1
  93. package/dist/workflows/index.js.map +1 -1
  94. package/dist/workflows/loader.d.ts +19 -15
  95. package/dist/workflows/loader.js +63 -19
  96. package/dist/workflows/loader.js.map +1 -1
  97. package/dist/workflows/migrate-sources.d.ts +41 -0
  98. package/dist/workflows/migrate-sources.js +129 -0
  99. package/dist/workflows/migrate-sources.js.map +1 -0
  100. package/dist/workflows/schema.js +3 -2
  101. package/dist/workflows/schema.js.map +1 -1
  102. package/dist/workflows/store.d.ts +50 -6
  103. package/dist/workflows/store.js +448 -53
  104. package/dist/workflows/store.js.map +1 -1
  105. package/dist/workflows/types.d.ts +27 -3
  106. package/docs/CONTROLLERS.md +215 -0
  107. package/docs/development.md +14 -9
  108. package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
  109. package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
  110. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
  111. package/docs/plans/2026-08-12-coordinated-workflow-timeouts-plan.md +74 -0
  112. package/docs/plans/2026-08-13-built-in-workflow-catalog-plan.md +97 -0
  113. package/docs/run-bundles.md +85 -28
  114. package/docs/workflows.md +161 -17
  115. package/examples/controllers/pull-request.controller.ts +215 -0
  116. package/package.json +10 -2
  117. package/src/builtins/catalog.ts +22 -0
  118. package/src/builtins/monitor.workflow.ts +302 -0
  119. package/src/controllers/conditions.ts +110 -0
  120. package/src/controllers/definition.ts +65 -0
  121. package/src/controllers/effects.ts +123 -0
  122. package/src/controllers/errors.ts +27 -0
  123. package/src/controllers/index.ts +90 -0
  124. package/src/controllers/json.ts +62 -0
  125. package/src/controllers/loader.ts +104 -0
  126. package/src/controllers/manager.ts +533 -0
  127. package/src/controllers/results.ts +46 -0
  128. package/src/controllers/sqlite.ts +1545 -0
  129. package/src/controllers/store.ts +160 -0
  130. package/src/controllers/types.ts +183 -0
  131. package/src/controllers/workflow-engine-scheduler.ts +148 -0
  132. package/src/controllers/workflows.ts +152 -0
  133. package/src/extension/controller-host.ts +163 -0
  134. package/src/extension/executor.ts +13 -2
  135. package/src/extension/index.ts +1325 -130
  136. package/src/extension/recorder.ts +65 -36
  137. package/src/extension/workflow-tool.ts +59 -0
  138. package/src/host/processes.ts +119 -0
  139. package/src/host/rpc-bridge.ts +44 -0
  140. package/src/host/rpc-executor.ts +299 -0
  141. package/src/host/runner.ts +445 -0
  142. package/src/viewer/cli.ts +167 -21
  143. package/src/workflows/catalog.ts +135 -0
  144. package/src/workflows/engine.ts +404 -41
  145. package/src/workflows/errors.ts +45 -0
  146. package/src/workflows/graph.ts +0 -5
  147. package/src/workflows/index.ts +2 -0
  148. package/src/workflows/loader.ts +76 -22
  149. package/src/workflows/migrate-sources.ts +167 -0
  150. package/src/workflows/schema.ts +3 -2
  151. package/src/workflows/store.ts +557 -48
  152. package/src/workflows/types.ts +22 -3
@@ -77,9 +77,9 @@ Consequences for readers:
77
77
 
78
78
  Large payloads are stored once, content-addressed, under `artifacts/` and
79
79
  referenced from the documents that use them. This applies uniformly to every
80
- **persisted value position**: `input`, `outputs.*`, `results.*.output`,
81
- `steps[*].prompt`, `steps[*].output`, `finalOutput`, and trace event payload
82
- values.
80
+ **persisted value position**. These positions include `input`, `outputs.*`,
81
+ `results.*.output`, `steps[*].prompt`, `steps[*].output`, and `finalOutput`.
82
+ Trace event payload values follow the same rule.
83
83
 
84
84
  Encoding rule, applied recursively to a persisted value:
85
85
 
@@ -109,9 +109,9 @@ Artifact rules:
109
109
  - Artifacts are immutable once written and deduplicate by content hash.
110
110
  - Readers must tolerate unknown `mediaType` values.
111
111
 
112
- Because the same output can legitimately appear in `outputs`, `results`,
113
- `steps`, and the trace, externalization makes that duplication cheap: each
114
- copy is the same small reference.
112
+ The same output can legitimately appear in several places. Externalization
113
+ keeps each copy in `outputs`, `results`, `steps`, or the trace as the same small
114
+ reference.
115
115
 
116
116
  ## manifest.json
117
117
 
@@ -123,7 +123,11 @@ Identity and pointers, kept in sync with the state on every snapshot:
123
123
  "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
124
124
  "workflowName": "autoimplement",
125
125
  "runTitle": "autoimplement: fix the flaky test",
126
- "workflowPath": "/repo/.pi/workflows/autoimplement.workflow.ts",
126
+ "workflowSource": {
127
+ "kind": "file",
128
+ "path": "/repo/.pi/workflows/autoimplement.workflow.ts",
129
+ "hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
130
+ },
127
131
  "startedAt": "2026-07-29T02:39:12.412Z",
128
132
  "finishedAt": "2026-07-29T02:41:03.977Z",
129
133
  "status": "completed",
@@ -138,6 +142,11 @@ Identity and pointers, kept in sync with the state on every snapshot:
138
142
  }
139
143
  ```
140
144
 
145
+ `workflowSource` identifies the definition used by the run. User workflow
146
+ files use an absolute path and SHA-256 hash. Package-provided workflows use a
147
+ stable identity such as `{ "kind": "builtin", "id": "monitor", "revision": "1" }`.
148
+ A built-in identity does not contain an installation path.
149
+
141
150
  `paths.artifacts` is declared from bundle creation so a live session-event
142
151
  patch can safely reference a newly written artifact before the next workflow
143
152
  state projection. The directory itself is created only when needed.
@@ -154,6 +163,30 @@ validators are not serialized. Each node keeps only its metadata (`nodeType`,
154
163
  and edges are copied verbatim. The snapshot is what lets viewers draw all
155
164
  nodes, including ones that have not run yet. It is immutable after run start.
156
165
 
166
+ ## Resume and repair
167
+
168
+ An interrupted run (status `running` with no terminal trace event) can resume
169
+ instead of failing. Resume is a named operation with strict rules:
170
+
171
+ 1. The caller must hold the run's queue claim. Only the current claim holder
172
+ may resume or interrupt a bundle, and every bundle write verifies the
173
+ claim token first (write fencing).
174
+ 2. A torn trace tail (a crash mid-append) is truncated to the last complete
175
+ line. Trace events the state projection never recorded are dropped, so
176
+ `state.traceSeq` and the trace agree again before any new event.
177
+ 3. Completed nodes replay from the projection. The in-flight node reruns with
178
+ a fresh attempt; a `run_resumed` trace event marks the boundary.
179
+ 4. `state.workflowSource` pins the workflow source from run start. File
180
+ sources require the same hash. Built-in sources require the same catalog
181
+ id and revision. Resume refuses a mismatch unless forced, and a forced
182
+ resume records the mismatch in the `run_resumed` payload.
183
+
184
+ Continuation runs (answering a checkpoint) are new bundles, not resumed ones.
185
+ They link back through `state.parentRunId`, carry the parent's outputs,
186
+ results, and step records forward, and note `continuedFrom` in their
187
+ `run_started` payload. Bundles stay append-only; a continuation is the only
188
+ way work follows a terminal `waiting` state.
189
+
157
190
  ## state.json
158
191
 
159
192
  The full run projection (`WorkflowRunState` in
@@ -166,6 +199,11 @@ The full run projection (`WorkflowRunState` in
166
199
  "traceSeq": 17,
167
200
  "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
168
201
  "workflowName": "autoimplement",
202
+ "workflowSource": {
203
+ "kind": "file",
204
+ "path": "/repo/.pi/workflows/autoimplement.workflow.ts",
205
+ "hash": "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
206
+ },
169
207
  "startedAt": "…",
170
208
  "updatedAt": "…",
171
209
  "status": "running",
@@ -176,11 +214,20 @@ The full run projection (`WorkflowRunState` in
176
214
  }
177
215
  ```
178
216
 
217
+ - `workflowSource` is the canonical source identity. Resuming a file requires
218
+ the same hash. Resuming a built-in requires the same catalog revision. A
219
+ mismatch refuses the resume instead of loading another definition.
179
220
  - `status` is one of `running`, `waiting`, `completed`, `failed`, `timed_out`,
180
- or `cancelled`.
181
- - While a node is executing, `currentNode`, `currentAttemptId`,
182
- `currentNodeStartedAt`, and `statusDetail` describe it; they disappear when
183
- the node finishes. The executing node's type comes from the definition
221
+ or `cancelled`. A controller host records an abandoned bundle as `failed`
222
+ with a final `run_interrupted` trace event. Before doing that, recovery checks
223
+ the trace tail and repairs a stale projection when the terminal event was
224
+ already appended. The controller store can then retry a genuinely abandoned
225
+ child attempt without changing this schema. If startup stopped before a
226
+ manifest existed, the scheduler preserves that incomplete directory as a
227
+ hidden sibling before creating the reserved run.
228
+ - While a node is executing, `currentNode` and `currentAttemptId` identify it.
229
+ `currentNodeStartedAt` and `statusDetail` add timing and display context.
230
+ These fields disappear when the node finishes. The executing node's type comes from the definition
184
231
  snapshot, not from the state.
185
232
  - While a pause request holds the run at a step boundary, `paused` is `true`
186
233
  (with matching `run_paused`/`run_resumed` trace events); it disappears when
@@ -218,10 +265,9 @@ The full run projection (`WorkflowRunState` in
218
265
  produced by this attempt, from prompt delivery through accepted submission.
219
266
  Viewers must use this explicit linkage and never infer it heuristically.
220
267
  - Action steps carry an `action` receipt with `actionType`
221
- (`shell`/`function`) and, for shell actions, `command`, `args`, `cwd`,
222
- `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the
223
- step output (the parsed or raw shell result) and are externalized when
224
- large.
268
+ (`shell`/`function`). Shell actions also record `command`, `args`, `cwd`,
269
+ `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the step
270
+ output (the parsed or raw shell result) and are externalized when large.
225
271
  - When a run pauses at a checkpoint, `waitingOn` names the checkpoint node.
226
272
  Terminal runs carry `finalOutput` on success and `error` on failure.
227
273
 
@@ -248,8 +294,8 @@ One event per line, monotonically sequenced per run, schema
248
294
  ignore unknown event types and unknown payload fields so new ones can be added
249
295
  within the same schema version.
250
296
 
251
- The trace alone is sufficient to reconstruct the run: outputs and receipts are
252
- part of the terminal node events, not only of `state.json`.
297
+ The trace alone is sufficient to reconstruct the run because terminal node
298
+ events carry outputs and receipts.
253
299
 
254
300
  Event catalog and payload contracts:
255
301
 
@@ -268,6 +314,7 @@ Event catalog and payload contracts:
268
314
  | `run_failed` | run | `status`, `error` |
269
315
  | `run_timed_out` | run | `status`, `error` |
270
316
  | `run_cancelled` | run | `status`, `error?` |
317
+ | `run_interrupted` | run | `error` |
271
318
 
272
319
  Invariants:
273
320
 
@@ -283,6 +330,15 @@ Present when the run executed inside a Pi conversation. The extension records
283
330
  the conversation into the bundle so replay never depends on Pi's global
284
331
  session store.
285
332
 
333
+ A run that outlives its first session (parked, then resumed by another
334
+ session or the host) gains a second capture under
335
+ `session/segments/<attemptId>/` with the same file layout (`binding.json`,
336
+ `entries.ndjson`, `events.ndjson`, `capture.json`). The first capture stays
337
+ flat at `session/`; only captures from the second bind onward become
338
+ segments, so readers that predate segments keep working on single-session
339
+ bundles. An interrupted run finalizes any segment still `recording` as
340
+ `failed` with the interruption reason.
341
+
286
342
  ### binding.json
287
343
 
288
344
  Written once when the run binds to the conversation
@@ -325,22 +381,23 @@ while the run was active, schema `pi-workflows.session-entry.v1`:
325
381
  - `entry` is the verbatim Pi session entry (Pi's own versioned format),
326
382
  including user messages, assistant messages, tool results, model changes,
327
383
  and compaction entries. Nothing is normalized or rewritten.
328
- - Entries include everything that happened in the conversation during the run:
329
- workflow prompts, nudges, and user interruptions are all part of the record.
384
+ - Entries include everything that happened in the conversation during the run.
385
+ This includes workflow prompts and nudges together with user interruptions.
330
386
  - `conversation` ranges in step records and `node_finished` events address
331
387
  entries by Pi entry id (`entry.id`).
332
388
 
333
389
  ### events.ndjson
334
390
 
335
- The temporal journal records documented Pi `turn_*`, `message_*`, and
391
+ The temporal journal records documented Pi `turn_*` and `message_*` hooks plus
336
392
  `tool_execution_*` hooks with schema `pi-workflows.session-event.v1`. Each
337
- record has a per-file `seq`, timestamp, `nodeId`, `attemptId`, optional
338
- `turnId`, `messageId`, and `toolCallId`, a normalized `type`, and `payload`.
393
+ record has a per-file `seq`, timestamp, `nodeId`, and `attemptId`. Optional
394
+ turn, message, and tool call IDs link related records. A normalized `type` and
395
+ `payload` carry the event data.
339
396
  The full contract and event catalog are in
340
397
  [session-event-journal.md](session-event-journal.md).
341
398
 
342
- Events preserve semantic deltas. Assistant `partial`, terminal `message`, and
343
- terminal `error` snapshots are never stored. Tool update records omit Pi's
399
+ Events preserve semantic deltas. Assistant `partial` snapshots are never
400
+ stored, and neither are terminal `message` or `error` snapshots. Tool update records omit Pi's
344
401
  cumulative `partialResult`. Final `message_finished` records link to settled
345
402
  Pi entries with `entryId`; after that linkage, `entries.ndjson` is the
346
403
  verbatim content authority.
@@ -364,10 +421,10 @@ complete lines, sequence gaps, and terminal torn tails are integrity failures.
364
421
  }
365
422
  ```
366
423
 
367
- `status` is `recording`, `complete`, or `failed`. Failed capture adds
368
- `failure` with `failedAt`, `code`, and `message`. Capture failure is visible to
369
- readers but does not fail the workflow. Terminal readers verify the counts,
370
- last sequence, schemas, and contiguous event sequence. Missing temporal files
424
+ `status` starts as `recording` and ends as `complete` or `failed`. Failed
425
+ capture adds `failure` with `failedAt` plus a code and message. Capture failure
426
+ is visible to readers but does not fail the workflow. Terminal readers verify
427
+ the counts and last sequence, then check schemas and contiguous event order. Missing temporal files
371
428
  in a session-bound bundle are invalid, not an older supported layout.
372
429
 
373
430
  ## Versioning
package/docs/workflows.md CHANGED
@@ -9,10 +9,21 @@ model sees, and how runs behave at runtime. For the on-disk run format, see
9
9
 
10
10
  A workflow is a TypeScript module whose default export is `defineWorkflow(...)`.
11
11
  Files are discovered by suffix (`.workflow.ts`, `.workflow.js`, `.workflow.mts`,
12
- `.workflow.mjs`) from two directories, in precedence order:
12
+ `.workflow.mjs`) from these sources, in precedence order:
13
13
 
14
14
  1. `.pi/workflows/` in the project (highest precedence on name collisions)
15
15
  2. `~/.pi/agent/workflows/` globally
16
+ 3. Workflows built into Pi Workflows
17
+
18
+ Pi Workflows includes a built-in `monitor` workflow. A project or global file
19
+ named `monitor.workflow.ts` replaces it. The package registers each built-in in
20
+ a process-local catalog with a stable reference such as `builtin:monitor` and
21
+ an explicit revision. Built-ins are imported with the engine when a Pi process
22
+ starts. They are not read from the package directory when a run starts or
23
+ resumes. Updating the package on disk cannot mix a new built-in with that
24
+ process's old engine; reload or restart Pi to use the new built-in. A revision
25
+ mismatch refuses resume. Project and global workflow files still reload on
26
+ each run and use their path and SHA-256 hash as their source identity.
16
27
 
17
28
  The workflow's command name is the file stem, so `.pi/workflows/triage.workflow.ts`
18
29
  runs as `/workflow triage`. A direct path also works: `/workflow ./somewhere/x.workflow.ts`.
@@ -44,7 +55,7 @@ Top-level fields:
44
55
 
45
56
  | Field | Type | Notes |
46
57
  | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
47
- | `name` | `string` | Required. Used in run ids and the step contract. `cancel`, `list`, `pause`, and `resume` are reserved for `/workflow` subcommands. |
58
+ | `name` | `string` | Required. Used in run ids and the step contract. `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are reserved for `/workflow` subcommands. |
48
59
  | `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
49
60
  | `presentationPrompt` | `string` or function | Optional instructions for a normal assistant response after the run. A function receives `{ state, finalOutput, signal }` and may return a prompt or `undefined`. See [Result presentation](#result-presentation). |
50
61
  | `startAt` | `string` | Required. Id of the first node. |
@@ -79,6 +90,38 @@ Long-running compute, action, and checkpoint callbacks should observe
79
90
  steps). When the node times out or the run is cancelled, the engine stops
80
91
  waiting immediately, but only cooperative callbacks stop doing work.
81
92
 
93
+ ## Durable runs, parking, and resume
94
+
95
+ Every interactive `/workflow` run is tracked in the project run queue (see
96
+ [CONTROLLERS.md](CONTROLLERS.md) for the store). The session that starts a run
97
+ claims it and owns it while it executes; every bundle write proves the claim
98
+ first (write fencing).
99
+
100
+ Closing the Pi session mid-run no longer cancels the run. The engine **parks**:
101
+ it stops without a terminal event, releases the claim, and leaves the bundle
102
+ resumable. When a runner is available again (a reopened Pi session or the
103
+ standalone host), the run **resumes** at the node it stopped on. Completed
104
+ nodes replay from the recorded state; only the interrupted node and everything
105
+ downstream rerun. Resume repairs a torn trace tail, drops trace events the
106
+ state projection never recorded, and refuses to continue if the workflow
107
+ source changed since the run started (a forced resume records the mismatch).
108
+
109
+ The standalone host runs without any Pi session:
110
+
111
+ ```bash
112
+ pi-workflows host --project /path/to/project
113
+ ```
114
+
115
+ The host claims parked runs, resumes them, and reconciles durable controllers.
116
+ Conversation nodes execute in headless `pi --mode rpc` children that load a
117
+ small bridge extension; the model sees the same `workflow` tool contract as an
118
+ in-session run. The host is a foreground process: start it in a terminal and
119
+ stop it with Ctrl-C. A second host for the same project refuses to start, and
120
+ a host that dies has its orphaned children reaped by the next one. While the
121
+ host works, any open Pi session stays current: a per-session watermark over
122
+ the shared run event feed produces catch-up summaries and quiet context
123
+ updates.
124
+
82
125
  ## Node types
83
126
 
84
127
  ### agent
@@ -91,7 +134,10 @@ agent({
91
134
  prompt: ({ outputs }) => `Review this: ${JSON.stringify(outputs.implement)}`,
92
135
  expectedOutput: `{ "verdict": "clean" | "issues_found" }`,
93
136
  validate: (output) => output, // optional; throw to reject the submission
94
- timeoutMs: 30 * 60_000, // optional; default 15 minutes
137
+ timeoutMs: ({ input }) =>
138
+ (input as { timeoutMinutes?: number }).timeoutMinutes
139
+ ? (input as { timeoutMinutes: number }).timeoutMinutes * 60_000
140
+ : 30 * 60_000, // optional number or context callback; default 15 minutes
95
141
  statusDetail: "reviewing", // optional; shown in widget and viewer
96
142
  });
97
143
  ```
@@ -101,7 +147,15 @@ calls the tool, the output passes through normalization (a JSON string is
101
147
  parsed tolerantly) and then `validate`. If `validate` throws, the tool call
102
148
  returns an error and the model can retry within the same step. If the agent
103
149
  ends its turn without submitting, the extension nudges it, twice by default,
104
- then fails the step.
150
+ then fails the step. If an agent node times out or the workflow is cancelled,
151
+ the extension also aborts its active Pi turn. The model cannot continue to use
152
+ tools after the engine has closed that attempt.
153
+
154
+ `timeoutMs` can be a finite positive number or a function of the normal node
155
+ context. A timeout function can use prepared outputs to select a deadline for
156
+ this run. It has 30 seconds to return a value. Computed timeout functions are
157
+ runtime code, so definition snapshots omit them; fixed numeric timeouts remain
158
+ in the snapshot.
105
159
 
106
160
  ### compute
107
161
 
@@ -147,10 +201,15 @@ command fails.
147
201
 
148
202
  ### checkpoint
149
203
 
150
- Ends the run in a `waiting` state for human review. Runs after a checkpoint do
151
- not resume automatically; the checkpoint output is the run's final output.
152
- Because nothing resumes past a checkpoint, graph validation rejects outgoing
153
- edges from checkpoint nodes.
204
+ Ends the run in a `waiting` state for human review. The checkpoint bundle is
205
+ terminal, so no process keeps running while the run waits. The human answers
206
+ with `/workflow answer <json>` (or plain text), which starts a **continuation
207
+ run**: a new run with its own bundle and trace, linked to the checkpointed run
208
+ through `parentRunId`. The continuation receives the answer as its input,
209
+ carries forward every output the parent produced (including the checkpoint's),
210
+ and continues routing along the checkpoint's outgoing edge. Outgoing edges
211
+ from checkpoint nodes are allowed exactly so continuations have somewhere to
212
+ go; step accounting carries over, so `maxSteps` bounds the whole chain.
154
213
 
155
214
  ```typescript
156
215
  checkpoint({
@@ -207,6 +266,60 @@ A missing case for the resolved value fails the run with a routing error. A
207
266
  node with no outgoing edge (or no matching failure route) ends the run:
208
267
  `completed` on success, `failed`/`timed_out`/`cancelled` otherwise.
209
268
 
269
+ ## Model workflow control
270
+
271
+ The model sees one `workflow` tool. Its `action` field supports:
272
+
273
+ - `list` for discovered workflow names and sources.
274
+ - `start` with a workflow name or path and structured input.
275
+ - `status` for the active run or a supplied run ID.
276
+ - `pause`, `resume`, and `cancel` for the active run.
277
+ - `answer` with checkpoint input and an optional run ID.
278
+ - `submit` for the current workflow step contract.
279
+
280
+ A model-started run is queued until the model's current turn settles. The first
281
+ workflow prompt then starts a new turn. This keeps the requesting turn outside
282
+ the workflow's first attempt and prevents an early missing-submission reminder.
283
+ The normal extension offers all actions. The headless RPC bridge offers only
284
+ `submit`, so a workflow child cannot recursively control other runs.
285
+
286
+ ### Built-in monitor
287
+
288
+ The built-in `monitor` workflow turns a plain request for repeated checks into
289
+ one looping workflow run. Its input is:
290
+
291
+ ```json
292
+ {
293
+ "task": "Check pull request 123",
294
+ "everyMinutes": 30,
295
+ "reportWhen": "Checks fail or the state changes materially",
296
+ "stopWhen": "The pull request is merged or closed",
297
+ "maxChecks": 1000,
298
+ "checkTimeoutMinutes": 60
299
+ }
300
+ ```
301
+
302
+ The first check runs immediately. Each accepted check records a bounded current
303
+ observation and chooses whether to continue, report, or stop. A report uses a
304
+ separate agent node so its structured check result is validated before the user
305
+ sees the message. The next check can read the previous accepted observation.
306
+
307
+ Intervals must be whole minutes from 1 through 1,440. `maxChecks` defaults to
308
+ 1,000 and cannot exceed 1,000. `checkTimeoutMinutes` is optional and applies to
309
+ check and report agent nodes. It must be from 5 through 1,440 minutes. Its
310
+ default is the larger of 60 minutes and `everyMinutes`. The workflow also has
311
+ a finite step limit and bounded observation and report sizes.
312
+
313
+ The interval uses the existing shell action to launch the current Node
314
+ executable with a timer. This works on every platform supported by Pi. The node
315
+ and command timeouts are higher than the maximum interval. Cancelling the
316
+ workflow aborts the timer process immediately. If the owning Pi process or
317
+ standalone host stops during the wait, normal parking rules abort the shell node
318
+ and resume later by running that wait again from the beginning.
319
+
320
+ A monitor uses the session's single active workflow slot. It does not provide
321
+ cron syntax, calendar scheduling, OS notifications, or a background service.
322
+
210
323
  ## The step contract
211
324
 
212
325
  Every `agent` prompt ends with a step contract block naming the workflow, the
@@ -217,16 +330,17 @@ step id, the attempt id, and the expected output shape:
217
330
  Workflow step contract (workflow: autoimplement, step: review, attempt: 6f9d…)
218
331
 
219
332
  Complete this step by calling the `workflow` tool exactly once with:
220
- {"step": "review", "attempt": "6f9d…", "output": <your result>}
333
+ {"action": "submit", "step": "review", "attempt": "6f9d…", "output": <your result>}
221
334
  Expected output: { "route": "clean" | "issues_found", "reason": "short justification" }
222
335
  The step is complete only after the workflow tool accepts the output.
223
336
  If the tool reports a validation error, correct the output and call it again.
224
337
  ```
225
338
 
226
- The `workflow` tool takes `{ step, attempt, output }`. Submissions are
227
- rejected (with a reason the model sees) when no step is pending, the step id
228
- is wrong, the attempt id belongs to an earlier attempt of the same node (loops
229
- revisit node ids, so each attempt gets a fresh id), or `validate` throws.
339
+ The `workflow` tool uses `{ action: "submit", step, attempt, output }` for step
340
+ results. Submissions are rejected (with a reason the model sees) when no step
341
+ is pending, the step id is wrong, the attempt id belongs to an earlier attempt
342
+ of the same node (loops revisit node ids, so each attempt gets a fresh id), or
343
+ `validate` throws.
230
344
  Acceptance resolves the step and the engine advances; the next agent prompt
231
345
  arrives as a new user message in the same conversation.
232
346
 
@@ -251,8 +365,9 @@ After the final run state has been persisted, the Pi extension sends the
251
365
  presentation instructions and bounded final result to the model as a hidden
252
366
  follow-up message. The next visible message is a normal assistant response.
253
367
  Returning `undefined`, returning an empty string, or omitting
254
- `presentationPrompt` produces no follow-up. Cancelled runs are never
255
- presented. Async prompt builders have 30 seconds to finish and receive an
368
+ `presentationPrompt` produces no follow-up. Failed, timed-out, and cancelled
369
+ runs are never presented; the extension reports their persisted status and
370
+ error directly. Async prompt builders have 30 seconds to finish and receive an
256
371
  `AbortSignal` that fires on timeout, session shutdown, or when a new workflow
257
372
  or normal user turn starts; stale presentations are discarded. Once a presentation message has
258
373
  been queued, another workflow cannot start until that assistant response
@@ -271,8 +386,10 @@ Runs execute one node at a time. Every transition is persisted to the run
271
386
  bundle before the engine moves on, which is what makes the live viewer
272
387
  possible. Defaults worth knowing:
273
388
 
274
- - Node timeout is 15 minutes unless the node sets `timeoutMs`. A timed-out
275
- node has outcome `timed_out` and can be routed with `$result.outcome`.
389
+ - Node timeout is 15 minutes unless the node sets `timeoutMs` to a positive
390
+ number or context callback. A timed-out node has outcome `timed_out` and can
391
+ be routed with `$result.outcome`. A timed-out agent node also aborts its Pi
392
+ turn, and late output for that attempt is rejected.
276
393
  - `maxSteps` (workflow-level, default 100) bounds loops built from cycles in
277
394
  the graph.
278
395
  - `/workflow pause` requests a pause: the current step finishes normally,
@@ -290,6 +407,33 @@ possible. Defaults worth knowing:
290
407
  - Agent nudges: if the model ends its turn without submitting the pending
291
408
  step, it gets a reminder, twice by default, then the step fails.
292
409
 
410
+ ## Workflows started by controllers
411
+
412
+ A controller can start a workflow as a finite child job with `ctx.workflows.ensure()`. The request key is stable across reconciliation passes, and the input fingerprint prevents one key from being reused for different work.
413
+
414
+ ```typescript
415
+ const run = await ctx.workflows.ensure({
416
+ requestKey: `repair:${resource.metadata.generation}`,
417
+ workflow: "repair-pull-request",
418
+ input: { repository: resource.spec.repository, number: resource.spec.number },
419
+ });
420
+
421
+ if (run.state !== "succeeded") {
422
+ return ctx.requeueAfter(5_000, {
423
+ workflowRun: {
424
+ requestId: run.requestId,
425
+ ...(run.runId ? { runId: run.runId } : {}),
426
+ state: run.state,
427
+ attempt: run.attempt,
428
+ },
429
+ });
430
+ }
431
+ ```
432
+
433
+ Child workflow completion queues the parent resource again. A running child left by a stopped host is recorded as a failed run bundle with a `run_interrupted` event. The controller treats that child attempt as interrupted, and the next parent reconciliation starts another immutable attempt. Consequential external mutations should use the controller effect API so uncertain results are observed before retry.
434
+
435
+ See [CONTROLLERS.md](CONTROLLERS.md) for controller definitions and the full recovery contract.
436
+
293
437
  ## Using the engine outside pi
294
438
 
295
439
  The engine is pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`, so