@osolmaz/pi-workflows 0.1.0 → 0.3.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 (171) hide show
  1. package/README.md +107 -26
  2. package/dist/builtins/monitor.workflow.d.ts +71 -0
  3. package/dist/builtins/monitor.workflow.js +234 -0
  4. package/dist/builtins/monitor.workflow.js.map +1 -0
  5. package/dist/controllers/conditions.d.ts +6 -0
  6. package/dist/controllers/conditions.js +68 -0
  7. package/dist/controllers/conditions.js.map +1 -0
  8. package/dist/controllers/definition.d.ts +6 -0
  9. package/dist/controllers/definition.js +45 -0
  10. package/dist/controllers/definition.js.map +1 -0
  11. package/dist/controllers/effects.d.ts +14 -0
  12. package/dist/controllers/effects.js +104 -0
  13. package/dist/controllers/effects.js.map +1 -0
  14. package/dist/controllers/errors.d.ts +12 -0
  15. package/dist/controllers/errors.js +25 -0
  16. package/dist/controllers/errors.js.map +1 -0
  17. package/dist/controllers/index.d.ts +13 -0
  18. package/dist/controllers/index.js +13 -0
  19. package/dist/controllers/index.js.map +1 -0
  20. package/dist/controllers/json.d.ts +5 -0
  21. package/dist/controllers/json.js +57 -0
  22. package/dist/controllers/json.js.map +1 -0
  23. package/dist/controllers/loader.d.ts +23 -0
  24. package/dist/controllers/loader.js +74 -0
  25. package/dist/controllers/loader.js.map +1 -0
  26. package/dist/controllers/manager.d.ts +58 -0
  27. package/dist/controllers/manager.js +399 -0
  28. package/dist/controllers/manager.js.map +1 -0
  29. package/dist/controllers/results.d.ts +5 -0
  30. package/dist/controllers/results.js +32 -0
  31. package/dist/controllers/results.js.map +1 -0
  32. package/dist/controllers/sqlite.d.ts +212 -0
  33. package/dist/controllers/sqlite.js +1009 -0
  34. package/dist/controllers/sqlite.js.map +1 -0
  35. package/dist/controllers/store.d.ts +112 -0
  36. package/dist/controllers/store.js +32 -0
  37. package/dist/controllers/store.js.map +1 -0
  38. package/dist/controllers/types.d.ts +159 -0
  39. package/dist/controllers/types.js +2 -0
  40. package/dist/controllers/types.js.map +1 -0
  41. package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
  42. package/dist/controllers/workflow-engine-scheduler.js +93 -0
  43. package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
  44. package/dist/controllers/workflows.d.ts +27 -0
  45. package/dist/controllers/workflows.js +109 -0
  46. package/dist/controllers/workflows.js.map +1 -0
  47. package/dist/extension/controller-host.d.ts +47 -0
  48. package/dist/extension/controller-host.js +110 -0
  49. package/dist/extension/controller-host.js.map +1 -0
  50. package/dist/extension/executor.d.ts +14 -1
  51. package/dist/extension/executor.js +12 -2
  52. package/dist/extension/executor.js.map +1 -1
  53. package/dist/extension/index.d.ts +7 -0
  54. package/dist/extension/index.js +1118 -89
  55. package/dist/extension/index.js.map +1 -1
  56. package/dist/extension/recorder.d.ts +91 -0
  57. package/dist/extension/recorder.js +537 -0
  58. package/dist/extension/recorder.js.map +1 -0
  59. package/dist/extension/session-events.d.ts +134 -0
  60. package/dist/extension/session-events.js +60 -0
  61. package/dist/extension/session-events.js.map +1 -0
  62. package/dist/extension/widget.js +25 -24
  63. package/dist/extension/widget.js.map +1 -1
  64. package/dist/extension/workflow-tool.d.ts +28 -0
  65. package/dist/extension/workflow-tool.js +33 -0
  66. package/dist/extension/workflow-tool.js.map +1 -0
  67. package/dist/host/processes.d.ts +24 -0
  68. package/dist/host/processes.js +114 -0
  69. package/dist/host/processes.js.map +1 -0
  70. package/dist/host/rpc-bridge.d.ts +9 -0
  71. package/dist/host/rpc-bridge.js +39 -0
  72. package/dist/host/rpc-bridge.js.map +1 -0
  73. package/dist/host/rpc-executor.d.ts +38 -0
  74. package/dist/host/rpc-executor.js +254 -0
  75. package/dist/host/rpc-executor.js.map +1 -0
  76. package/dist/host/runner.d.ts +49 -0
  77. package/dist/host/runner.js +350 -0
  78. package/dist/host/runner.js.map +1 -0
  79. package/dist/render/canvas.d.ts +1 -1
  80. package/dist/render/canvas.js +5 -0
  81. package/dist/render/canvas.js.map +1 -1
  82. package/dist/render/graph-render.d.ts +5 -0
  83. package/dist/render/graph-render.js +211 -48
  84. package/dist/render/graph-render.js.map +1 -1
  85. package/dist/viewer/cli.d.ts +7 -3
  86. package/dist/viewer/cli.js +150 -19
  87. package/dist/viewer/cli.js.map +1 -1
  88. package/dist/viewer/render.js +19 -3
  89. package/dist/viewer/render.js.map +1 -1
  90. package/dist/viewer/session-reducer.d.ts +45 -0
  91. package/dist/viewer/session-reducer.js +266 -0
  92. package/dist/viewer/session-reducer.js.map +1 -0
  93. package/dist/workflows/artifacts.d.ts +40 -0
  94. package/dist/workflows/artifacts.js +155 -0
  95. package/dist/workflows/artifacts.js.map +1 -0
  96. package/dist/workflows/engine.d.ts +38 -0
  97. package/dist/workflows/engine.js +281 -20
  98. package/dist/workflows/engine.js.map +1 -1
  99. package/dist/workflows/errors.d.ts +23 -0
  100. package/dist/workflows/errors.js +38 -0
  101. package/dist/workflows/errors.js.map +1 -1
  102. package/dist/workflows/graph.js +0 -5
  103. package/dist/workflows/graph.js.map +1 -1
  104. package/dist/workflows/index.d.ts +3 -2
  105. package/dist/workflows/index.js +2 -1
  106. package/dist/workflows/index.js.map +1 -1
  107. package/dist/workflows/loader.d.ts +5 -3
  108. package/dist/workflows/loader.js +10 -1
  109. package/dist/workflows/loader.js.map +1 -1
  110. package/dist/workflows/schema.js +1 -1
  111. package/dist/workflows/schema.js.map +1 -1
  112. package/dist/workflows/store.d.ts +98 -10
  113. package/dist/workflows/store.js +921 -46
  114. package/dist/workflows/store.js.map +1 -1
  115. package/dist/workflows/types.d.ts +136 -3
  116. package/docs/CONTROLLERS.md +215 -0
  117. package/docs/development.md +51 -24
  118. package/docs/live-replay-protocol.md +155 -0
  119. package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
  120. package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
  121. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
  122. package/docs/plans/piw-viewer-experience-implementation-plan.md +674 -0
  123. package/docs/plans/replayable-run-bundles-implementation-plan.md +65 -0
  124. package/docs/plans/session-event-replay-implementation-plan.md +494 -0
  125. package/docs/plans/tui-viewer-implementation-plan.md +64 -0
  126. package/docs/run-bundles.md +359 -55
  127. package/docs/session-event-journal.md +470 -0
  128. package/docs/tui-viewer.md +218 -0
  129. package/docs/workflows.md +131 -11
  130. package/examples/controllers/pull-request.controller.ts +215 -0
  131. package/package.json +11 -2
  132. package/src/builtins/monitor.workflow.ts +278 -0
  133. package/src/controllers/conditions.ts +110 -0
  134. package/src/controllers/definition.ts +65 -0
  135. package/src/controllers/effects.ts +123 -0
  136. package/src/controllers/errors.ts +27 -0
  137. package/src/controllers/index.ts +90 -0
  138. package/src/controllers/json.ts +62 -0
  139. package/src/controllers/loader.ts +104 -0
  140. package/src/controllers/manager.ts +533 -0
  141. package/src/controllers/results.ts +46 -0
  142. package/src/controllers/sqlite.ts +1427 -0
  143. package/src/controllers/store.ts +160 -0
  144. package/src/controllers/types.ts +183 -0
  145. package/src/controllers/workflow-engine-scheduler.ts +145 -0
  146. package/src/controllers/workflows.ts +152 -0
  147. package/src/extension/controller-host.ts +163 -0
  148. package/src/extension/executor.ts +29 -2
  149. package/src/extension/index.ts +1315 -109
  150. package/src/extension/recorder.ts +662 -0
  151. package/src/extension/session-events.ts +119 -0
  152. package/src/extension/widget.ts +26 -24
  153. package/src/extension/workflow-tool.ts +59 -0
  154. package/src/host/processes.ts +119 -0
  155. package/src/host/rpc-bridge.ts +44 -0
  156. package/src/host/rpc-executor.ts +299 -0
  157. package/src/host/runner.ts +406 -0
  158. package/src/render/canvas.ts +19 -1
  159. package/src/render/graph-render.ts +277 -44
  160. package/src/viewer/cli.ts +167 -21
  161. package/src/viewer/render.ts +21 -3
  162. package/src/viewer/session-reducer.ts +347 -0
  163. package/src/workflows/artifacts.ts +188 -0
  164. package/src/workflows/engine.ts +365 -19
  165. package/src/workflows/errors.ts +45 -0
  166. package/src/workflows/graph.ts +0 -5
  167. package/src/workflows/index.ts +15 -0
  168. package/src/workflows/loader.ts +13 -3
  169. package/src/workflows/schema.ts +1 -1
  170. package/src/workflows/store.ts +1157 -48
  171. package/src/workflows/types.ts +151 -3
