@osolmaz/pi-workflows 0.2.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 (133) hide show
  1. package/README.md +71 -5
  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.js +1 -1
  51. package/dist/extension/executor.js.map +1 -1
  52. package/dist/extension/index.d.ts +7 -0
  53. package/dist/extension/index.js +1053 -96
  54. package/dist/extension/index.js.map +1 -1
  55. package/dist/extension/recorder.d.ts +6 -0
  56. package/dist/extension/recorder.js +20 -8
  57. package/dist/extension/recorder.js.map +1 -1
  58. package/dist/extension/workflow-tool.d.ts +28 -0
  59. package/dist/extension/workflow-tool.js +33 -0
  60. package/dist/extension/workflow-tool.js.map +1 -0
  61. package/dist/host/processes.d.ts +24 -0
  62. package/dist/host/processes.js +114 -0
  63. package/dist/host/processes.js.map +1 -0
  64. package/dist/host/rpc-bridge.d.ts +9 -0
  65. package/dist/host/rpc-bridge.js +39 -0
  66. package/dist/host/rpc-bridge.js.map +1 -0
  67. package/dist/host/rpc-executor.d.ts +38 -0
  68. package/dist/host/rpc-executor.js +254 -0
  69. package/dist/host/rpc-executor.js.map +1 -0
  70. package/dist/host/runner.d.ts +49 -0
  71. package/dist/host/runner.js +350 -0
  72. package/dist/host/runner.js.map +1 -0
  73. package/dist/viewer/cli.d.ts +7 -3
  74. package/dist/viewer/cli.js +150 -19
  75. package/dist/viewer/cli.js.map +1 -1
  76. package/dist/workflows/engine.d.ts +36 -0
  77. package/dist/workflows/engine.js +244 -14
  78. package/dist/workflows/engine.js.map +1 -1
  79. package/dist/workflows/errors.d.ts +23 -0
  80. package/dist/workflows/errors.js +38 -0
  81. package/dist/workflows/errors.js.map +1 -1
  82. package/dist/workflows/graph.js +0 -5
  83. package/dist/workflows/graph.js.map +1 -1
  84. package/dist/workflows/loader.d.ts +5 -3
  85. package/dist/workflows/loader.js +10 -1
  86. package/dist/workflows/loader.js.map +1 -1
  87. package/dist/workflows/schema.js +1 -1
  88. package/dist/workflows/schema.js.map +1 -1
  89. package/dist/workflows/store.d.ts +50 -6
  90. package/dist/workflows/store.js +446 -51
  91. package/dist/workflows/store.js.map +1 -1
  92. package/dist/workflows/types.d.ts +10 -0
  93. package/docs/CONTROLLERS.md +215 -0
  94. package/docs/development.md +12 -9
  95. package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
  96. package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
  97. package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
  98. package/docs/run-bundles.md +66 -27
  99. package/docs/workflows.md +131 -11
  100. package/examples/controllers/pull-request.controller.ts +215 -0
  101. package/package.json +10 -2
  102. package/src/builtins/monitor.workflow.ts +278 -0
  103. package/src/controllers/conditions.ts +110 -0
  104. package/src/controllers/definition.ts +65 -0
  105. package/src/controllers/effects.ts +123 -0
  106. package/src/controllers/errors.ts +27 -0
  107. package/src/controllers/index.ts +90 -0
  108. package/src/controllers/json.ts +62 -0
  109. package/src/controllers/loader.ts +104 -0
  110. package/src/controllers/manager.ts +533 -0
  111. package/src/controllers/results.ts +46 -0
  112. package/src/controllers/sqlite.ts +1427 -0
  113. package/src/controllers/store.ts +160 -0
  114. package/src/controllers/types.ts +183 -0
  115. package/src/controllers/workflow-engine-scheduler.ts +145 -0
  116. package/src/controllers/workflows.ts +152 -0
  117. package/src/extension/controller-host.ts +163 -0
  118. package/src/extension/executor.ts +1 -1
  119. package/src/extension/index.ts +1243 -117
  120. package/src/extension/recorder.ts +65 -36
  121. package/src/extension/workflow-tool.ts +59 -0
  122. package/src/host/processes.ts +119 -0
  123. package/src/host/rpc-bridge.ts +44 -0
  124. package/src/host/rpc-executor.ts +299 -0
  125. package/src/host/runner.ts +406 -0
  126. package/src/viewer/cli.ts +167 -21
  127. package/src/workflows/engine.ts +327 -13
  128. package/src/workflows/errors.ts +45 -0
  129. package/src/workflows/graph.ts +0 -5
  130. package/src/workflows/loader.ts +13 -3
  131. package/src/workflows/schema.ts +1 -1
  132. package/src/workflows/store.ts +555 -46
  133. package/src/workflows/types.ts +10 -0
