@chrok/braid 0.1.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/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
4
+
5
+ No changes yet.
6
+
7
+ ## 0.1.0 — 2026-09-27
8
+
9
+ Initial public release. Publication is tracked in
10
+ [GitHub releases](https://github.com/Epsirom/braid/releases).
11
+
12
+ - Framework-independent DAG runtime with conditional decisions, bounded
13
+ concurrency, explicit joins, failure propagation, and isolated node context.
14
+ - Caller cancellation, node/graph deadlines, immutable events, partial results,
15
+ model overrides, and provider-reported token accounting.
16
+ - OpenAI-compatible Chat Completions runner with validated decision tool calls.
17
+ - Pi background jobs, completion reminders, cancellation, guarded worker tools,
18
+ and a live flow panel. The Pi package includes the matching core runtime.
19
+ - Core-managed Git worktrees, checkpoint/backup refs, agent-driven merge nodes,
20
+ failure context on unconditional edges, and optional Pi tool budgets.
21
+ - Clean package builds, isolated tarball checks, CI for Node and supported
22
+ operating systems, and an npm trusted-publishing workflow.
23
+ - Install guides, runnable offline examples, compatibility and resource-limit
24
+ documentation, scheduler benchmarks, and contribution/security policies.
25
+
26
+ Limitations: no persistent jobs, retries, token/spend budgets, streaming core
27
+ responses, graph mutation, nested runs, or sandboxing. See the roadmap and
28
+ resource-limit documentation before using it as a service.
@@ -0,0 +1,22 @@
1
+ # Code of conduct
2
+
3
+ Participants should be able to ask questions, disagree, and contribute without
4
+ harassment. Be respectful, assume good faith, focus criticism on the work, and
5
+ accept feedback. Welcome people regardless of background, identity, experience,
6
+ or ability. English and Chinese contributions are welcome.
7
+
8
+ Harassment, threats, discriminatory remarks, unwanted sexual attention, doxxing,
9
+ and repeated personal attacks are not acceptable. Do not publish another
10
+ person's private information without permission.
11
+
12
+ This applies to project issues, pull requests, reviews, and other spaces where
13
+ participants represent Braid. The maintainer may edit or remove harmful content,
14
+ warn participants, limit interaction, or ban repeated or serious violations.
15
+ Actions should be proportionate to the behavior and its impact.
16
+
17
+ Report concerns to [Epsirom](https://github.com/Epsirom) using a contact method on
18
+ the GitHub profile. Do not post sensitive reports publicly. If no private contact
19
+ is available, or a report concerns the maintainer, use GitHub's Report abuse
20
+ feature. Reports are handled discreetly, with information shared only as needed
21
+ to address the incident. This is a volunteer project; response times are not
22
+ guaranteed.
@@ -0,0 +1,61 @@
1
+ # Contributing to Braid
2
+
3
+ Bug reports, documentation fixes, small examples, and focused runtime or adapter
4
+ improvements are welcome. Start with an issue before a large API change. Explain
5
+ the user problem and how it fits the [scope and roadmap](ROADMAP.md).
6
+
7
+ ## Development
8
+
9
+ Use Node.js 22.19+ and npm. From a fresh checkout:
10
+
11
+ ```sh
12
+ git clone https://github.com/Epsirom/braid.git
13
+ cd braid
14
+ npm ci
15
+ npm ci --prefix integrations/pi
16
+ npm run verify
17
+ ```
18
+
19
+ There are two packages and two lockfiles. The core has no runtime dependencies;
20
+ Pi's dependencies belong in `integrations/pi`. Update and commit the corresponding
21
+ lockfile when changing a dependency. Do not commit generated `dist` directories,
22
+ tarballs, credentials, or provider output containing private data.
23
+
24
+ `verify` type-checks both packages, runs deterministic tests and offline examples,
25
+ then builds tarballs and installs them into a temporary consumer outside the
26
+ checkout. That last check needs registry access for Pi dependencies but never
27
+ calls a model. `npm test` is the fast core-only loop; `npm run test:pi` tests Pi.
28
+ `npm run bench` measures the scheduler without a provider.
29
+
30
+ ## Changes and reviews
31
+
32
+ - Keep the TypeScript strict checks passing. Follow nearby code and the
33
+ repository's two-space formatting; use explicit public types.
34
+ - For behavior changes, add a regression test that fails before the change.
35
+ Use deferred promises or fake runners instead of live models or long sleeps.
36
+ - Preserve decision-tool validation, join semantics, cancellation, context
37
+ isolation, partial results, and provider usage accounting.
38
+ - Explain the observable change, why it is needed, and how it was verified in
39
+ the PR. Update relevant docs and the Unreleased changelog for user-facing work.
40
+ - Read [compatibility](docs/compatibility.md) before changing public fields or
41
+ error/event shapes. Discuss breaking changes before implementing them.
42
+
43
+ Small documentation fixes do not need extra tests. No CLA is required;
44
+ contributions are provided under this project's MIT license. Do not submit code
45
+ or data you do not have permission to share.
46
+
47
+ ## Reporting and maintenance
48
+
49
+ Use [issues](https://github.com/Epsirom/braid/issues) for reproducible bugs,
50
+ questions, and feature proposals. Include package/Node/Pi versions, a minimal
51
+ graph, expected vs actual behavior, and redacted errors. Never include API keys,
52
+ private repository content, or a full model transcript unless safe to publish.
53
+ Report vulnerabilities through [SECURITY.md](SECURITY.md).
54
+
55
+ [Epsirom](https://github.com/Epsirom) maintains the project and reviews releases
56
+ and public API changes. Support is best effort, with no guaranteed response time.
57
+ For conduct concerns, contact the maintainer through a contact method listed on
58
+ their GitHub profile; use GitHub's Report abuse feature if private contact is
59
+ unavailable or the maintainer is involved. See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
60
+
61
+ Maintainers: follow the [release guide](docs/releasing.md).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Epsirom
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,513 @@
1
+ # Braid
2
+
3
+ Braid is a small execution runtime for dynamically constructed graphs of isolated
4
+ model invocations. A parent submits a complete DAG in one call; Braid resolves
5
+ routing and dependencies, runs independent nodes concurrently, and returns the
6
+ successful execution-terminal outputs. It is an agent primitive, not a workflow
7
+ builder.
8
+
9
+ [![CI](https://github.com/Epsirom/braid/actions/workflows/ci.yml/badge.svg)](https://github.com/Epsirom/braid/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
+
12
+ **v0.1:** Experimental, TypeScript, Node.js 22+, ESM, no runtime dependencies.
13
+ The core has no Pi, provider SDK, or framework dependency.
14
+
15
+ ## Why Braid?
16
+
17
+ For a code review, run correctness and test-coverage analysis independently,
18
+ then pass both results to a synthesis node. Add a decision when some tasks need
19
+ only a brief answer. Braid handles dependency readiness, conditional skips,
20
+ failed joins, per-node context, cancellation, and accounting around those calls.
21
+
22
+ ```mermaid
23
+ flowchart LR
24
+ route{Choose depth} -->|detailed| correctness[Correctness]
25
+ route -->|detailed| tests[Test coverage]
26
+ correctness --> review[Final review]
27
+ tests --> review
28
+ route -->|brief| brief[Short answer]
29
+ ```
30
+
31
+ Use it when independent reasoning branches and explicit handoffs help. A direct
32
+ model call or `Promise.all` is enough for a simple answer or independent calls
33
+ without routing or joins. Braid adds no persistence, workflow editor, or agent
34
+ framework. [Runnable examples](docs/examples.md) show the tradeoffs.
35
+
36
+ ## Install in your application
37
+
38
+ ```sh
39
+ npm install @chrok/braid
40
+ ```
41
+
42
+ Save this as `example.mjs` and run `node example.mjs` (no API key needed):
43
+
44
+ ```js
45
+ import { braid } from "@chrok/braid";
46
+ import { mkdtemp, rm } from "node:fs/promises";
47
+ import { tmpdir } from "node:os";
48
+ import { join } from "node:path";
49
+
50
+ // A non-Git directory keeps this text-only example outside workspace management.
51
+ const cwd = await mkdtemp(join(tmpdir(), "braid-hello-"));
52
+ try {
53
+ const result = await braid({
54
+ goal: "Try one isolated invocation.",
55
+ nodes: [{ type: "execute", id: "answer", prompt: "Say hello." }],
56
+ edges: [],
57
+ }, {
58
+ cwd,
59
+ runner: async () => ({ output: "Hello from Braid." }),
60
+ });
61
+ console.log(result.terminalOutputs.answer.output);
62
+ // Hello from Braid.
63
+ } finally {
64
+ await rm(cwd, { recursive: true, force: true });
65
+ }
66
+ ```
67
+
68
+ For a live provider, use the adapter in the API example below. Installation and
69
+ running the example above do not make model requests. In a Git checkout, Braid
70
+ creates node worktrees and may append a merge agent that can integrate changes
71
+ into the source checkout. Read [workspace behavior](#worktrees-and-merge-agents)
72
+ before running a custom adapter against a repository.
73
+
74
+ ## Install in Pi
75
+
76
+ With Pi 0.85.1 and Node.js 22.19+:
77
+
78
+ ```sh
79
+ pi install npm:@chrok/pi-braid
80
+ ```
81
+
82
+ Run `/reload`, ask Pi to analyze a task with Braid, and open `/braid` to inspect
83
+ the job. The extension includes the matching core runtime; no checkout is needed.
84
+
85
+ ![Braid Pi flow panel with an offline example](docs/assets/pi-panel.svg)
86
+
87
+ This snapshot uses the actual panel renderer and fake responses. See the
88
+ [Pi guide](integrations/pi/README.md) for background jobs, cancellation, and local
89
+ installation, and [compatibility](docs/compatibility.md) for the tested versions.
90
+
91
+ ## Develop from source
92
+
93
+ ```sh
94
+ git clone https://github.com/Epsirom/braid.git
95
+ cd braid
96
+ npm ci
97
+ npm run check
98
+ npm test
99
+ npm run build
100
+ npm run demo
101
+ ```
102
+
103
+ The demo uses a deterministic fake model runner and makes no network calls. To
104
+ run the same graph with an OpenAI-compatible Chat Completions endpoint:
105
+
106
+ ```sh
107
+ # Set OPENAI_API_KEY and BRAID_MODEL in your environment first.
108
+ npm run demo -- --live
109
+ ```
110
+
111
+ `OPENAI_BASE_URL` optionally changes the API root (for example,
112
+ `https://your-provider.example/v1`). Live mode makes billable model requests.
113
+ The adapter requires a model with function/tool calling support. No live provider
114
+ is needed for the test suite; its HTTP requests are intercepted in tests.
115
+
116
+ ## API
117
+
118
+ After installing the package, submit the graph in one call;
119
+ configuration and the trusted provider adapter are separate from the graph data:
120
+
121
+ ```ts
122
+ import { braid } from "@chrok/braid";
123
+ import { createOpenAICompatibleRunner } from "@chrok/braid/adapters/openai";
124
+
125
+ const apiKey = process.env.OPENAI_API_KEY;
126
+ const model = process.env.BRAID_MODEL;
127
+ if (!apiKey || !model) throw new Error("Set OPENAI_API_KEY and BRAID_MODEL");
128
+
129
+ const result = await braid({
130
+ goal: "Assess a proposed change and give a recommendation.",
131
+ nodes: [
132
+ {
133
+ type: "decision", id: "route", prompt: "Choose a brief answer or a detailed assessment.",
134
+ choices: ["brief", "detailed"], model: process.env.BRAID_ROUTER_MODEL ?? model,
135
+ },
136
+ { type: "execute", id: "benefits", prompt: "Assess the potential benefits." },
137
+ { type: "execute", id: "risks", prompt: "Assess the risks and unknowns." },
138
+ { type: "execute", id: "answer", prompt: "Use the available context to give a recommendation." },
139
+ ],
140
+ edges: [
141
+ { from: "route", to: "answer", choice: "brief" },
142
+ { from: "route", to: "benefits", choice: "detailed" },
143
+ { from: "route", to: "risks", choice: "detailed" },
144
+ { from: "benefits", to: "answer" },
145
+ { from: "risks", to: "answer" },
146
+ ],
147
+ }, {
148
+ runner: createOpenAICompatibleRunner({ apiKey }),
149
+ defaultModel: model,
150
+ maxConcurrency: 4,
151
+ nodeTimeoutMs: 60_000,
152
+ graphTimeoutMs: 300_000,
153
+ onEvent: event => console.log(`[${event.type}]`, event),
154
+ });
155
+
156
+ console.log(result.status, result.terminalOutputs);
157
+ ```
158
+
159
+ The detailed route starts `benefits` and `risks` concurrently. The brief route
160
+ skips both, propagates their inactivity, and runs `answer` with only `route`'s
161
+ output. The graph has no special fork, branch, or join nodes.
162
+
163
+ ### Graph schema
164
+
165
+ ```ts
166
+ type BraidNode =
167
+ | { type: "execute"; id: string; prompt: string; model?: string }
168
+ | { type: "decision"; id: string; prompt: string;
169
+ choices: readonly string[]; model?: string }
170
+ | { type: "merge"; id: string; prompt?: string; model?: string };
171
+
172
+ type Edge = { from: string; to: string; choice?: string };
173
+ type BraidInput = { goal: string; nodes: readonly BraidNode[]; edges: readonly Edge[] };
174
+ ```
175
+
176
+ IDs are unique, non-empty strings. Prompts, goals, models, and choices must be
177
+ non-empty strings when present. Decision choices must be non-empty and unique.
178
+ Unknown fields, unsupported node types, missing references, duplicate exact
179
+ edges, and cycles are rejected. Cycles are rejected even if a decision might
180
+ make them inactive. Disconnected components are allowed; every root runs.
181
+ Distinct choices may connect the same node pair. Choices need not all have
182
+ outgoing edges, and decisions may themselves be terminal.
183
+
184
+ `validateGraph(input)` is also exported for validation without execution. It and
185
+ `braid` throw `GraphValidationError` for invalid graphs, before invoking a model.
186
+ Invalid runtime options throw `TypeError`. Execution failures return a result
187
+ with `status: "failed"` instead of discarding the run's successful outputs.
188
+
189
+ ### Options
190
+
191
+ | Option | Default | Meaning |
192
+ | --- | --- | --- |
193
+ | `runner` | Required | A fresh, isolated invocation for each call |
194
+ | `defaultModel` | Adapter default | Overridden by each node's `model` |
195
+ | `cwd` | `process.cwd()` | Source checkout for core-managed worktrees; non-Git directories grant read-only capabilities |
196
+ | `maxConcurrency` | `4` | Maximum simultaneous runtime-managed node invocations; positive integer |
197
+ | `nodeTimeoutMs` | `60_000` | Separate deadline for each node, starting when it runs (not while queued) |
198
+ | `graphTimeoutMs` | `300_000` | Whole execution deadline, including node queueing; starts after validation |
199
+ | `signal` | None | Caller cancellation signal; aborts running nodes and marks queued nodes cancelled |
200
+ | `onEvent` | None | Live observer for graph/node creation, readiness, starts, handoffs, completions, skips, failures, and graph completion |
201
+
202
+ Timeouts must be positive finite milliseconds, at most `2_147_483_647`, or
203
+ `Infinity` to disable that deadline. Set both timeouts to `Infinity` to run
204
+ without a time limit; caller cancellation still works.
205
+
206
+ ## Scheduling and routing semantics
207
+
208
+ Node states are explicit:
209
+
210
+ ```text
211
+ pending -> runnable -> running -> completed | failed
212
+ pending -> skipped
213
+ pending | runnable -> skipped (graph timeout)
214
+ ```
215
+
216
+ Edges are resolved from their source node's state:
217
+
218
+ | Source result | Outgoing edge state |
219
+ | --- | --- |
220
+ | Not yet finished | Unresolved |
221
+ | Completed, unlabelled edge | Active |
222
+ | Completed decision, matching choice | Active |
223
+ | Completed decision, nonmatching choice | Inactive |
224
+ | Skipped because all inputs were inactive | Inactive |
225
+ | Failed, unlabelled edge | Active (passes error and available output/workspace) |
226
+ | Failed decision, choice-labelled edge | Blocked |
227
+ | Skipped because of a failed dependency | Blocked |
228
+
229
+ Only decision nodes may have choice-labelled outgoing edges. Unlabelled edges
230
+ are unconditional, including edges from decisions. A single choice can activate
231
+ any number of edges. Choice routing requires successful decision completion.
232
+ A decision must call `decide` exactly once with exactly one declared choice;
233
+ missing, invalid, or repeated calls fail it. Catching a tool validation error
234
+ inside an adapter does not turn that invocation into a success.
235
+
236
+ A pending node waits until **all incoming edges are resolved**. Then:
237
+
238
+ 1. Any blocked incoming edge makes it `skipped: upstream_failed`.
239
+ 2. If it has incoming edges but none are active, it becomes `skipped: inactive`.
240
+ 3. Otherwise it becomes `runnable` (including roots, which have no inputs).
241
+
242
+ Failures propagate as context through unconditional edges, allowing successors
243
+ and merge agents to inspect errors and recover partial work. Failed decisions
244
+ cannot activate choice-labelled edges; their unconditional successors can run.
245
+ Skip propagation uses topological order. There are no automatic retries. The
246
+ graph still reports failure if any node fails, even if a later node recovers its
247
+ work successfully.
248
+
249
+ ## Execution events
250
+
251
+ The runtime keeps an immutable `events` log on every execution result and can
252
+ stream the same events through `options.onEvent`. Events are numbered and
253
+ timestamped. The log is diagnostic data and does not alter scheduling; observer
254
+ exceptions and rejected promises are ignored. Event payloads are frozen before
255
+ being retained and delivered.
256
+
257
+ The event sequence includes:
258
+
259
+ - `graph_created`, `node_created`, and `edge_created` when the submitted DAG is
260
+ admitted; an appended final merge emits its own node/edge creation events.
261
+ - `node_runnable` and `node_started` when scheduling admits a node.
262
+ - `workspace_updated` for Git workspace preparation, checkpointing, and cleanup.
263
+ - `handoff` for every direct predecessor output passed to a downstream node,
264
+ including the upstream decision when present.
265
+ - `node_completed`, `node_skipped`, and `node_failed`, including output,
266
+ decision, model, usage, latency, skip reason, or error where applicable.
267
+ A node start/completion/failure event is emitted only once the corresponding
268
+ transition is admitted; a provider call that expires before admission has no
269
+ `node_started` event.
270
+ - `graph_completed` with execution terminal IDs, or `graph_failed` with the
271
+ representative error and any successful terminal IDs.
272
+
273
+ `node_completed` and `node_failed` include node latency; `node_completed` also
274
+ includes the selected decision and reported usage when available. Event output
275
+ is diagnostic context and may be previewed by an adapter; `BraidResult.events`
276
+ retains the complete event payloads.
277
+
278
+ The core event stream is intentionally a log, not a second control API. It does
279
+ not permit graph mutation or runtime intervention. A Pi adapter can use it to
280
+ render live topology, handoffs, failures, and active nodes without reconstructing
281
+ scheduler state from final results. Tool selection remains the responsibility of
282
+ the host agent; the optional Pi adapter supplies explicit proactive-use guidance
283
+ so Braid is considered for complex multi-branch reasoning without forcing it for
284
+ every prompt. In the Pi adapter, Git nodes can inspect and edit individual
285
+ worktrees; nodes outside Git stay read-only. Merge agents handle integration; the parent reviews results and runs shell commands and tests.
286
+
287
+ ## Context isolation and model runners
288
+
289
+ The core accepts a `ModelRunner` function with this contract:
290
+
291
+ ```ts
292
+ import type { ModelRequest } from "@chrok/braid";
293
+
294
+ type ModelRunner = (request: ModelRequest) => Promise<{
295
+ output: string;
296
+ model?: string;
297
+ usage?: { inputTokens: number; outputTokens: number };
298
+ }>;
299
+ ```
300
+
301
+ Each request contains the goal, node, resolved model, predecessor outputs,
302
+ execution IDs, and an abort signal. Decision nodes additionally receive
303
+ `request.decide(choice)`. Expose that callback as an actual model tool; do not
304
+ infer decisions by parsing the model's prose. Core assigns `request.workspace`,
305
+ provides local `request.git(args, input?)` operations in Git repositories, and
306
+ provides `request.merge.sources` and `request.merge.finish(dispositions)` to
307
+ merge agents. Adapters must enforce workspace capabilities and wrap mutating
308
+ file tools in `request.withWorkspaceWrite(operation)`, so cleanup waits for
309
+ in-flight writes and rejects later writes. Core Git mutations use this barrier.
310
+ The included Pi adapter provides guarded `write`/`edit` alongside its read tools.
311
+ Outside Git, adapters must provide read-only capabilities. Pi never provides
312
+ `bash`, `powershell`, or a test runner to nodes.
313
+
314
+ `request.predecessors` contains direct active predecessors in incoming-edge
315
+ order, including failures on unconditional edges with an `error` field. Each source appears once:
316
+
317
+ ```json
318
+ [{ "nodeId": "route", "output": "Use a detailed comparison.", "decision": "detailed", "model": "router" }]
319
+ ```
320
+
321
+ There is no parent conversation, global transcript, or automatic transitive
322
+ history. Each invocation gets fresh node/context objects, so modifying them
323
+ cannot affect the graph, another node, or recorded results. Both the submitted
324
+ input and options are snapshotted before asynchronous execution. Core manages
325
+ worktrees for all adapters; adapters decide which tools expose those capabilities.
326
+ Read tools follow the host filesystem permissions and are not a security sandbox.
327
+
328
+ ### Worktrees and merge agents
329
+
330
+ In a Git checkout, execute and decision nodes receive detached worktrees under
331
+ `os.tmpdir()/braid-workspaces-*/<unique-id>`. The first node captures tracked
332
+ staged/unstaged changes, deletions, and non-ignored untracked files with a temporary
333
+ index. Snapshot creation preserves the source index, branch, and files. Ignored
334
+ files are not copied, and submodules are not initialized or recursively captured.
335
+ Pi rejects writes inside submodules. If a custom runner populates one, core
336
+ reports cleanup failure and retains the worktree rather than losing those files.
337
+ Empty repositories are supported. Nodes share this baseline until a merge ends;
338
+ subsequent nodes snapshot the current source checkout. Relative working directories
339
+ are preserved. Code changes do not implicitly flow into successor worktrees.
340
+
341
+ Add `{ type: "merge", id: "integrate" }` with incoming edges from any number of
342
+ sources. The merge agent receives predecessor errors, workspace paths, and Git
343
+ checkpoint refs and operates directly in the invoking checkout. **Core does not
344
+ run merge, cherry-pick, or apply automatically.** The agent reviews each source,
345
+ chooses which changes to integrate and how, resolves conflicts, then calls the
346
+ `finish_merge` tool with exactly one disposition and reason per source:
347
+
348
+ ```json
349
+ { "dispositions": [
350
+ { "nodeId": "implementation", "disposition": "integrated", "reason": "Cherry-picked the reviewed checkpoint" },
351
+ { "nodeId": "alternative", "disposition": "discarded", "reason": "The selected implementation supersedes this alternative" }
352
+ ] }
353
+ ```
354
+
355
+ Each merge request includes bounded changed-file lists, diff statistics and diff
356
+ previews in `mergeSources[].changes`, plus the invoking checkout's dirty status.
357
+ These are inspection aids; the agent still chooses every integration operation.
358
+ The `finish_merge` schema lists only the current source IDs and requires exactly
359
+ one decision per source. Invalid calls report missing, unexpected and duplicate
360
+ IDs so the agent can correct the call.
361
+
362
+ The model-facing `git` tool takes a `command` from the node's allowed command
363
+ enum and a separate `args` array. For example, `{"command":"show","args":["REF:path"]}`.
364
+ The programmatic `ModelRequest.git` API continues to accept the complete argument
365
+ array. Rejected commands include relevant supported alternatives; Braid never
366
+ silently substitutes a different Git operation. A first argument identical to
367
+ `command` is rejected before execution: `{ "command": "status", "args": ["status"] }`
368
+ would otherwise silently query a path named `status`. Use `args: ["--short"]`
369
+ for the full status, or `args: ["--", "status"]` for an intentional path filter;
370
+ same-named branches can use a full ref such as `refs/heads/diff`.
371
+
372
+ `archived` means integration failed. A missing finish call, unresolved conflicts,
373
+ or any archived source fails the merge node. Once the agent ends, core removes
374
+ its predecessor worktrees. Every removed source retains a checkpoint ref,
375
+ including intentionally discarded changes and ignored node output files. A
376
+ later consumer can inspect a removed source through `git show <checkpointRef>`.
377
+ Core also records `backupRef` for the source checkout before each merge agent.
378
+
379
+ When declared nodes settle and worktrees remain, core appends an ordinary merge
380
+ agent named `__braid_merge__` (with a suffix if needed), using the run's default
381
+ model. It appears in results, events, usage, and terminal outputs. Merge nodes
382
+ are exclusive within a graph and serialized per source checkout across runs in
383
+ the same process. Avoid concurrent external edits to that checkout while merging;
384
+ this lock does not coordinate other processes or the parent editor.
385
+
386
+ Cancellation or graph timeout prevents new merge agents from starting. Core waits
387
+ for tracked writes, archives remaining work, and removes its worktrees. Merge
388
+ agent failure follows the same archive/cleanup path. It does not reset the source
389
+ checkout: partial integration or Git conflict state may remain for review, with
390
+ `backupRef` available for recovery. Filesystem/Git cleanup errors are reported as
391
+ `CLEANUP_FAILED` with retained workspace paths; a process crash cannot run cleanup.
392
+
393
+ `result.workspaces` and `node.workspace` report paths, states, reasons, and refs.
394
+ A cleaned worktree path is historical; use `checkpointRef` to recover its contents:
395
+
396
+ ```sh
397
+ git show <checkpointRef>:path/to/file
398
+ git diff <snapshotCommit> <checkpointRef>
399
+ ```
400
+
401
+ Recovery refs live under `refs/braid/checkpoints/` and `refs/braid/merge-backups/`.
402
+ After reviewing them, remove a particular ref with `git update-ref -d <ref>`.
403
+ They preserve recoverable Git objects without retaining worktree directories.
404
+
405
+ **The adapter is a trust boundary, not a security sandbox.** It must avoid shared
406
+ conversation state, expose only its declared capabilities, and forward `signal` to its provider.
407
+ The core never gives the model arbitrary code execution or a recursive Braid
408
+ tool. An optional Pi adapter translates this same contract without changing the
409
+ runtime; the core v0.1 package does not depend on Pi. See
410
+ [`integrations/pi/README.md`](integrations/pi/README.md) for installation and testing.
411
+
412
+ The included OpenAI-compatible adapter uses fresh Chat Completions contexts,
413
+ a strict `decide({ choice })` tool and one tool-free continuation for decisions.
414
+ Merge nodes use a local `git` / `finish_merge` tool loop; ordinary OpenAI nodes
415
+ have no filesystem tools. Pi exposes read and guarded write tools plus these
416
+ core Git/merge tools. Tool errors go back to merge agents for recovery. Both
417
+ adapters sum usage across their model calls and forward cancellation.
418
+ Pi writes reject external paths, Git metadata, symlinks, hard links, and special
419
+ files. These checks are not an OS sandbox against concurrent filesystem attacks.
420
+ See the [Pi filesystem capabilities](integrations/pi/README.md#node-filesystem-capabilities).
421
+
422
+ Pi tool and time budgets are unlimited by default. Its `options.maxToolRounds`
423
+ and `options.maxToolCalls` can impose positive integer limits per node; counts
424
+ include `decide` and rejected requests. Exceeding either limit fails the node
425
+ before executing the over-budget batch. Returning final text at the limit is
426
+ allowed. See the [Pi budget options](integrations/pi/README.md#node-filesystem-capabilities)
427
+ for configuration, including `nodeTimeoutMs` and `graphTimeoutMs`.
428
+
429
+ For finite budgets, Pi inserts a system reminder with remaining tool and time
430
+ budgets before every model call. The OpenAI-compatible adapter also refreshes
431
+ finite time-budget reminders before each request. The core supplies optional
432
+ `request.deadlines` on the `performance.now()` clock for adapters to calculate
433
+ remaining node and shared graph time. Reminders do not extend hard limits or
434
+ interrupt a model response already in progress. The core API's default timeouts
435
+ remain 60 seconds per node and 5 minutes per graph.
436
+
437
+ ## Results, timeouts, and accounting
438
+
439
+ `BraidResult` contains:
440
+
441
+ - `status`: `completed` or `failed`.
442
+ - `terminalOutputs`: `{ [nodeId]: { output, decision?, model? } }` for completed
443
+ nodes with **no active outgoing edges in this execution**. This includes a
444
+ decision selecting a choice with no successor. A completed node does not
445
+ become terminal merely because its active successor failed or was skipped.
446
+ - `nodes`: all node states plus available output, decision, model, usage, error,
447
+ skip reason, start/end timestamps (Unix milliseconds), and latency in ms.
448
+ Skipped nodes have no start time or latency. Nodes without a valid
449
+ response have no output. A failed decision may retain its text and selected
450
+ choice for debugging; choice-labelled edges still remain blocked.
451
+ - `workspaces`: Git workspace states and recovery refs, including cleaned sources.
452
+ - `events`: the immutable execution log described above. `onEvent` observes live
453
+ copies of the same state transitions while the run is in progress.
454
+ - `metadata`: run/root identity, timestamps, monotonic latency, summed reported
455
+ token usage, and `usageReportedNodes`. Missing usage is unknown, not proof of
456
+ zero consumption. Usage counts only what the runner actually returns; a
457
+ timeout or provider error may leave billable usage unavailable.
458
+ - `error`: one representative error when failed; `nodes` retains all errors.
459
+
460
+ A node timeout marks the running node `failed: NODE_TIMEOUT`; unconditional
461
+ successors can consume its error and partial workspace. A graph timeout marks running nodes `failed: GRAPH_TIMEOUT`,
462
+ skips queued/pending nodes with `graph_timeout`, and retains already completed
463
+ terminal outputs. Timers are cleared when no longer needed.
464
+
465
+ Timeouts abort the invocation signal and stop waiting for the model even if it
466
+ ignores cancellation. Workspace setup, tracked writes, and cleanup are awaited
467
+ to avoid deleting work still being written; this may extend total wall time. Late settlements cannot change the returned results, and late
468
+ rejections are observed. JavaScript cannot forcibly preempt synchronous work or
469
+ stop an uncooperative remote request: providers must honor cancellation to stop
470
+ resource consumption. Concurrency limits cover runtime-managed invocations;
471
+ uncancelled provider work after timeout can outlive a slot.
472
+
473
+ ## Internal architecture and scope
474
+
475
+ - [`src/types.ts`](src/types.ts): public graph, provider, and result types.
476
+ - [`src/validate.ts`](src/validate.ts): strict validation, graph snapshot,
477
+ dependency indexes, and iterative DAG validation.
478
+ - [`src/runtime.ts`](src/runtime.ts): edge resolution, explicit state transitions,
479
+ bounded concurrent scheduling, invocation deadlines, execution events, and result accounting.
480
+ - [`src/workspaces.ts`](src/workspaces.ts): Git snapshots, checkpoint refs, merge
481
+ serialization, local Git tools, and worktree cleanup.
482
+ - [`src/adapters/openai.ts`](src/adapters/openai.ts): optional provider translation.
483
+ - [`integrations/pi/`](integrations/pi/): thin Pi model-registry/tool adapter, Mermaid graph renderer, and live execution renderer (`display.ts`).
484
+ - [`test/`](test/): deterministic scheduling, execution-event, and intercepted HTTP/tool tests.
485
+
486
+ There is one in-memory execution context per run, plus a process-local mutex
487
+ per source checkout for merge agents. Worktree registration and removal are
488
+ serialized per common Git directory within the process; model calls remain
489
+ concurrent. These locks do not coordinate other processes. `rootRunId` equals
490
+ `runId` in v0.1. Centralized invocation admission and
491
+ usage aggregation leave places to thread a shared root budget in a future
492
+ nested-run implementation; **nested runs and shared budget enforcement are not
493
+ implemented**. The current scheduler deliberately rescans a small DAG after
494
+ completions; `onEvent` is an observer for diagnostics and visualization, not a
495
+ scheduler event bus.
496
+
497
+ Out of scope: loops, arbitrary code nodes, persistent workflows, saved templates,
498
+ resuming saved runs, human approval, editing UI, user-directed graph mutation,
499
+ and recursive Braid calls from model nodes.
500
+
501
+ ## Contributing and project status
502
+
503
+ Start with [CONTRIBUTING.md](CONTRIBUTING.md) for setup and verification,
504
+ [ROADMAP.md](ROADMAP.md) for scope, and [CHANGELOG.md](CHANGELOG.md) for changes.
505
+ See [compatibility](docs/compatibility.md), [resource limits](docs/resource-limits.md),
506
+ and [scheduler benchmarks](docs/benchmark.md) before adopting Braid for a service.
507
+ Questions and bugs belong in [GitHub issues](https://github.com/Epsirom/braid/issues);
508
+ report vulnerabilities privately as described in [SECURITY.md](SECURITY.md).
509
+ Participation follows the [code of conduct](CODE_OF_CONDUCT.md).
510
+
511
+ ## License
512
+
513
+ Braid is released under the [MIT License](LICENSE).