@@ -1,40 +1,117 @@
1
1
  # Run bundle format
2
2
 
3
3
  Every workflow run persists to its own directory, called a run bundle. The
4
- bundle is the contract between the engine and anything that observes runs,
5
- including the bundled terminal viewer. This document specifies the format so
6
- other tools can consume it.
4
+ bundle is the contract between the engine and anything that observes runs: the
5
+ bundled terminal viewer, the Rust TUI, and any external tool. A bundle is
6
+ **self-contained for replay**: a reader never needs access to Pi's global
7
+ session store or any other file outside the bundle directory.
8
+
9
+ This document is the authoritative specification. There is exactly one format
10
+ version; older layouts are not read and no compatibility paths exist.
7
11
 
8
12
  ## Location and layout
9
13
 
10
14
  Bundles live under `~/.pi/agent/workflows/runs/` by default. The
11
15
  `PI_WORKFLOWS_RUNS_DIR` environment variable overrides the location for both
12
- the engine and the viewer, which is how the test suite keeps runs inside
16
+ the engine and all viewers, which is how the test suite keeps runs inside
13
17
  temporary directories.
14
18
 
15
19
  ```
16
20
  ~/.pi/agent/workflows/runs/
17
- 20260719T023912Z-autoimplement-3f2a9c1b/
21
+ 20260729T023912Z-autoimplement-3f2a9c1b/
18
22
  manifest.json # pi-workflows.run-bundle.v1
19
23
  workflow.json # pi-workflows.definition-snapshot.v1
20
- state.json # full run projection
21
- trace.ndjson # pi-workflows.trace-event.v1, append-only
24
+ state.json # pi-workflows.run-state.v1, derived projection
25
+ trace.ndjson # pi-workflows.trace-event.v1, append-only source of truth
26
+ session/ # present when the run executed inside a Pi conversation
27
+ binding.json # pi-workflows.session-binding.v1
28
+ entries.ndjson # pi-workflows.session-entry.v1, append-only
29
+ events.ndjson # pi-workflows.session-event.v1, append-only
30
+ capture.json # pi-workflows.session-capture.v1, atomic projection
31
+ artifacts/ # present when any persisted value was externalized
32
+ sha256-<64 hex>.txt
22
33
  ```
