pi-worker-graph 0.1.0-dev.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 (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +455 -0
  3. package/SECURITY.md +54 -0
  4. package/dist/config.d.ts +25 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +181 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/context.d.ts +22 -0
  9. package/dist/context.d.ts.map +1 -0
  10. package/dist/context.js +81 -0
  11. package/dist/context.js.map +1 -0
  12. package/dist/coordination.d.ts +19 -0
  13. package/dist/coordination.d.ts.map +1 -0
  14. package/dist/coordination.js +267 -0
  15. package/dist/coordination.js.map +1 -0
  16. package/dist/execution-failure.d.ts +42 -0
  17. package/dist/execution-failure.d.ts.map +1 -0
  18. package/dist/execution-failure.js +90 -0
  19. package/dist/execution-failure.js.map +1 -0
  20. package/dist/extension.d.ts +8 -0
  21. package/dist/extension.d.ts.map +1 -0
  22. package/dist/extension.js +641 -0
  23. package/dist/extension.js.map +1 -0
  24. package/dist/graph.d.ts +44 -0
  25. package/dist/graph.d.ts.map +1 -0
  26. package/dist/graph.js +292 -0
  27. package/dist/graph.js.map +1 -0
  28. package/dist/index.d.ts +16 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +8 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/json.d.ts +9 -0
  33. package/dist/json.d.ts.map +1 -0
  34. package/dist/json.js +66 -0
  35. package/dist/json.js.map +1 -0
  36. package/dist/orchestrator.d.ts +15 -0
  37. package/dist/orchestrator.d.ts.map +1 -0
  38. package/dist/orchestrator.js +473 -0
  39. package/dist/orchestrator.js.map +1 -0
  40. package/dist/output.d.ts +61 -0
  41. package/dist/output.d.ts.map +1 -0
  42. package/dist/output.js +248 -0
  43. package/dist/output.js.map +1 -0
  44. package/dist/pi-subprocess.d.ts +92 -0
  45. package/dist/pi-subprocess.d.ts.map +1 -0
  46. package/dist/pi-subprocess.js +897 -0
  47. package/dist/pi-subprocess.js.map +1 -0
  48. package/dist/run.d.ts +89 -0
  49. package/dist/run.d.ts.map +1 -0
  50. package/dist/run.js +562 -0
  51. package/dist/run.js.map +1 -0
  52. package/dist/store.d.ts +331 -0
  53. package/dist/store.d.ts.map +1 -0
  54. package/dist/store.js +1993 -0
  55. package/dist/store.js.map +1 -0
  56. package/dist/usage.d.ts +35 -0
  57. package/dist/usage.d.ts.map +1 -0
  58. package/dist/usage.js +88 -0
  59. package/dist/usage.js.map +1 -0
  60. package/docs/DECISIONS.md +221 -0
  61. package/docs/DESIGN.md +392 -0
  62. package/docs/NEXT.md +229 -0
  63. package/docs/PLAN.md +203 -0
  64. package/docs/worker-graph.example.json +12 -0
  65. package/extensions/index.ts +1 -0
  66. package/extensions/tsconfig.json +11 -0
  67. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pi-worker-graph contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,455 @@
1
+ # pi-worker-graph
2
+
3
+ A small DAG-first worker orchestration runtime for the
4
+ [Pi coding agent](https://github.com/earendil-works/pi).
5
+
6
+ `pi-worker-graph` is designed for one orchestrator coordinating multiple writable
7
+ workers in a shared checkout. Dependency edges control scheduling and carry
8
+ bounded structured context; an optional run-scoped journal carries facts
9
+ discovered while work is in progress.
10
+
11
+ ## Status
12
+
13
+ Early implementation. The package currently provides tested graph primitives,
14
+ a versioned structured worker-report contract, canonical byte-bounded
15
+ prerequisite context, an explicit-root filesystem store, a bounded DAG runner,
16
+ a one-shot Pi subprocess adapter, immutable run-scoped coordination events and
17
+ inboxes, and an explicitly activated parent orchestration tool. The parent tool
18
+ is inactive by default; worker children receive only the final-report and
19
+ coordination tools.
20
+
21
+ ## Setup
22
+
23
+ The extension has no provider or model defaults, so it does not run until a
24
+ configuration exists. Copy the example and edit its profiles:
25
+
26
+ ```bash
27
+ mkdir -p ~/.pi/agent
28
+ cp docs/worker-graph.example.json ~/.pi/agent/worker-graph.json
29
+ ```
30
+
31
+ The npm artifact ships `docs/` too, so the same file is present in an installed
32
+ copy of the package.
33
+
34
+ Every profile must name its `provider`, `model`, `thinkingLevel`, and `tools`
35
+ explicitly; unknown fields are rejected. Valid thinking levels are `off`,
36
+ `minimal`, `low`, `medium`, `high`, `xhigh`, and `max`. Valid worker tools are
37
+ `read`, `bash`, `powershell`, `edit`, `write`, `grep`, `find`, and `ls`. A graph
38
+ task selects a profile by name, so the name in the example is illustrative
39
+ rather than required.
40
+
41
+ Profiles configure workers. The capable model belongs on the parent, which
42
+ decomposes the work, reads the integrated checkout, and decides acceptance; a
43
+ worker executes one narrow assignment the parent already scoped, so the example
44
+ configures a cheaper model here. Add further profiles when tasks genuinely need
45
+ different capability or a narrower tool set.
46
+
47
+ The parent session can be configured in the same file, through an optional
48
+ `orchestrator` block:
49
+
50
+ ```json
51
+ {
52
+ "orchestrator": {
53
+ "provider": "anthropic",
54
+ "model": "claude-opus-4-5",
55
+ "thinkingLevel": "high"
56
+ }
57
+ }
58
+ ```
59
+
60
+ Enabling the mode then moves the session onto that model and restores the
61
+ previous one on `/swarm off` and on leaving the branch. Shutdown attempts the
62
+ same restore, but it is a best effort: the model is put back through an
63
+ asynchronous Pi call, and a session ending need not wait for it. A restore that
64
+ cannot be performed at all — the earlier model has left Pi's catalogue, or its
65
+ provider lost authentication — is reported rather than passed over in silence.
66
+ The block has no `tools` field: the parent's tools stay governed by the mode's
67
+ own snapshot.
68
+ `thinkingLevel` accepts every level a worker profile accepts except `off`,
69
+ which Pi's session thinking level cannot express.
70
+
71
+ The block is deliberately absent from the example, because a model your Pi
72
+ install cannot find would refuse to enable the mode. Leave it out and the
73
+ parent stays exactly as you started it, with its model coming from Pi's own
74
+ settings or preset.
75
+
76
+ A valid configuration is required to enable the mode at all, and by `/swarm
77
+ runs` and `/swarm delete`, which resolve the run-store state root through it.
78
+ See [Orchestrator tool](#orchestrator-tool) for `stateRoot`,
79
+ `maxRetainedRuns`, and the activation commands.
80
+
81
+ ## Graph semantics
82
+
83
+ - The complete graph is validated before execution.
84
+ - Task IDs are normalized, non-empty, and unique.
85
+ - Dependencies must exist, cannot refer to the same task, and must be acyclic.
86
+ - A task becomes ready only after all direct prerequisites succeed.
87
+ - Failed, aborted, or blocked tasks block their descendants.
88
+ - Unrelated branches remain eligible to run.
89
+ - Ready task ordering is deterministic.
90
+
91
+ ```ts
92
+ import {
93
+ createInitialState,
94
+ normalizeGraph,
95
+ readyFrontier,
96
+ setNodeStatus,
97
+ } from "pi-worker-graph";
98
+
99
+ const graph = normalizeGraph({
100
+ tasks: [
101
+ { id: "api", payload: { task: "Implement the API" } },
102
+ { id: "ui", payload: { task: "Implement the UI" } },
103
+ {
104
+ id: "integration",
105
+ needs: ["api", "ui"],
106
+ payload: { task: "Integrate and validate" },
107
+ },
108
+ ],
109
+ concurrency: 2,
110
+ });
111
+
112
+ let state = createInitialState(graph);
113
+ console.log(readyFrontier(graph, state)); // ["api", "ui"]
114
+
115
+ state = setNodeStatus(graph, state, "api", "running");
116
+ state = setNodeStatus(graph, state, "api", "succeeded");
117
+ ```
118
+
119
+ The graph module is generic over `payload` and has no provider, model, process,
120
+ filesystem, or Pi runtime dependency. `readyFrontier()` returns every eligible
121
+ task; `runGraph()` applies concurrency and fixed resource limits when selecting
122
+ and executing work through an injected adapter.
123
+
124
+ ## Worker reports
125
+
126
+ A completed task executor must return a complete schema-versioned `NodeOutput`
127
+ containing its summary, changed files, interfaces, decisions, validation, and
128
+ blockers. Changed-file paths are normalized repository-relative paths. Reports
129
+ reject unknown fields, empty or oversized text, excessive item counts, hostile
130
+ values, and oversized serialized JSON. `parseNodeOutput()` validates untrusted
131
+ values and returns an immutable snapshot suitable for publication or
132
+ dependency-edge propagation. A report with blockers fails its node, retains the
133
+ report for review, and blocks dependents.
134
+
135
+ Direct-prerequisite reports are serialized in deterministic task-ID order into
136
+ named JSON blocks. The complete UTF-8 context, including labels and an explicit
137
+ untrusted-worker-data warning, is measured against a hard byte limit. Oversized
138
+ context fails the downstream node and is never silently truncated. Only the
139
+ validated reports of declared direct prerequisites are included.
140
+
141
+ A worker may also retain one bounded text artifact beside its report, through
142
+ the optional `artifact` field on `worker_graph_report`: supplemental long-form
143
+ material such as a log, investigation notes, or detailed review findings. It is
144
+ a sibling of the report rather than a field of it, so the report envelope stays
145
+ at schema version 1 and the artifact is bounded separately. The runtime never
146
+ parses it and never places it on a dependency edge, so a report that leans on
147
+ its artifact is an incomplete report. It is stored under the run, the output
148
+ envelope records the byte length that vouches for it, and it is removed when
149
+ the run is deleted. `readNodeArtifact()` reads it back, and the orchestrator
150
+ tool result names its byte length so a retained artifact is discoverable. An
151
+ aborted task produced nothing to retain and may not publish one.
152
+
153
+ Neither overflow becomes truncation. An oversized report is rejected so the
154
+ worker can correct and resubmit it, and oversized prerequisite context fails
155
+ the downstream node rather than handing it a partial prerequisite contract.
156
+
157
+ ## Pi worker adapter
158
+
159
+ `createPiSubprocessExecutor()` selects an explicitly named worker profile for
160
+ each task. It starts one ephemeral Pi JSON-mode child in the target checkout,
161
+ disables discovered extensions, skills, prompts, and session persistence, and
162
+ applies a strict built-in tool allowlist plus the child-only report tool. Task
163
+ assignments and edge context are written to stdin and never added to child-process
164
+ arguments.
165
+
166
+ ```ts
167
+ import { createPiSubprocessExecutor, runGraph } from "pi-worker-graph";
168
+
169
+ const executor = createPiSubprocessExecutor({
170
+ profiles: {
171
+ worker: {
172
+ provider: "anthropic",
173
+ model: "claude-haiku-4-5",
174
+ thinkingLevel: "medium",
175
+ tools: ["read", "bash", "edit", "write"],
176
+ },
177
+ },
178
+ });
179
+
180
+ await runGraph({
181
+ stateRoot: "/path/outside/the/checkout",
182
+ workingDirectory: process.cwd(),
183
+ executor,
184
+ graph: {
185
+ tasks: [
186
+ {
187
+ id: "implementation",
188
+ payload: {
189
+ profile: "worker",
190
+ assignment: "Implement the requested change",
191
+ acceptanceCriteria: ["Tests pass"],
192
+ expectedPaths: ["src/"],
193
+ },
194
+ },
195
+ ],
196
+ },
197
+ });
198
+ ```
199
+
200
+ The adapter requires a valid terminating `worker_graph_report` call, maps
201
+ process/provider/report failures to fixed safe diagnostics, and terminates the
202
+ child process group on cancellation. Automated tests use fake subprocesses and
203
+ make no provider calls.
204
+
205
+ Event-stream handling is deliberately tolerant of normal worker behaviour. Pi's
206
+ JSON mode reports every session event, so single lines carry whole tool results
207
+ and whole assistant messages; lines too large to parse are skipped rather than
208
+ failing the task, and the framing bound is derived from the report envelope so a
209
+ valid report can never be skipped. A rejected report is recoverable: the worker
210
+ may correct and resubmit it, and only a worker that never produces a valid
211
+ report fails on that signal. Once a report is captured, a provider error or a
212
+ nonzero exit afterwards does not discard it — the structured report is the task
213
+ contract. Cancellation still outranks a captured report.
214
+
215
+ The report must also be the worker's *last* action. Pi executes the tool calls
216
+ of one assistant message as a batch, and a terminating result only ends the
217
+ session when every result in that batch terminates, so a report called alongside
218
+ `write` or `bash` leaves the worker running. The adapter tracks the batch the
219
+ report belonged to and any tool that runs afterwards, and fails such a task
220
+ instead of accepting a report that work outlived.
221
+
222
+ With no explicit `command`, the adapter locates Pi's CLI entry point through
223
+ this package's dependency on Pi and runs it under the current JavaScript
224
+ runtime. Nothing is inferred from `PI_CODING_AGENT`, which Pi exports to every
225
+ process it launches, and no command interpreter is involved on any platform.
226
+ Supply `command` when Pi cannot be resolved that way.
227
+
228
+ An executor rejects with `TaskExecutionFailure`, whose diagnostics come from a
229
+ fixed allowlist, so no provider text or repository content reaches persisted run
230
+ state. `runGraph` reports a graph the executor refuses through
231
+ `RunGraphValidationError` with an `adapter_validation` issue, the same error type
232
+ as every other pre-run rejection.
233
+
234
+ The adapter projects bounded progress snapshots containing only task identity,
235
+ phase, allowlisted tool name, and numeric usage. Worker text, tool arguments,
236
+ tool results, and stderr are never included. Progress callbacks are capped and
237
+ cannot alter worker execution if an observer throws.
238
+
239
+ ## Worker coordination
240
+
241
+ Workers receive four child-only tools for facts discovered after scheduling:
242
+ `worker_graph_event` publishes one bounded coordination fact, optionally
243
+ addressed to named tasks and tagged with paths or symbols;
244
+ `worker_graph_message` sends one directed handoff; `worker_graph_events` and
245
+ `worker_graph_inbox` read them. Nothing is injected automatically — a worker
246
+ reads only what it asks for — and returned records are labeled as untrusted
247
+ worker-authored data.
248
+
249
+ Every record in a run shares one monotonic sequence, so an identifier is also a
250
+ position: a cursor cannot skip a record published between two reads. Claiming a
251
+ sequence number and storing the record it names are one exclusive create in one
252
+ journal, so no identifier is ever reserved for a record that lands after a
253
+ reader has been handed a cursor past it. A read is bounded by a requested record
254
+ count and by the serialized JSON array size, including its brackets and
255
+ separators. This holds strictly because every record is bounded at 32 KiB when
256
+ published, half of one 64 KiB page; a read returns a cursor for the remainder.
257
+ Both kinds share the journal, so a read also passes over records it is never
258
+ given — the other kind, another task's mail — and its cursor runs past them, so
259
+ polling an inbox costs only the records published since the last call rather
260
+ than the whole journal each time. A cursor therefore belongs to the query that
261
+ produced it, and a page can come back empty with one; a reader stops when no
262
+ cursor is returned.
263
+ Publication synchronizes the active-owner check with ownership release. The
264
+ parent and every worker contend for one run mutation lock, which is claimed by
265
+ linking a record that already names its holder, so contention with a live
266
+ worker is never mistaken for a lock a killed worker left behind: a parent
267
+ mutation waits contention out and recovers a lock only when it names a task of
268
+ its own graph that has already finished. Publication requires the run to have
269
+ an active owner but not the orchestrator's ownership capability, which never
270
+ leaves the parent,
271
+ so workers cannot advance node state or publish another task's output. Records
272
+ are attributed to the publishing task rather than authenticated: workers of one
273
+ run share the state root as they already share the checkout.
274
+
275
+ A run retains at most 256 coordination records. Publishing past that, or a
276
+ record over its size bound, fails explicitly and the worker continues without
277
+ it; as with retained runs, nothing is deleted automatically, so reclaiming the
278
+ capacity means deleting the run directory and its slot together.
279
+
280
+ ## Orchestrator tool
281
+
282
+ The extension reads `worker-graph.json` from Pi's agent directory (normally
283
+ `~/.pi/agent`); [`docs/worker-graph.example.json`](docs/worker-graph.example.json) is
284
+ a copyable starting point. The configuration is byte-bounded, rejects unknown
285
+ fields, and requires every worker profile to select its provider, model,
286
+ thinking level, and tool allowlist explicitly:
287
+
288
+ ```json
289
+ {
290
+ "schemaVersion": 1,
291
+ "maxRetainedRuns": 64,
292
+ "profiles": {
293
+ "worker": {
294
+ "provider": "anthropic",
295
+ "model": "claude-haiku-4-5",
296
+ "thinkingLevel": "medium",
297
+ "tools": ["read", "bash", "edit", "write"]
298
+ }
299
+ }
300
+ }
301
+ ```
302
+
303
+ Run state defaults to the `worker-graph` subdirectory of Pi's agent directory.
304
+ An optional `stateRoot` may be absolute or relative to the agent directory, but
305
+ the extension rejects the filesystem root and any path that is inside the target
306
+ checkout, including through an existing symlink.
307
+ `maxRetainedRuns` defaults to 64 and may be set from 1 through 256. Capacity is
308
+ a fixed set of slot files under `runs/slots`, so the limit is structural: at
309
+ most that many slots can exist, so at most that many runs can publish, and
310
+ concurrent creators are arbitrated by the filesystem rather than by counting —
311
+ an available slot is always claimed by exactly one of them. A slot is claimed by
312
+ hard-linking a record that is already complete on disk, so an interrupted
313
+ creation can never leave a slot that holds capacity without naming its owner.
314
+ Reaching the limit rejects the new graph; run state is never deleted
315
+ automatically.
316
+
317
+ A creation interrupted between claiming its slot and publishing its run leaves
318
+ the slot claimed. `/swarm runs` names everything holding capacity — each run
319
+ with its slot and creation time, a slot whose run was never published, and a
320
+ run whose slot is missing — and `/swarm delete <run-id>` removes a named run's
321
+ directory and its slot together. The same operations are exported as
322
+ `listRetainedRuns()` and `deleteRun()`. They are a command and not a parent
323
+ tool: deleting a run destroys the diagnostic state it was kept for, so it is an
324
+ operator's act and the model has no way to reach it. Cleanup is by name:
325
+ nothing decides on the operator's behalf which diagnostic state is worth
326
+ losing, so there is no deletion by age or by count. A run an orchestrator holds
327
+ is refused. The directory goes first and the slot second, so an interruption
328
+ strands a slot the store reports as reclaimable rather than leaving a published
329
+ run whose slot is missing: that disagreement stops the store admitting any new
330
+ work at all — rather than letting every waiting creator claim the same
331
+ apparently free capacity — until the two agree again.
332
+
333
+ Load the package and activate orchestration explicitly:
334
+
335
+ ```text
336
+ /swarm on
337
+ /swarm status
338
+ /swarm off
339
+ /swarm runs
340
+ /swarm usage <run-id>
341
+ /swarm delete <run-id>
342
+ ```
343
+
344
+ The `--swarm` extension flag enables the mode at startup. While the mode is
345
+ off, the `worker_graph` tool is excluded from the active tool set. Enabling the
346
+ mode snapshots the active tools, disables the built-in `bash`, `edit`, and
347
+ `write` tools in the parent, and persists the mode state in the Pi session.
348
+ Turning it off restores the exact snapshot. When the configuration names an
349
+ `orchestrator` block, the same snapshot covers the parent's model and thinking
350
+ level, and enabling the mode is refused outright unless the mode first knows
351
+ the identifiers that put the session back, the configured model is one Pi can
352
+ find, and its provider has configured authentication; a refusal applies
353
+ nothing. Navigating the session tree
354
+ restores whatever the target branch recorded, so `/swarm off` is never undone
355
+ by the flag that started the session. A tool set the extension could not read
356
+ back — more than 256 tools, or a tool name longer than 256 bytes — refuses to
357
+ enable the mode rather than suppressing parent tools it could not restore after
358
+ a reload. While a graph runs, the tool streams bounded status and returns
359
+ deterministic node statuses, aggregate usage, and a compact bounded projection
360
+ of worker reports. Worker transcripts never enter the parent model context;
361
+ projected report fields are marked as untrusted data inside a labeled block
362
+ that worker text cannot close.
363
+
364
+ Only one graph may run in a parent session at a time. Include validation as a
365
+ dependent worker task. After reviewing the shared checkout with the remaining
366
+ read-only tools, invoke another narrow graph for any repairs.
367
+
368
+ ## Cost and token accounting
369
+
370
+ Each worker's spend is recorded with the attempt that incurred it, in the node
371
+ output envelope: turns, input, output, cache-read and cache-write tokens, and
372
+ Pi's price for each. It is recorded for every terminal status,
373
+ including a task the run aborted, because an attempt that was stopped had still
374
+ spent what it spent by then. A failed worker reports its spend on
375
+ `TaskExecutionFailure`, and a timeout or an abort — where the runner discards
376
+ the executor's own outcome — still keeps the usage the executor reported.
377
+
378
+ Because it is persisted rather than only reported to the session, spend
379
+ survives the run. `/swarm usage <run-id>` sums a retained run and breaks it
380
+ down by task, and `readRunUsage()` returns the same thing to a library caller.
381
+ The total is derived from the outputs the run published, so it is correct for
382
+ an interrupted run; a task with no readable output is named as unaccounted
383
+ rather than counted as work that was free.
384
+
385
+ The `worker_graph` tool result carries the same numbers per node, so an
386
+ orchestrator can see which worker was expensive rather than only what the graph
387
+ cost in total. Its aggregate comes from live progress instead of the store, so
388
+ it still accounts for a task whose output could not be persisted.
389
+
390
+ Token counts come from the provider's telemetry; cost is Pi's pricing of those
391
+ tokens, so it is only as good as Pi's pricing table. Treat cost as an estimate
392
+ and tokens as the sturdier number.
393
+
394
+ Zero and unknown are kept apart. A worker whose provider reported no usage, or
395
+ reported a field that was not a usable number, records no usage at all rather
396
+ than a complete-looking set of zeros, and `/swarm usage` names it as
397
+ unaccounted. A task that never ran is reported separately again: it is not a
398
+ gap in the accounting. The live aggregate on the tool result is best-effort by
399
+ contrast — it projects the running figures whether or not they turned out to be
400
+ usable, which is what makes it a superset of what was persisted.
401
+
402
+ The runtime bounds tasks, concurrency, payload, output, context, and runtime,
403
+ but does not yet enforce a token or cost ceiling.
404
+
405
+ ## Planned runtime
406
+
407
+ The remaining runtime will add:
408
+
409
+ - a configured token or cost budget, enforced against recorded usage;
410
+ - persisted worker attempts and interrupted-run recovery behavior.
411
+
412
+ Writable workers will intentionally share one checkout. The runtime will not
413
+ create worktrees or perform automatic branches, commits, merges, stashes, resets,
414
+ restores, cleans, or pushes.
415
+
416
+ ## Development
417
+
418
+ Requires Node.js 22.19 or newer. Pi integration is currently tested against
419
+ `@earendil-works/pi-coding-agent` 0.85.1; the peer dependency follows Pi package
420
+ conventions and compatibility outside the tested version is not yet guaranteed.
421
+
422
+ For a local source checkout, install dependencies and build before loading the
423
+ package:
424
+
425
+ ```bash
426
+ npm install
427
+ npm run check
428
+ npm run build
429
+ pi -e /absolute/path/to/pi-worker-graph
430
+ ```
431
+
432
+ After version `0.1.0` is published, install that exact packaged build with:
433
+
434
+ ```bash
435
+ pi install npm:pi-worker-graph@0.1.0
436
+ ```
437
+
438
+ The npm artifact contains the compiled `dist/` tree. Do not install directly
439
+ from Git until the repository has a production-safe build lifecycle for Pi's
440
+ `--omit=dev` package installation path.
441
+
442
+ See [`docs/NEXT.md`](docs/NEXT.md) for current development status,
443
+ [`docs/PLAN.md`](docs/PLAN.md) for the implementation sequence, and
444
+ [`docs/DESIGN.md`](docs/DESIGN.md) for the proposed runtime contract.
445
+
446
+ ## Security
447
+
448
+ Pi packages execute with the permissions of the Pi process. Concurrent writable
449
+ workers are not sandboxed and can conflict even when tasks appear independent.
450
+ Review the source and use the runtime only in checkouts where this operating model
451
+ is acceptable. See [`SECURITY.md`](SECURITY.md).
452
+
453
+ ## License
454
+
455
+ [MIT](LICENSE)
package/SECURITY.md ADDED
@@ -0,0 +1,54 @@
1
+ # Security policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please report suspected vulnerabilities privately through the repository's
6
+ GitHub Security Advisory page. Do not include credentials, private source code,
7
+ or sensitive run artifacts in a public issue.
8
+
9
+ ## Security model
10
+
11
+ Pi packages execute with the permissions of the Pi process. This project does
12
+ not provide a sandbox for workers. Worker subprocesses inherit the parent
13
+ environment so Pi can resolve configured provider credentials, and writable tools
14
+ can access everything permitted to the parent operating-system user. Task content
15
+ is sent over stdin rather than process arguments, but provider requests and tool
16
+ activity still handle repository data. Repository `AGENTS.md`/`CLAUDE.md` context
17
+ files remain enabled intentionally and must be treated as trusted worker
18
+ instructions.
19
+
20
+ Worker profiles are loaded from the global Pi agent directory, not from the
21
+ target checkout. The configuration must not contain provider credentials or
22
+ other secrets. Progress projection retains only bounded task status, allowlisted
23
+ tool names, and numeric usage; worker messages, tool arguments, tool results, and
24
+ stderr are not forwarded into the parent model context. The final result carries
25
+ a bounded projection of worker reports inside a labeled block; every `<` in that
26
+ serialization is escaped, so worker-authored text cannot close the block and
27
+ address the parent as something other than untrusted data.
28
+
29
+ Run state defaults beneath the global Pi agent directory. Explicit state roots
30
+ cannot be the filesystem root or resolve into the target checkout through an
31
+ existing symlink; operators should still choose a private, access-controlled
32
+ directory outside repositories. Retained runs have a configurable hard count
33
+ limit enforced by atomically claimed capacity slots, and reaching it rejects
34
+ new work rather than deleting prior state. No elapsed-time heuristic can release
35
+ a claimed slot, so a slow creator is never displaced by a second one, and a
36
+ published run whose slot is missing stops the store from admitting new work
37
+ instead of handing the same free capacity to several creators at once.
38
+
39
+ Swarm mode removes Pi's built-in `bash`, `edit`, and `write` tools from the
40
+ parent tool set and restores the exact pre-mode snapshot on exit. Pi does not
41
+ label arbitrary extension tools as read-only or writable, so separately
42
+ installed third-party tools remain the operator's responsibility.
43
+
44
+ Workers are started without a command interpreter: the executable is Pi's own
45
+ CLI entry point, resolved through this package's dependency on Pi, and every
46
+ argument is passed through `spawn` with `shell: false`. Configured provider and
47
+ model values are restricted to a plain-identifier allowlist, and Pi's own
48
+ `PI_CODING_AGENT` variable is never treated as proof of what the current process
49
+ is, because Pi exports it to everything it launches.
50
+
51
+ Review the source before installation, use least-privilege credentials, and run
52
+ concurrent writable workers only in a checkout where overlapping edits are
53
+ acceptable. Process isolation, tool allowlists, and disabled child session
54
+ persistence reduce accidental coupling; they are not security boundaries.
@@ -0,0 +1,25 @@
1
+ import type { PiOrchestratorProfile, PiWorkerProfile } from "./pi-subprocess.js";
2
+ export declare const WORKER_GRAPH_CONFIG_FILENAME = "worker-graph.json";
3
+ export declare const WORKER_GRAPH_DEFAULT_STATE_DIRECTORY = "worker-graph";
4
+ export declare const WORKER_GRAPH_CONFIG_MAX_BYTES: number;
5
+ export type WorkerGraphConfigurationErrorCode = "missing" | "too_large" | "malformed" | "invalid" | "unsafe_state_root";
6
+ export declare class WorkerGraphConfigurationError extends Error {
7
+ readonly code: WorkerGraphConfigurationErrorCode;
8
+ constructor(code: WorkerGraphConfigurationErrorCode);
9
+ }
10
+ export interface WorkerGraphConfiguration {
11
+ readonly stateRoot: string;
12
+ readonly maxRetainedRuns: number;
13
+ /**
14
+ * The parent session's own model while the mode is active. Absent leaves the
15
+ * session exactly as the operator started it.
16
+ */
17
+ readonly orchestrator?: PiOrchestratorProfile;
18
+ readonly profiles: Readonly<Record<string, PiWorkerProfile>>;
19
+ }
20
+ export interface LoadWorkerGraphConfigurationOptions {
21
+ readonly agentDirectory: string;
22
+ readonly workingDirectory: string;
23
+ }
24
+ export declare function loadWorkerGraphConfiguration(options: LoadWorkerGraphConfigurationOptions): Promise<WorkerGraphConfiguration>;
25
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,qBAAqB,EACrB,eAAe,EAChB,MAAM,oBAAoB,CAAC;AAO5B,eAAO,MAAM,4BAA4B,sBAAsB,CAAC;AAChE,eAAO,MAAM,oCAAoC,iBAAiB,CAAC;AACnE,eAAO,MAAM,6BAA6B,QAAY,CAAC;AAWvD,MAAM,MAAM,iCAAiC,GACzC,SAAS,GACT,WAAW,GACX,WAAW,GACX,SAAS,GACT,mBAAmB,CAAC;AAYxB,qBAAa,6BAA8B,SAAQ,KAAK;IACtD,QAAQ,CAAC,IAAI,EAAE,iCAAiC,CAAC;gBAErC,IAAI,EAAE,iCAAiC;CAKpD;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC;CAC9D;AAED,MAAM,WAAW,mCAAmC;IAClD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAyJD,wBAAsB,4BAA4B,CAChD,OAAO,EAAE,mCAAmC,GAC3C,OAAO,CAAC,wBAAwB,CAAC,CA2BnC"}