@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.
- package/README.md +71 -5
- package/dist/builtins/monitor.workflow.d.ts +71 -0
- package/dist/builtins/monitor.workflow.js +234 -0
- package/dist/builtins/monitor.workflow.js.map +1 -0
- package/dist/controllers/conditions.d.ts +6 -0
- package/dist/controllers/conditions.js +68 -0
- package/dist/controllers/conditions.js.map +1 -0
- package/dist/controllers/definition.d.ts +6 -0
- package/dist/controllers/definition.js +45 -0
- package/dist/controllers/definition.js.map +1 -0
- package/dist/controllers/effects.d.ts +14 -0
- package/dist/controllers/effects.js +104 -0
- package/dist/controllers/effects.js.map +1 -0
- package/dist/controllers/errors.d.ts +12 -0
- package/dist/controllers/errors.js +25 -0
- package/dist/controllers/errors.js.map +1 -0
- package/dist/controllers/index.d.ts +13 -0
- package/dist/controllers/index.js +13 -0
- package/dist/controllers/index.js.map +1 -0
- package/dist/controllers/json.d.ts +5 -0
- package/dist/controllers/json.js +57 -0
- package/dist/controllers/json.js.map +1 -0
- package/dist/controllers/loader.d.ts +23 -0
- package/dist/controllers/loader.js +74 -0
- package/dist/controllers/loader.js.map +1 -0
- package/dist/controllers/manager.d.ts +58 -0
- package/dist/controllers/manager.js +399 -0
- package/dist/controllers/manager.js.map +1 -0
- package/dist/controllers/results.d.ts +5 -0
- package/dist/controllers/results.js +32 -0
- package/dist/controllers/results.js.map +1 -0
- package/dist/controllers/sqlite.d.ts +212 -0
- package/dist/controllers/sqlite.js +1009 -0
- package/dist/controllers/sqlite.js.map +1 -0
- package/dist/controllers/store.d.ts +112 -0
- package/dist/controllers/store.js +32 -0
- package/dist/controllers/store.js.map +1 -0
- package/dist/controllers/types.d.ts +159 -0
- package/dist/controllers/types.js +2 -0
- package/dist/controllers/types.js.map +1 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +25 -0
- package/dist/controllers/workflow-engine-scheduler.js +93 -0
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -0
- package/dist/controllers/workflows.d.ts +27 -0
- package/dist/controllers/workflows.js +109 -0
- package/dist/controllers/workflows.js.map +1 -0
- package/dist/extension/controller-host.d.ts +47 -0
- package/dist/extension/controller-host.js +110 -0
- package/dist/extension/controller-host.js.map +1 -0
- package/dist/extension/executor.js +1 -1
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/index.d.ts +7 -0
- package/dist/extension/index.js +1053 -96
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +6 -0
- package/dist/extension/recorder.js +20 -8
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/workflow-tool.d.ts +28 -0
- package/dist/extension/workflow-tool.js +33 -0
- package/dist/extension/workflow-tool.js.map +1 -0
- package/dist/host/processes.d.ts +24 -0
- package/dist/host/processes.js +114 -0
- package/dist/host/processes.js.map +1 -0
- package/dist/host/rpc-bridge.d.ts +9 -0
- package/dist/host/rpc-bridge.js +39 -0
- package/dist/host/rpc-bridge.js.map +1 -0
- package/dist/host/rpc-executor.d.ts +38 -0
- package/dist/host/rpc-executor.js +254 -0
- package/dist/host/rpc-executor.js.map +1 -0
- package/dist/host/runner.d.ts +49 -0
- package/dist/host/runner.js +350 -0
- package/dist/host/runner.js.map +1 -0
- package/dist/viewer/cli.d.ts +7 -3
- package/dist/viewer/cli.js +150 -19
- package/dist/viewer/cli.js.map +1 -1
- package/dist/workflows/engine.d.ts +36 -0
- package/dist/workflows/engine.js +244 -14
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +23 -0
- package/dist/workflows/errors.js +38 -0
- package/dist/workflows/errors.js.map +1 -1
- package/dist/workflows/graph.js +0 -5
- package/dist/workflows/graph.js.map +1 -1
- package/dist/workflows/loader.d.ts +5 -3
- package/dist/workflows/loader.js +10 -1
- package/dist/workflows/loader.js.map +1 -1
- package/dist/workflows/schema.js +1 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +50 -6
- package/dist/workflows/store.js +446 -51
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +10 -0
- package/docs/CONTROLLERS.md +215 -0
- package/docs/development.md +12 -9
- package/docs/plans/2026-08-04-controller-runtime-plan.md +169 -0
- package/docs/plans/2026-08-05-always-on-workflows-plan.md +125 -0
- package/docs/plans/2026-08-10-agent-managed-monitor-workflows-plan.md +184 -0
- package/docs/run-bundles.md +66 -27
- package/docs/workflows.md +131 -11
- package/examples/controllers/pull-request.controller.ts +215 -0
- package/package.json +10 -2
- package/src/builtins/monitor.workflow.ts +278 -0
- package/src/controllers/conditions.ts +110 -0
- package/src/controllers/definition.ts +65 -0
- package/src/controllers/effects.ts +123 -0
- package/src/controllers/errors.ts +27 -0
- package/src/controllers/index.ts +90 -0
- package/src/controllers/json.ts +62 -0
- package/src/controllers/loader.ts +104 -0
- package/src/controllers/manager.ts +533 -0
- package/src/controllers/results.ts +46 -0
- package/src/controllers/sqlite.ts +1427 -0
- package/src/controllers/store.ts +160 -0
- package/src/controllers/types.ts +183 -0
- package/src/controllers/workflow-engine-scheduler.ts +145 -0
- package/src/controllers/workflows.ts +152 -0
- package/src/extension/controller-host.ts +163 -0
- package/src/extension/executor.ts +1 -1
- package/src/extension/index.ts +1243 -117
- package/src/extension/recorder.ts +65 -36
- package/src/extension/workflow-tool.ts +59 -0
- package/src/host/processes.ts +119 -0
- package/src/host/rpc-bridge.ts +44 -0
- package/src/host/rpc-executor.ts +299 -0
- package/src/host/runner.ts +406 -0
- package/src/viewer/cli.ts +167 -21
- package/src/workflows/engine.ts +327 -13
- package/src/workflows/errors.ts +45 -0
- package/src/workflows/graph.ts +0 -5
- package/src/workflows/loader.ts +13 -3
- package/src/workflows/schema.ts +1 -1
- package/src/workflows/store.ts +555 -46
- 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
|
|
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 `
|
|
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.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
227
|
-
rejected (with a reason the model sees) when no step
|
|
228
|
-
is wrong, the attempt id belongs to an earlier attempt
|
|
229
|
-
revisit node ids, so each attempt gets a fresh id), or
|
|
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.
|
|
4
|
-
"description": "Workflow
|
|
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
|
},
|