23
34
 
24
35
  Run ids are `<UTC timestamp>-<workflow slug>-<8 hex chars>`, so lexical order
25
36
  is chronological order.
26
37
 
27
- ## Write discipline
38
+ Bundle directories are created with mode `0700` and files with mode `0600`.
39
+ Bundles can contain prompts, model output, shell commands, environment
40
+ details, and absolute paths; treat them as private data and review before
41
+ exporting.
42
+
43
+ ## Source of truth and write discipline
44
+
45
+ `trace.ndjson` is the source of truth for workflow execution. Final Pi
46
+ conversation entries and temporal session history have separate authority in
47
+ `session/entries.ndjson` and `session/events.ndjson`. `session/capture.json`
48
+ reports whether temporal capture is complete. These sequence spaces are
49
+ independent and must not be compared.
50
+
51
+ Write order for every transition:
52
+
53
+ 1. Append the trace event (one JSON object per line, appends serialized per
54
+ file, `seq` starting at 1 and increasing by exactly 1).
55
+ 2. Atomically replace `state.json`, carrying `traceSeq` = the `seq` of the
56
+ trace event it reflects (write to a temp file in the same directory, then
57
+ rename).
58
+ 3. Atomically replace `manifest.json`.
28
59
 
29
- Every JSON file in the bundle is written atomically (write to a temp file in
30
- the same directory, then rename), so a reader never sees a partial document. `trace.ndjson` is append-only, one JSON object
31
- per line, with writes serialized per file. After a run reaches a terminal
32
- status (`completed`, `failed`, `timed_out`, `cancelled`, or `waiting`), the
33
- bundle no longer changes.
60
+ Consequences for readers:
34
61
 