package/docs/workflows.md CHANGED
@@ -9,10 +9,14 @@ 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.
16
20
 
17
21
  The workflow's command name is the file stem, so `.pi/workflows/triage.workflow.ts`
18
22
  runs as `/workflow triage`. A direct path also works: `/workflow ./somewhere/x.workflow.ts`.
@@ -44,7 +48,7 @@ Top-level fields:
44
48
 
45
49
  | Field | Type | Notes |
46
50
  | -------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
47
- | `name` | `string` | Required. Used in run ids and the step contract. `cancel`, `list`, `pause`, and `resume` are reserved for `/workflow` subcommands. |
51
+ | `name` | `string` | Required. Used in run ids and the step contract. `answer`, `cancel`, `list`, `pause`, `resume`, and `status` are reserved for `/workflow` subcommands. |
48
52
  | `title` | `string` or function | Optional run title, resolved once at start from `{ input, workflowName }`. Async resolution is bounded (30s) and cancellable. |
49
53
  | `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
54
  | `startAt` | `string` | Required. Id of the first node. |
@@ -79,6 +83,38 @@ Long-running compute, action, and checkpoint callbacks should observe
79
83
  steps). When the node times out or the run is cancelled, the engine stops
80
84
  waiting immediately, but only cooperative callbacks stop doing work.
81
85
 
86
+ ## Durable runs, parking, and resume
87
+
88
+ Every interactive `/workflow` run is tracked in the project run queue (see
89
+ [CONTROLLERS.md](CONTROLLERS.md) for the store). The session that starts a run
90
+ claims it and owns it while it executes; every bundle write proves the claim
91
+ first (write fencing).
92
+
93
+ Closing the Pi session mid-run no longer cancels the run. The engine **parks**:
94
+ it stops without a terminal event, releases the claim, and leaves the bundle
95
+ resumable. When a runner is available again (a reopened Pi session or the
96
+ standalone host), the run **resumes** at the node it stopped on. Completed
97
+ nodes replay from the recorded state; only the interrupted node and everything
98
+ downstream rerun. Resume repairs a torn trace tail, drops trace events the
99
+ state projection never recorded, and refuses to continue if the workflow
100
+ source changed since the run started (a forced resume records the mismatch).
101
+
102
+ The standalone host runs without any Pi session:
103
+
104
+ ```bash
105
+ pi-workflows host --project /path/to/project
106
+ ```
107
+
108
+ The host claims parked runs, resumes them, and reconciles durable controllers.
109
+ Conversation nodes execute in headless `pi --mode rpc` children that load a
110
+ small bridge extension; the model sees the same `workflow` tool contract as an
111
+ in-session run. The host is a foreground process: start it in a terminal and
112
+ stop it with Ctrl-C. A second host for the same project refuses to start, and
113
+ a host that dies has its orphaned children reaped by the next one. While the
114
+ host works, any open Pi session stays current: a per-session watermark over
115
+ the shared run event feed produces catch-up summaries and quiet context
116
+ updates.
117
+
82
118
  ## Node types
83
119
 
84
120
  ### agent
@@ -147,10 +183,15 @@ command fails.
147
183
 
148
184
  ### checkpoint
149
185
 
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.
186
+ Ends the run in a `waiting` state for human review. The checkpoint bundle is
187
+ terminal, so no process keeps running while the run waits. The human answers
188
+ with `/workflow answer <json>` (or plain text), which starts a **continuation
189
+ run**: a new run with its own bundle and trace, linked to the checkpointed run
190
+ through `parentRunId`. The continuation receives the answer as its input,
191
+ carries forward every output the parent produced (including the checkpoint's),
192
+ and continues routing along the checkpoint's outgoing edge. Outgoing edges
193
+ from checkpoint nodes are allowed exactly so continuations have somewhere to
194
+ go; step accounting carries over, so `maxSteps` bounds the whole chain.
154
195
 
155
196
  ```typescript