35
- A live viewer needs only two behaviors. Treat `state.json` as the current
36
- projection and re-read it on any file change, and treat `trace.ndjson` as the
37
- event timeline when history matters.
62
+ - A reader never sees a partial JSON document; a torn final trace line must be
63
+ ignored.
64
+ - `state.json` with `traceSeq` older than the last trace line is a stale
65
+ projection: either re-read after the writer catches up or fold the trace
66
+ tail on top of it.
67
+ - Before the engine writes a terminal workflow event, session recording stops,
68
+ drains accepted entries and events, and atomically writes `capture.json`.
69
+ - After a run reaches a terminal status (`completed`, `failed`, `timed_out`,
70
+ `cancelled`, or `waiting`), the bundle no longer changes, and
71
+ `state.traceSeq` equals the final trace `seq`.
72
+ - A bundle whose state is `running` but whose files have stopped growing may
73
+ be an interrupted run (crash, reboot); viewers should label it as possibly
74
+ interrupted rather than live.
75
+
76
+ ## Externalized values and artifacts
77
+
78
+ Large payloads are stored once, content-addressed, under `artifacts/` and
79
+ referenced from the documents that use them. This applies uniformly to every
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
+
84
+ Encoding rule, applied recursively to a persisted value:
85
+
86
+ - A string leaf whose UTF-8 encoding is larger than 4096 bytes is written to
87
+ `artifacts/sha256-<digest>.txt` (UTF-8, digest over the exact bytes) and
88
+ replaced by an artifact reference:
89
+
90
+ ```json
91
+ {
92
+ "$artifact": {
93
+ "path": "artifacts/sha256-2b1f….txt",
94
+ "mediaType": "text/plain",
95
+ "bytes": 18342,
96
+ "sha256": "2b1f…"
97
+ }
98
+ }
99
+ ```
100
+
101
+ - Any user object that has an own key `$artifact` or `$escaped` is wrapped as
102
+ `{ "$escaped": <object> }` so the sentinel stays unambiguous. Decoders
103
+ unwrap `$escaped` one level and resolve `$artifact` refs.
104
+ - Everything else is stored inline. Small values are never externalized.
105
+
106
+ Artifact rules:
107
+
108
+ - `path` is bundle-relative; a reference never points outside the bundle.
109
+ - Artifacts are immutable once written and deduplicate by content hash.
110
+ - Readers must tolerate unknown `mediaType` values.
111
+
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.
38
115
 
39
116
  ## manifest.json
40
117
 
@@ -43,72 +120,299 @@ Identity and pointers, kept in sync with the state on every snapshot:
43
120
  ```json
44
121
  {
45
122
  "schema": "pi-workflows.run-bundle.v1",
46
- "runId": "20260719T023912Z-autoimplement-3f2a9c1b",
123
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
47
124
  "workflowName": "autoimplement",
48
125
  "runTitle": "autoimplement: fix the flaky test",
49
126
  "workflowPath": "/repo/.pi/workflows/autoimplement.workflow.ts",
50
- "startedAt": "2026-07-19T02:39:12.412Z",
51
- "finishedAt": "2026-07-19T02:41:03.977Z",
127
+ "startedAt": "2026-07-29T02:39:12.412Z",
128
+ "finishedAt": "2026-07-29T02:41:03.977Z",
52
129
  "status": "completed",
53
130
  "traceSchema": "pi-workflows.trace-event.v1",
54
- "paths": { "workflow": "workflow.json", "state": "state.json", "trace": "trace.ndjson" }
131
+ "paths": {
132
+ "workflow": "workflow.json",
133
+ "state": "state.json",
134
+ "trace": "trace.ndjson",
135
+ "session": "session",
136
+ "artifacts": "artifacts"
137
+ }
55
138
  }
56
139
  ```
57
140
 
141
+ `paths.artifacts` is declared from bundle creation so a live session-event
142
+ patch can safely reference a newly written artifact before the next workflow
143
+ state projection. The directory itself is created only when needed.
144
+ `paths.session` appears when the run binds to Pi. Readers must start from
145
+ `manifest.json`, check `schema`, skip bundles they do not understand, resolve
146
+ files through `paths`, and reject any path that escapes the bundle directory.
147
+
58
148
  ## workflow.json
59
149
 
60
- A serializable snapshot of the graph taken at run start. Functions such as
61
- prompts and validators are not serialized. Each node keeps only its metadata
62
- (`nodeType`, `timeoutMs`, `statusDetail`, `expectedOutput`, `summary`,
63
- `actionExecution`), and edges are copied verbatim. The snapshot is what lets
64
- the viewer draw all nodes, including ones that have not run yet.
150
+ A serializable snapshot of the graph taken at run start
151
+ (`pi-workflows.definition-snapshot.v1`). Functions such as prompts and
152
+ validators are not serialized. Each node keeps only its metadata (`nodeType`,
153
+ `timeoutMs`, `statusDetail`, `expectedOutput`, `summary`, `actionExecution`),
154
+ and edges are copied verbatim. The snapshot is what lets viewers draw all
155
+ nodes, including ones that have not run yet. It is immutable after run start.
156
+
157
+ ## Resume and repair
158
+
159
+ An interrupted run (status `running` with no terminal trace event) can resume
160
+ instead of failing. Resume is a named operation with strict rules:
161
+
162
+ 1. The caller must hold the run's queue claim. Only the current claim holder
163
+ may resume or interrupt a bundle, and every bundle write verifies the
164
+ claim token first (write fencing).
165
+ 2. A torn trace tail (a crash mid-append) is truncated to the last complete
166
+ line. Trace events the state projection never recorded are dropped, so
167
+ `state.traceSeq` and the trace agree again before any new event.
168
+ 3. Completed nodes replay from the projection. The in-flight node reruns with
169
+ a fresh attempt; a `run_resumed` trace event marks the boundary.
170
+ 4. `state.workflowHash` pins the workflow source from run start. Resume
171
+ refuses a hash mismatch unless forced, and a forced resume records the
172
+ mismatch in the `run_resumed` payload.
173
+
174
+ Continuation runs (answering a checkpoint) are new bundles, not resumed ones.
175
+ They link back through `state.parentRunId`, carry the parent's outputs,
176
+ results, and step records forward, and note `continuedFrom` in their
177
+ `run_started` payload. Bundles stay append-only; a continuation is the only
178
+ way work follows a terminal `waiting` state.
65
179
 
66
180
  ## state.json
67
181
 