156
197
  checkpoint({
@@ -207,6 +248,57 @@ A missing case for the resolved value fails the run with a routing error. A
207
248
  node with no outgoing edge (or no matching failure route) ends the run:
208
249
  `completed` on success, `failed`/`timed_out`/`cancelled` otherwise.
209
250
 
251
+ ## Model workflow control
252
+
253
+ The model sees one `workflow` tool. Its `action` field supports:
254
+
255
+ - `list` for discovered workflow names and sources.
256
+ - `start` with a workflow name or path and structured input.
257
+ - `status` for the active run or a supplied run ID.
258
+ - `pause`, `resume`, and `cancel` for the active run.
259
+ - `answer` with checkpoint input and an optional run ID.
260
+ - `submit` for the current workflow step contract.
261
+
262
+ A model-started run is queued until the model's current turn settles. The first
263
+ workflow prompt then starts a new turn. This keeps the requesting turn outside
264
+ the workflow's first attempt and prevents an early missing-submission reminder.
265
+ The normal extension offers all actions. The headless RPC bridge offers only
266
+ `submit`, so a workflow child cannot recursively control other runs.
267
+
268
+ ### Built-in monitor
269
+
270
+ The built-in `monitor` workflow turns a plain request for repeated checks into
271
+ one looping workflow run. Its input is:
272
+
273
+ ```json
274
+ {
275
+ "task": "Check pull request 123",
276
+ "everyMinutes": 30,
277
+ "reportWhen": "Checks fail or the state changes materially",
278
+ "stopWhen": "The pull request is merged or closed",
279
+ "maxChecks": 1000
280
+ }
281
+ ```
282
+
283
+ The first check runs immediately. Each accepted check records a bounded current
284
+ observation and chooses whether to continue, report, or stop. A report uses a
285
+ separate agent node so its structured check result is validated before the user
286
+ sees the message. The next check can read the previous accepted observation.
287
+
288
+ Intervals must be whole minutes from 1 through 1,440. `maxChecks` defaults to
289
+ 1,000 and cannot exceed 1,000. The workflow also has a finite step limit and
290
+ bounded observation and report sizes.
291
+
292
+ The interval uses the existing shell action to launch the current Node
293
+ executable with a timer. This works on every platform supported by Pi. The node
294
+ and command timeouts are higher than the maximum interval. Cancelling the
295
+ workflow aborts the timer process immediately. If the owning Pi process or
296
+ standalone host stops during the wait, normal parking rules abort the shell node
297
+ and resume later by running that wait again from the beginning.
298
+
299
+ A monitor uses the session's single active workflow slot. It does not provide
300
+ cron syntax, calendar scheduling, OS notifications, or a background service.
301
+
210
302
  ## The step contract
211
303
 
212
304
  Every `agent` prompt ends with a step contract block naming the workflow, the
@@ -217,16 +309,17 @@ step id, the attempt id, and the expected output shape:
217
309
  Workflow step contract (workflow: autoimplement, step: review, attempt: 6f9d…)
218
310
 
219
311
  Complete this step by calling the `workflow` tool exactly once with:
220
- {"step": "review", "attempt": "6f9d…", "output": <your result>}
312
+ {"action": "submit", "step": "review", "attempt": "6f9d…", "output": <your result>}
221
313
  Expected output: { "route": "clean" | "issues_found", "reason": "short justification" }
222
314
  The step is complete only after the workflow tool accepts the output.
223
315
  If the tool reports a validation error, correct the output and call it again.
224
316
  ```
225
317
 
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.
318
+ The `workflow` tool uses `{ action: "submit", step, attempt, output }` for step
319
+ results. Submissions are rejected (with a reason the model sees) when no step
320
+ is pending, the step id is wrong, the attempt id belongs to an earlier attempt
321
+ of the same node (loops revisit node ids, so each attempt gets a fresh id), or
322
+ `validate` throws.
230
323
  Acceptance resolves the step and the engine advances; the next agent prompt
231
324
  arrives as a new user message in the same conversation.
232
325
 
@@ -290,6 +383,33 @@ possible. Defaults worth knowing:
290
383
  - Agent nudges: if the model ends its turn without submitting the pending
291
384
  step, it gets a reminder, twice by default, then the step fails.
292
385
 
386
+ ## Workflows started by controllers
387
+
388
+ 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.
389
+
390
+ ```typescript
391
+ const run = await ctx.workflows.ensure({
392
+ requestKey: `repair:${resource.metadata.generation}`,
393
+ workflow: "repair-pull-request",
394
+ input: { repository: resource.spec.repository, number: resource.spec.number },
395
+ });
396
+
397
+ if (run.state !== "succeeded") {
398
+ return ctx.requeueAfter(5_000, {
399
+ workflowRun: {
400
+ requestId: run.requestId,
401
+ ...(run.runId ? { runId: run.runId } : {}),
402
+ state: run.state,
403
+ attempt: run.attempt,
404
+ },
405
+ });
406
+ }
407
+ ```
408
+
409
+ 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.
410
+
411
+ See [CONTROLLERS.md](CONTROLLERS.md) for controller definitions and the full recovery contract.
412
+
293
413
  ## Using the engine outside pi
294
414
 
295
415
  The engine is pi-agnostic. `WorkflowEngine` takes any `AgentStepExecutor`, so
@@ -0,0 +1,215 @@
1
+ import {
2
+ conditionFalse,
3
+ conditionTrue,
4
+ conditionUnknown,
5
+ defineController,
6
+ type ChildWorkflowRecord,
7
+ } from "@osolmaz/pi-workflows/controllers";
8
+
9
+ type PullRequestSpec = {
10
+ apiBaseUrl: string;
11
+ repository: string;
12
+ number: number;
13
+ expectedHeadSha: string;
14
+ repairWorkflow: string;
15
+ mergeApproved: boolean;
16
+ };
17
+
18
+ type PullRequestStatus = {
19
+ phase: "observing" | "repairing" | "waiting" | "ready" | "merged" | "blocked";
20
+ observedHeadSha?: string;
21
+ };
22
+
23
+ type PullRequest = {
24
+ merged: boolean;
25
+ merge_commit_sha?: string | null;
26
+ head: { sha: string };
27
+ };
28
+
29
+ type CommitStatus = {
30
+ state: "error" | "failure" | "pending" | "success";
31
+ };
32
+
33
+ export default defineController<PullRequestSpec, PullRequestStatus>({
34
+ name: "pull-request",
35
+ initialStatus: () => ({ phase: "observing" }),
36
+
37
+ async reconcile(ctx, resource) {
38
+ const client = new GitHubClient(resource.spec.apiBaseUrl, resource.spec.repository);
39
+ const pullRequest = await client.pullRequest(resource.spec.number, ctx.signal);
40
+ const observed = { observedHeadSha: pullRequest.head.sha };
41
+
42
+ if (pullRequest.merged) {
43
+ return ctx.settled({
44
+ controllerStatus: { phase: "merged", ...observed },
45
+ conditions: [conditionTrue("Ready", "Merged")],
46
+ workflowRun: null,
47
+ });
48
+ }
49
+ if (pullRequest.head.sha !== resource.spec.expectedHeadSha) {
50
+ return ctx.settled({
51
+ controllerStatus: { phase: "blocked", ...observed },
52
+ conditions: [conditionFalse("Ready", "HeadChanged")],
53
+ });
54
+ }
55
+
56
+ const child = await ctx.workflows.ensure({
57
+ requestKey: `repair:${resource.metadata.generation}:${pullRequest.head.sha}`,
58
+ workflow: resource.spec.repairWorkflow,
59
+ input: {
60
+ repository: resource.spec.repository,
61
+ number: resource.spec.number,
62
+ expectedHeadSha: pullRequest.head.sha,
63
+ },
64
+ });
65
+ const workflowRun = workflowReference(child);
66
+ if (child.state === "failed") {
67
+ return ctx.settled({
68
+ controllerStatus: { phase: "blocked", ...observed },
69
+ conditions: [conditionFalse("Ready", "RepairFailed", child.error)],
70
+ workflowRun,
71
+ });
72
+ }
73
+ if (child.state !== "succeeded") {
74
+ return ctx.requeueAfter(5_000, {
75
+ controllerStatus: { phase: "repairing", ...observed },
76
+ conditions: [conditionUnknown("Ready", "RepairRunning")],
77
+ workflowRun,
78
+ });
79
+ }
80
+
81
+ const checks = await client.commitStatus(pullRequest.head.sha, ctx.signal);
82
+ if (checks.state !== "success") {
83
+ return ctx.requeueAfter(30_000, {
84
+ controllerStatus: { phase: "waiting", ...observed },
85
+ conditions: [conditionUnknown("Ready", "ChecksPending", checks.state)],
86
+ workflowRun,
87
+ });
88
+ }
89
+ if (!resource.spec.mergeApproved) {
90
+ return ctx.settled({
91
+ controllerStatus: { phase: "ready", ...observed },
92
+ conditions: [conditionFalse("Ready", "ApprovalRequired")],
93
+ workflowRun,
94
+ });
95
+ }
96
+
97
+ const effect = await ctx.effects.ensure({
98
+ key: `merge:${resource.metadata.generation}:${pullRequest.head.sha}`,
99
+ kind: "github-merge",
100
+ request: {
101
+ repository: resource.spec.repository,
102
+ number: resource.spec.number,
103
+ expectedHeadSha: pullRequest.head.sha,
104
+ },
105
+ observe: async (signal) => {
106
+ const latest = await client.pullRequest(resource.spec.number, signal);
107
+ return latest.merged
108
+ ? {
109
+ state: "applied",
110
+ ...(latest.merge_commit_sha ? { externalRef: latest.merge_commit_sha } : {}),
111
+ }
112
+ : { state: "not_applied" };
113
+ },
114
+ apply: async (signal) =>
115
+ await client.merge(resource.spec.number, pullRequest.head.sha, signal),
116
+ });
117
+ if (effect.state === "rejected") {
118
+ return ctx.settled({
119
+ controllerStatus: { phase: "blocked", ...observed },
120
+ conditions: [conditionFalse("Ready", "MergeRejected", effect.error)],
121
+ workflowRun,
122
+ });
123
+ }
124
+ if (effect.state === "indeterminate") {
125
+ return ctx.requeueAfter(10_000, {
126
+ controllerStatus: { phase: "waiting", ...observed },
127
+ conditions: [conditionUnknown("Ready", "MergeUncertain", effect.error)],
128
+ workflowRun,
129
+ });
130
+ }
131
+ return ctx.requeue({
132
+ controllerStatus: { phase: "waiting", ...observed },
133
+ conditions: [conditionUnknown("Ready", "MergeSubmitted")],
134
+ workflowRun,
135
+ });
136
+ },
137
+ });
138
+
139
+ class GitHubClient {
140
+ private readonly baseUrl: URL;
141
+ private readonly owner: string;
142
+ private readonly repository: string;
143
+
144
+ constructor(baseUrl: string, repository: string) {
145
+ this.baseUrl = new URL(baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`);
146
+ const parts = repository.split("/");
147
+ if (parts.length !== 2 || parts.some((part) => part.length === 0)) {
148
+ throw new Error(`Invalid repository: ${repository}`);
149
+ }
150
+ this.owner = parts[0] as string;
151
+ this.repository = parts[1] as string;
152
+ }
153
+
154
+ async pullRequest(number: number, signal: AbortSignal): Promise<PullRequest> {
155
+ return await this.request<PullRequest>(`pulls/${number}`, { signal });
156
+ }
157
+
158
+ async commitStatus(sha: string, signal: AbortSignal): Promise<CommitStatus> {
159
+ return await this.request<CommitStatus>(`commits/${encodeURIComponent(sha)}/status`, {
160
+ signal,
161
+ });
162
+ }
163
+
164
+ async merge(number: number, headSha: string, signal: AbortSignal) {
165
+ const response = await this.rawRequest(`pulls/${number}/merge`, {
166
+ method: "PUT",
167
+ signal,
168
+ headers: { "content-type": "application/json" },
169
+ body: JSON.stringify({ sha: headSha }),
170
+ });
171
+ const body = (await response.json()) as {
172
+ merged?: boolean;
173
+ message?: string;
174
+ sha?: string;
175
+ };
176
+ if (!response.ok || body.merged !== true) {
177
+ return { state: "rejected" as const, error: body.message ?? `HTTP ${response.status}` };
178
+ }
179
+ return {
180
+ state: "applied" as const,
181
+ ...(body.sha !== undefined ? { externalRef: body.sha } : {}),
182
+ };
183
+ }
184
+
185
+ private async request<T>(path: string, init: RequestInit): Promise<T> {
186
+ const response = await this.rawRequest(path, init);
187
+ if (!response.ok) {
188
+ throw new Error(`GitHub request failed with HTTP ${response.status}`);
189
+ }
190
+ return (await response.json()) as T;
191
+ }
192
+
193
+ private async rawRequest(path: string, init: RequestInit): Promise<Response> {
194
+ const url = new URL(
195
+ `repos/${encodeURIComponent(this.owner)}/${encodeURIComponent(this.repository)}/${path}`,
196
+ this.baseUrl,
197
+ );
198
+ return await fetch(url, {
199
+ ...init,
200
+ headers: {
201
+ accept: "application/vnd.github+json",
202
+ ...init.headers,
203
+ },
204
+ });
205
+ }
206
+ }
207
+
208
+ function workflowReference(child: ChildWorkflowRecord) {
209
+ return {
210
+ requestId: child.requestId,
211
+ ...(child.runId !== undefined ? { runId: child.runId } : {}),
212
+ state: child.state,
213
+ attempt: child.attempt,
214
+ };
215
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@osolmaz/pi-workflows",
3
- "version": "0.2.0",
4
- "description": "Workflow engine, JSON control-flow tool, and live terminal viewer for the pi coding agent",
3
+ "version": "0.3.0",
4
+ "description": "Workflow and controller runtime with a live terminal viewer for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi-package"
7
7
  ],