68
182
  The full run projection (`WorkflowRunState` in
69
- [`src/workflows/types.ts`](../src/workflows/types.ts)). The `status` field is
70
- one of `running`, `waiting`, `completed`, `failed`, `timed_out`, or
71
- `cancelled`. While a node is executing, `currentNode`, `currentNodeType`,
72
- `currentNodeStartedAt`, and `statusDetail` describe it, and they disappear
73
- when the node finishes. While a pause request holds the run at a step
74
- boundary, `paused` is `true` (with matching `run_paused`/`run_resumed` trace
75
- events); it disappears when the run resumes or ends.
76
-
77
- Per-node data lives in `outputs` (the accepted output of each finished node,
78
- where the latest attempt wins on loops) and in `results` (the full result
79
- record including the outcome and timing). The ordered history is `steps`,
80
- with one record per node execution that includes the prompt text for agent
81
- steps and an action receipt with the command, exit code, and duration for
82
- action steps. When a run pauses at a checkpoint, `waitingOn` names the
83
- checkpoint node. Terminal runs carry `finalOutput` on success and `error` on
84
- failure.
183
+ [`src/workflows/types.ts`](../src/workflows/types.ts)), schema
184
+ `pi-workflows.run-state.v1`:
185
+
186
+ ```json
187
+ {
188
+ "schema": "pi-workflows.run-state.v1",
189
+ "traceSeq": 17,
190
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
191
+ "workflowName": "autoimplement",
192
+ "startedAt": "…",
193
+ "updatedAt": "…",
194
+ "status": "running",
195
+ "input": { "task": "fix the flaky test" },
196
+ "outputs": {},
197
+ "results": {},
198
+ "steps": []
199
+ }
200
+ ```
201
+
202
+ - `status` is one of `running`, `waiting`, `completed`, `failed`, `timed_out`,
203
+ or `cancelled`. A controller host records an abandoned bundle as `failed`
204
+ with a final `run_interrupted` trace event. Before doing that, recovery checks
205
+ the trace tail and repairs a stale projection when the terminal event was
206
+ already appended. The controller store can then retry a genuinely abandoned
207
+ child attempt without changing this schema. If startup stopped before a
208
+ manifest existed, the scheduler preserves that incomplete directory as a
209
+ hidden sibling before creating the reserved run.
210
+ - While a node is executing, `currentNode` and `currentAttemptId` identify it.
211
+ `currentNodeStartedAt` and `statusDetail` add timing and display context.
212
+ These fields disappear when the node finishes. The executing node's type comes from the definition
213
+ snapshot, not from the state.
214
+ - While a pause request holds the run at a step boundary, `paused` is `true`
215
+ (with matching `run_paused`/`run_resumed` trace events); it disappears when
216
+ the run resumes or ends.
217
+ - Per-node data lives in `outputs` (the accepted output of each finished node,
218
+ latest attempt wins on loops) and `results` (the full result record of the
219
+ latest attempt, including outcome and timing).
220
+ - `steps` is the ordered history, one record per node attempt:
221
+
222
+ ```json
223
+ {
224
+ "attemptId": "d81f…",
225
+ "nodeId": "implement",
226
+ "nodeType": "agent",
227
+ "outcome": "ok",
228
+ "startedAt": "…",
229
+ "finishedAt": "…",
230
+ "prompt": {
231
+ "$artifact": {
232
+ "path": "artifacts/sha256-….txt",
233
+ "mediaType": "text/plain",
234
+ "bytes": 9120,
235
+ "sha256": "…"
236
+ }
237
+ },
238
+ "output": { "summary": "…" },
239
+ "conversation": { "firstEntryId": "a1b2c3d4", "lastEntryId": "c3d4e5f6" }
240
+ }
241
+ ```
242
+
243
+ - `prompt` is the full prompt text for agent steps (`null` for other node
244
+ types), subject to value externalization.
245
+ - `conversation` is present on agent steps recorded inside a Pi conversation:
246
+ the inclusive range of Pi session entry ids in `session/entries.ndjson`
247
+ produced by this attempt, from prompt delivery through accepted submission.
248
+ Viewers must use this explicit linkage and never infer it heuristically.
249
+ - Action steps carry an `action` receipt with `actionType`
250
+ (`shell`/`function`). Shell actions also record `command`, `args`, `cwd`,
251
+ `exitCode`, `signal`, and `durationMs`. Shell stdout/stderr live in the step
252
+ output (the parsed or raw shell result) and are externalized when large.
253
+ - When a run pauses at a checkpoint, `waitingOn` names the checkpoint node.
254
+ Terminal runs carry `finalOutput` on success and `error` on failure.
85
255
 
86
256
  ## trace.ndjson
87
257
 
88
- One event per line, monotonically sequenced per run:
258
+ One event per line, monotonically sequenced per run, schema
259
+ `pi-workflows.trace-event.v1`:
89
260
 
90
261
  ```json
91
262
  {
92
263
  "seq": 3,
93
- "at": "2026-07-19T02:39:14.101Z",
264
+ "at": "2026-07-29T02:39:14.101Z",
94
265
  "scope": "agent",
95
266
  "type": "agent_prompt_sent",
96
- "runId": "...",
267
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
97
268
  "nodeId": "implement",
98
- "attemptId": "...",
99
- "payload": { "prompt": "..." }
269
+ "attemptId": "d81f…",
270
+ "payload": { "prompt": "" }
271
+ }
272
+ ```
273
+
274
+ `scope` is one of `run`, `node`, `agent`, `action`, or `session`. `nodeId` and
275
+ `attemptId` are present on node-scoped and agent-scoped events. Consumers must
276
+ ignore unknown event types and unknown payload fields so new ones can be added
277
+ within the same schema version.
278
+
279
+ The trace alone is sufficient to reconstruct the run because terminal node
280
+ events carry outputs and receipts.
281
+
282
+ Event catalog and payload contracts:
283
+
284
+ | type | scope | payload |
285
+ | ------------------- | ------- | ------------------------------------------------------------------- |
286
+ | `run_started` | run | `workflowName`, `runTitle?`, `input` |
287
+ | `session_bound` | session | `piSessionId` |
288
+ | `node_started` | node | `nodeType`, `statusDetail?` |
289
+ | `agent_prompt_sent` | agent | `prompt` |
290
+ | `node_finished` | node | `outcome: "ok"`, `durationMs`, `output`, `conversation?`, `action?` |
291
+ | `node_failed` | node | `outcome`, `durationMs`, `error`, `conversation?`, `action?` |
292
+ | `run_paused` | run | _(empty)_ |
293
+ | `run_resumed` | run | _(empty)_ |
294
+ | `run_completed` | run | `status`, `finalOutput` |
295
+ | `run_waiting` | run | `status`, `waitingOn`, `finalOutput` |
296
+ | `run_failed` | run | `status`, `error` |
297
+ | `run_timed_out` | run | `status`, `error` |
298
+ | `run_cancelled` | run | `status`, `error?` |
299
+ | `run_interrupted` | run | `error` |
300
+
301
+ Invariants:
302
+
303
+ - every node attempt has exactly one `node_started` and exactly one terminal
304
+ `node_finished`/`node_failed` with the same `attemptId`;
305
+ - `attemptId` values are unique within a run;
306
+ - a terminal `run_*` event is the last event of the run;
307
+ - events are never rewritten or deleted.
308
+
309
+ ## session/
310
+
311
+ Present when the run executed inside a Pi conversation. The extension records
312
+ the conversation into the bundle so replay never depends on Pi's global
313
+ session store.
314
+
315
+ A run that outlives its first session (parked, then resumed by another
316
+ session or the host) gains a second capture under
317
+ `session/segments/<attemptId>/` with the same file layout (`binding.json`,
318
+ `entries.ndjson`, `events.ndjson`, `capture.json`). The first capture stays
319
+ flat at `session/`; only captures from the second bind onward become
320
+ segments, so readers that predate segments keep working on single-session
321
+ bundles. An interrupted run finalizes any segment still `recording` as
322
+ `failed` with the interruption reason.
323
+
324
+ ### binding.json
325
+
326
+ Written once when the run binds to the conversation
327
+ (`pi-workflows.session-binding.v1`):
328
+
329
+ ```json
330
+ {
331
+ "schema": "pi-workflows.session-binding.v1",
332
+ "runId": "20260729T023912Z-autoimplement-3f2a9c1b",
333
+ "piSessionId": "019fad89-…",
334
+ "piSessionFile": "/home/user/.pi/agent/sessions/--repo--/2026-07-29….jsonl",
335
+ "cwd": "/repo",
336
+ "boundAt": "2026-07-29T02:39:12.412Z"
337
+ }
338
+ ```
339
+
340
+ `piSessionFile` is provenance only and absent for in-memory sessions; replay
341
+ readers must not read it.
342
+
343
+ ### entries.ndjson
344
+
345
+ Append-only copies of the Pi session entries produced on the current branch
346
+ while the run was active, schema `pi-workflows.session-entry.v1`:
347
+
348
+ ```json
349
+ {
350
+ "seq": 1,
351
+ "at": "2026-07-29T02:39:12.902Z",
352
+ "entry": {
353
+ "type": "message",
354
+ "id": "a1b2c3d4",
355
+ "parentId": "9f8e7d6c",
356
+ "timestamp": "…",
357
+ "message": { "role": "user", "content": "…" }
358
+ }
359
+ }
360
+ ```
361
+
362
+ - `seq` is strictly increasing within the file, starting at 1.
363
+ - `entry` is the verbatim Pi session entry (Pi's own versioned format),
364
+ including user messages, assistant messages, tool results, model changes,
365
+ and compaction entries. Nothing is normalized or rewritten.
366
+ - Entries include everything that happened in the conversation during the run.
367
+ This includes workflow prompts and nudges together with user interruptions.
368
+ - `conversation` ranges in step records and `node_finished` events address
369
+ entries by Pi entry id (`entry.id`).
370
+
371
+ ### events.ndjson
372
+
373
+ The temporal journal records documented Pi `turn_*` and `message_*` hooks plus
374
+ `tool_execution_*` hooks with schema `pi-workflows.session-event.v1`. Each
375
+ record has a per-file `seq`, timestamp, `nodeId`, and `attemptId`. Optional
376
+ turn, message, and tool call IDs link related records. A normalized `type` and
377
+ `payload` carry the event data.
378
+ The full contract and event catalog are in
379
+ [session-event-journal.md](session-event-journal.md).
380
+
381
+ Events preserve semantic deltas. Assistant `partial` snapshots are never
382
+ stored, and neither are terminal `message` or `error` snapshots. Tool update records omit Pi's
383
+ cumulative `partialResult`. Final `message_finished` records link to settled
384
+ Pi entries with `entryId`; after that linkage, `entries.ndjson` is the
385
+ verbatim content authority.
386
+
387
+ Readers process events by `seq`. Timestamps schedule playback but never reorder
388
+ records. A torn final line is buffered while capture is `recording`; malformed
389
+ complete lines, sequence gaps, and terminal torn tails are integrity failures.
390
+
391
+ ### capture.json
392
+
393
+ `capture.json` is an atomically replaced integrity projection:
394
+
395
+ ```json
396
+ {
397
+ "schema": "pi-workflows.session-capture.v1",
398
+ "eventSchema": "pi-workflows.session-event.v1",
399
+ "status": "complete",
400
+ "eventCount": 241,
401
+ "entryCount": 7,
402
+ "lastEventSeq": 241
100
403
  }
101
404
  ```
102
405
 
103
- Event types: `run_started`, `node_started`, `agent_prompt_sent`,
104
- `node_finished`, `node_failed`, and a terminal `run_<status>`. The `scope`
105
- field (`run`, `node`, `agent`, `action`) groups them. Consumers should ignore
106
- unknown event types so new ones can be added within the same schema version.
406
+ `status` starts as `recording` and ends as `complete` or `failed`. Failed
407
+ capture adds `failure` with `failedAt` plus a code and message. Capture failure
408
+ is visible to readers but does not fail the workflow. Terminal readers verify
409
+ the counts and last sequence, then check schemas and contiguous event order. Missing temporal files
410
+ in a session-bound bundle are invalid, not an older supported layout.
107
411
 
108
412
  ## Versioning
109
413
 
110
- Each file carries a versioned schema identifier such as
111
- `pi-workflows.run-bundle.v1`, and the identifier changes only on breaking
112
- shape changes. Readers should check `manifest.json`'s `schema` field and skip
113
- bundles they do not understand, which is exactly what the bundled viewer does
114
- with unreadable directories.
414
+ Each file carries a versioned schema identifier, and the identifier changes
415
+ only on breaking shape changes. Readers check `manifest.json`'s `schema` field
416
+ and skip bundles they do not understand. Within a version, additions of new
417
+ fields and new trace event types are allowed; readers must ignore what they do
418
+ not know.