@@ -34,6 +34,10 @@
34
34
  "./extension": {
35
35
  "types": "./dist/extension/index.d.ts",
36
36
  "default": "./dist/extension/index.js"
37
+ },
38
+ "./controllers": {
39
+ "types": "./dist/controllers/index.d.ts",
40
+ "default": "./dist/controllers/index.js"
37
41
  }
38
42
  },
39
43
  "publishConfig": {
@@ -53,10 +57,13 @@
53
57
  "typecheck": "tsc -p tsconfig.json --noEmit"
54
58
  },
55
59
  "dependencies": {
60
+ "better-sqlite3": "^13.0.2",
56
61
  "jiti": "^2.7.0"
57
62
  },
58
63
  "devDependencies": {
64
+ "@earendil-works/pi-ai": "^0.80.10",
59
65
  "@earendil-works/pi-coding-agent": "^0.80.10",
66
+ "@types/better-sqlite3": "^7.6.13",
60
67
  "@types/node": "^26.1.1",
61
68
  "@vitest/coverage-istanbul": "^4.1.10",
62
69
  "oxfmt": "^0.59.0",
@@ -67,6 +74,7 @@
67
74
  "vitest": "^4.1.10"
68
75
  },
69
76
  "peerDependencies": {
77
+ "@earendil-works/pi-ai": "*",
70
78
  "@earendil-works/pi-coding-agent": "*",
71
79
  "typebox": "*"
72
80
  },