@chrok/braid 0.1.1 → 0.1.3
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 +26 -0
- package/CONTRIBUTING.md +11 -4
- package/README.md +53 -11
- package/SECURITY.md +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/runtime.js +21 -2
- package/dist/types.d.ts +6 -2
- package/dist/validate.js +7 -2
- package/dist/workspaces.d.ts +4 -1
- package/dist/workspaces.js +46 -7
- package/docs/compatibility.md +15 -4
- package/docs/releasing.md +12 -5
- package/docs/repository-settings.md +9 -3
- package/docs/resource-limits.md +4 -2
- package/examples/code-review.ts +3 -3
- package/package.json +11 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.3 — 2026-09-29
|
|
6
|
+
|
|
7
|
+
- Add per-node `workspace: "read-only" | "worktree"` for execute/decision nodes.
|
|
8
|
+
Read-only nodes inspect the live source directory with Git inspection, without
|
|
9
|
+
writable tools, snapshots, worktrees, or merge sources. Existing defaults are
|
|
10
|
+
preserved. Pi's braid tool guides analysis/review/routing/synthesis to read-only
|
|
11
|
+
nodes and implementation to worktrees; explicit read-only workspace metadata
|
|
12
|
+
is included in results and events (#22).
|
|
13
|
+
- Skip the automatic merge agent when all remaining workspaces match their
|
|
14
|
+
snapshots, preserving analysis-only terminal outputs and original node errors.
|
|
15
|
+
Mixed runs pass only changed sources to the automatic merge. Unchanged sources
|
|
16
|
+
retain recovery refs pointing to their snapshots without empty checkpoint
|
|
17
|
+
commits; explicit merge nodes still run even for unchanged sources (#21).
|
|
18
|
+
- Prevent Pi TUI crashes in narrow terminals by truncating the flowchart fallback
|
|
19
|
+
message to the terminal width.
|
|
20
|
+
- Scan fork pull requests with CodeQL advanced setup so external contributions
|
|
21
|
+
can satisfy the required security checks.
|
|
22
|
+
|
|
23
|
+
## 0.1.2 — 2026-09-27
|
|
24
|
+
|
|
25
|
+
- Make Pi depend on the exact `@chrok/braid` version instead of bundling core.
|
|
26
|
+
Export shared model-adapter helpers from the existing root entry point.
|
|
27
|
+
- Use one npm workspace lockfile and a development-only local core link; test
|
|
28
|
+
Pi-only installation against the unpublished core tarball outside the checkout.
|
|
29
|
+
- Wait for core registry metadata and tarball availability before publishing Pi.
|
|
30
|
+
|
|
5
31
|
## 0.1.1 — 2026-09-27
|
|
6
32
|
|
|
7
33
|
- Validate the Pi integration against Pi 0.87.1; update GitHub Actions and tsx.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -12,13 +12,20 @@ Use Node.js 22.19+ and npm. From a fresh checkout:
|
|
|
12
12
|
git clone https://github.com/Epsirom/braid.git
|
|
13
13
|
cd braid
|
|
14
14
|
npm ci
|
|
15
|
-
npm ci --prefix integrations/pi
|
|
16
15
|
npm run verify
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
The root npm workspace lockfile covers both packages. A development-only
|
|
19
|
+
`@chrok/braid: file:.` dependency links the core checkout into `node_modules`;
|
|
20
|
+
Pi's manifest still declares the exact release version. This lets CI test a
|
|
21
|
+
new core before it is published. `check:pi`, `test:pi`, and `build:pi` rebuild
|
|
22
|
+
core so package imports resolve current JavaScript and declarations. Consumers
|
|
23
|
+
installing either published package do not install this development dependency.
|
|
24
|
+
Use `npm install --workspace @chrok/pi-braid <dependency>` for Pi dependency
|
|
25
|
+
updates; do not create a separate lockfile in `integrations/pi`.
|
|
26
|
+
|
|
27
|
+
The core has no runtime dependencies. Pi's dependencies belong in its workspace
|
|
28
|
+
manifest. Update and commit the root lockfile when changing a dependency. Do not commit generated `dist` directories,
|
|
22
29
|
tarballs, credentials, or provider output containing private data.
|
|
23
30
|
|
|
24
31
|
`verify` type-checks both packages, runs deterministic tests and offline examples,
|
package/README.md
CHANGED
|
@@ -164,9 +164,10 @@ output. The graph has no special fork, branch, or join nodes.
|
|
|
164
164
|
|
|
165
165
|
```ts
|
|
166
166
|
type BraidNode =
|
|
167
|
-
| { type: "execute"; id: string; prompt: string; model?: string
|
|
167
|
+
| { type: "execute"; id: string; prompt: string; model?: string;
|
|
168
|
+
workspace?: "read-only" | "worktree" }
|
|
168
169
|
| { type: "decision"; id: string; prompt: string;
|
|
169
|
-
choices: readonly string[]; model?: string }
|
|
170
|
+
choices: readonly string[]; model?: string; workspace?: "read-only" | "worktree" }
|
|
170
171
|
| { type: "merge"; id: string; prompt?: string; model?: string };
|
|
171
172
|
|
|
172
173
|
type Edge = { from: string; to: string; choice?: string };
|
|
@@ -175,6 +176,11 @@ type BraidInput = { goal: string; nodes: readonly BraidNode[]; edges: readonly E
|
|
|
175
176
|
|
|
176
177
|
IDs are unique, non-empty strings. Prompts, goals, models, and choices must be
|
|
177
178
|
non-empty strings when present. Decision choices must be non-empty and unique.
|
|
179
|
+
`workspace` is optional on execute/decision nodes and forbidden on merge nodes.
|
|
180
|
+
Use `"read-only"` for analysis, review, routing, and synthesis. Omit it or use
|
|
181
|
+
`"worktree"` for the existing behavior: an isolated writable worktree in Git,
|
|
182
|
+
read-only access outside Git. Explicit read-only nodes read the live source
|
|
183
|
+
directory, not a fixed snapshot; see [workspace semantics](#worktrees-and-merge-agents).
|
|
178
184
|
Unknown fields, unsupported node types, missing references, duplicate exact
|
|
179
185
|
edges, and cycles are rejected. Cycles are rejected even if a decision might
|
|
180
186
|
make them inactive. Disconnected components are allowed; every root runs.
|
|
@@ -259,7 +265,8 @@ The event sequence includes:
|
|
|
259
265
|
- `graph_created`, `node_created`, and `edge_created` when the submitted DAG is
|
|
260
266
|
admitted; an appended final merge emits its own node/edge creation events.
|
|
261
267
|
- `node_runnable` and `node_started` when scheduling admits a node.
|
|
262
|
-
- `workspace_updated` for Git workspace preparation, checkpointing, and
|
|
268
|
+
- `workspace_updated` for Git workspace preparation, checkpointing, cleanup, and
|
|
269
|
+
explicitly requested read-only workspaces.
|
|
263
270
|
- `handoff` for every direct predecessor output passed to a downstream node,
|
|
264
271
|
including the upstream decision when present.
|
|
265
272
|
- `node_completed`, `node_skipped`, and `node_failed`, including output,
|
|
@@ -281,8 +288,9 @@ render live topology, handoffs, failures, and active nodes without reconstructin
|
|
|
281
288
|
scheduler state from final results. Tool selection remains the responsibility of
|
|
282
289
|
the host agent; the optional Pi adapter supplies explicit proactive-use guidance
|
|
283
290
|
so Braid is considered for complex multi-branch reasoning without forcing it for
|
|
284
|
-
every prompt. In the Pi adapter,
|
|
285
|
-
worktrees; nodes outside Git stay read-only. Merge agents
|
|
291
|
+
every prompt. In the Pi adapter, nodes can inspect the live source read-only or
|
|
292
|
+
edit individual Git worktrees; nodes outside Git stay read-only. Merge agents
|
|
293
|
+
handle integration; the parent reviews results and runs shell commands and tests.
|
|
286
294
|
|
|
287
295
|
## Context isolation and model runners
|
|
288
296
|
|
|
@@ -308,7 +316,8 @@ merge agents. Adapters must enforce workspace capabilities and wrap mutating
|
|
|
308
316
|
file tools in `request.withWorkspaceWrite(operation)`, so cleanup waits for
|
|
309
317
|
in-flight writes and rejects later writes. Core Git mutations use this barrier.
|
|
310
318
|
The included Pi adapter provides guarded `write`/`edit` alongside its read tools.
|
|
311
|
-
|
|
319
|
+
For read-only workspaces, adapters must omit mutating tools; the core write
|
|
320
|
+
barrier also rejects writes. This includes all nodes outside Git. Pi never provides
|
|
312
321
|
`bash`, `powershell`, or a test runner to nodes.
|
|
313
322
|
|
|
314
323
|
`request.predecessors` contains direct active predecessors in incoming-edge
|
|
@@ -327,17 +336,39 @@ Read tools follow the host filesystem permissions and are not a security sandbox
|
|
|
327
336
|
|
|
328
337
|
### Worktrees and merge agents
|
|
329
338
|
|
|
330
|
-
|
|
331
|
-
`os.tmpdir()/braid-workspaces-*/<unique-id>`. The first node captures tracked
|
|
339
|
+
By default, execute and decision nodes in a Git checkout receive detached worktrees
|
|
340
|
+
under `os.tmpdir()/braid-workspaces-*/<unique-id>`. The first writable node captures tracked
|
|
332
341
|
staged/unstaged changes, deletions, and non-ignored untracked files with a temporary
|
|
333
342
|
index. Snapshot creation preserves the source index, branch, and files. Ignored
|
|
334
343
|
files are not copied, and submodules are not initialized or recursively captured.
|
|
335
344
|
Pi rejects writes inside submodules. If a custom runner populates one, core
|
|
336
345
|
reports cleanup failure and retains the worktree rather than losing those files.
|
|
337
|
-
Empty repositories are supported.
|
|
338
|
-
subsequent nodes snapshot the current source checkout. Relative working directories
|
|
346
|
+
Empty repositories are supported. Worktree nodes share this baseline until a merge
|
|
347
|
+
ends; subsequent worktree nodes snapshot the current source checkout. Relative working directories
|
|
339
348
|
are preserved. Code changes do not implicitly flow into successor worktrees.
|
|
340
349
|
|
|
350
|
+
Set `workspace: "read-only"` on execute/decision nodes that only inspect or reason
|
|
351
|
+
about files. They read the original `cwd`, including ignored files accessible to
|
|
352
|
+
the adapter, without creating a snapshot, worktree, checkpoint, or merge source.
|
|
353
|
+
In Git they retain inspection commands (`status`, `diff`, `show`, `log`,
|
|
354
|
+
`ls-files`, `rev-parse`), with optional Git index writes disabled. Relative paths
|
|
355
|
+
use the original `cwd`, including when it is a subdirectory of the repository.
|
|
356
|
+
These reads observe the live checkout: parent edits or concurrent merge nodes
|
|
357
|
+
may change files during execution. Use worktree mode when a fixed snapshot is
|
|
358
|
+
needed. Predecessor edits are visible in the source only after integration;
|
|
359
|
+
their worktrees/checkpoints can still be inspected explicitly.
|
|
360
|
+
|
|
361
|
+
Explicit read-only allocations appear in `workspace_updated`, node/predecessor
|
|
362
|
+
workspace metadata, and `result.workspaces` with mode `read-only` and state
|
|
363
|
+
`ready`. They have no cleanup lifecycle or recovery refs. Implicit non-Git
|
|
364
|
+
read-only runs retain their existing event/result behavior. A custom runner is
|
|
365
|
+
trusted code and must honor the workspace capability; this is not an OS sandbox.
|
|
366
|
+
|
|
367
|
+
For a mixed graph, give review branches `workspace: "read-only"` and leave
|
|
368
|
+
implementation branches in worktree mode. Use a read-only execute node to
|
|
369
|
+
summarize findings; a merge node integrates file changes and does not accept
|
|
370
|
+
the `workspace` field.
|
|
371
|
+
|
|
341
372
|
Add `{ type: "merge", id: "integrate" }` with incoming edges from any number of
|
|
342
373
|
sources. The merge agent receives predecessor errors, workspace paths, and Git
|
|
343
374
|
checkpoint refs and operates directly in the invoking checkout. **Core does not
|
|
@@ -376,13 +407,24 @@ including intentionally discarded changes and ignored node output files. A
|
|
|
376
407
|
later consumer can inspect a removed source through `git show <checkpointRef>`.
|
|
377
408
|
Core also records `backupRef` for the source checkout before each merge agent.
|
|
378
409
|
|
|
379
|
-
When declared nodes settle
|
|
410
|
+
When declared nodes settle, core releases worktrees whose contents match their
|
|
411
|
+
own snapshot, including those from failed nodes, as `discarded` with reason
|
|
412
|
+
`No changes from snapshot`. Their checkpoint refs point to the snapshot commit
|
|
413
|
+
without creating empty checkpoint commits; original node errors remain visible.
|
|
414
|
+
This comparison includes ignored output files. Explicit merge nodes still run
|
|
415
|
+
even when their sources have no changes.
|
|
416
|
+
|
|
417
|
+
When worktrees with changes remain, core appends an ordinary merge
|
|
380
418
|
agent named `__braid_merge__` (with a suffix if needed), using the run's default
|
|
381
419
|
model. It appears in results, events, usage, and terminal outputs. Merge nodes
|
|
382
420
|
are exclusive within a graph and serialized per source checkout across runs in
|
|
383
421
|
the same process. Avoid concurrent external edits to that checkout while merging;
|
|
384
422
|
this lock does not coordinate other processes or the parent editor.
|
|
385
423
|
|
|
424
|
+
Analysis-only graphs therefore keep their declared terminal outputs and do not
|
|
425
|
+
incur an automatic merge model call. Consumers should not assume that every Git
|
|
426
|
+
run includes `__braid_merge__`; use `terminalOutputs` for the completed endpoints.
|
|
427
|
+
|
|
386
428
|
Cancellation or graph timeout prevents new merge agents from starting. Core waits
|
|
387
429
|
for tracked writes, archives remaining work, and removes its worktrees. Merge
|
|
388
430
|
agent failure follows the same archive/cleanup path. It does not reset the source
|
package/SECURITY.md
CHANGED
|
@@ -22,6 +22,10 @@ upgrade. Before the first release, report issues against the current main branch
|
|
|
22
22
|
changes into the source checkout; they are not restricted to read-only analysis.
|
|
23
23
|
Outside Git, Pi file tools stay read-only. Read paths can expose files outside
|
|
24
24
|
the checkout and disclose content to a model provider.
|
|
25
|
+
- Execute/decision nodes can request `workspace: "read-only"` inside Git. Pi
|
|
26
|
+
omits write/edit tools, and core rejects write-barrier operations. Git inspection
|
|
27
|
+
remains available. These nodes read the live source directory, not an isolated
|
|
28
|
+
snapshot; custom runners must honor this capability themselves.
|
|
25
29
|
- Guarded write tools reject external paths, Git metadata, symlinks, hard links,
|
|
26
30
|
and special files, but are not an OS sandbox against concurrent filesystem
|
|
27
31
|
attacks. Avoid concurrent external source edits while merge agents run. A
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { braid } from "./runtime.js";
|
|
2
2
|
export { validateGraph, GraphValidationError } from "./validate.js";
|
|
3
3
|
export type { BraidInput, BraidNode, BraidOptions, BraidResult, DecisionNode, Edge, ExecuteNode, ExecutionContext, ExecutionError, ExecutionEvent, ModelRequest, ModelResponse, ModelRunner, MergeNode, MergeDisposition, MergeSource, GitPreview, SourceCheckoutStatus, NodeWorkspace, GitResult, NodeOutput, NodeResult, NodeStatus, PredecessorOutput, TokenUsage, } from "./types.js";
|
|
4
|
+
export { formatBudgetReminder } from "./budgets.js";
|
|
5
|
+
export { gitToolDefinition, finishMergeToolDefinition, mergeInstructions, parseGitToolArguments, parseFinishMergeArguments, } from "./merge-tools.js";
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { braid } from "./runtime.js";
|
|
2
2
|
export { validateGraph, GraphValidationError } from "./validate.js";
|
|
3
|
+
// Shared helpers for model adapters, including the Pi integration.
|
|
4
|
+
export { formatBudgetReminder } from "./budgets.js";
|
|
5
|
+
export { gitToolDefinition, finishMergeToolDefinition, mergeInstructions, parseGitToolArguments, parseFinishMergeArguments, } from "./merge-tools.js";
|
package/dist/runtime.js
CHANGED
|
@@ -179,6 +179,7 @@ async function runNode(request, result, runner, timeoutMs, graphSignal, graphDea
|
|
|
179
179
|
});
|
|
180
180
|
void aborted.catch(() => { });
|
|
181
181
|
let acceptingWrites = true;
|
|
182
|
+
let writableWorkspace = false;
|
|
182
183
|
const writes = new Set();
|
|
183
184
|
let merge;
|
|
184
185
|
let acceptingDecisions = true;
|
|
@@ -191,6 +192,8 @@ async function runNode(request, result, runner, timeoutMs, graphSignal, graphDea
|
|
|
191
192
|
signal.throwIfAborted();
|
|
192
193
|
if (!acceptingWrites)
|
|
193
194
|
throw new Error("Node has finished; further writes are unavailable");
|
|
195
|
+
if (!writableWorkspace)
|
|
196
|
+
throw new Error("Node workspace is read-only; writes are unavailable");
|
|
194
197
|
const pending = Promise.resolve().then(() => {
|
|
195
198
|
signal.throwIfAborted();
|
|
196
199
|
return operation();
|
|
@@ -249,6 +252,7 @@ async function runNode(request, result, runner, timeoutMs, graphSignal, graphDea
|
|
|
249
252
|
invocation.workspace = await workspaces.prepare(invocation);
|
|
250
253
|
}
|
|
251
254
|
invocation.workspace = structuredClone(invocation.workspace);
|
|
255
|
+
writableWorkspace = invocation.workspace.mode !== "read-only";
|
|
252
256
|
if (invocation.workspace?.sourceRoot) {
|
|
253
257
|
const gitRequest = { ...invocation, node: structuredClone(invocation.node), workspace: structuredClone(invocation.workspace) };
|
|
254
258
|
invocation.git = (args, input) => workspaces.git(gitRequest, args, input);
|
|
@@ -375,8 +379,11 @@ export async function braid(input, options) {
|
|
|
375
379
|
result.model = model;
|
|
376
380
|
return [node.id, result];
|
|
377
381
|
}));
|
|
382
|
+
const explicitReadOnly = new Set(graph.nodes.filter(node => node.type !== "merge" && node.workspace === "read-only").map(node => node.id));
|
|
383
|
+
// Preserve the existing event/result shape for implicit non-Git read-only runs.
|
|
384
|
+
const reportWorkspace = (workspace) => workspace.mode !== "read-only" || explicitReadOnly.has(workspace.nodeId);
|
|
378
385
|
const workspaces = new GitWorkspaces(resolve(options.cwd ?? process.cwd()), workspace => {
|
|
379
|
-
if (workspace
|
|
386
|
+
if (!reportWorkspace(workspace))
|
|
380
387
|
return;
|
|
381
388
|
const node = results.get(workspace.nodeId);
|
|
382
389
|
if (node)
|
|
@@ -438,6 +445,18 @@ export async function braid(input, options) {
|
|
|
438
445
|
running.set(node.id, task);
|
|
439
446
|
}
|
|
440
447
|
if (running.size === 0) {
|
|
448
|
+
if (!automaticMergeAdded) {
|
|
449
|
+
try {
|
|
450
|
+
await workspaces.discardUnchanged();
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
cleanupError = { code: "CLEANUP_FAILED", message: error instanceof Error ? error.message : String(error) };
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
// Checkpointing/cleanup can outlast the deadline or trigger cancellation.
|
|
457
|
+
if (controller.signal.aborted || performance.now() >= graphDeadline)
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
441
460
|
const pending = workspaces.pending();
|
|
442
461
|
if (!automaticMergeAdded && pending.length) {
|
|
443
462
|
automaticMergeAdded = true;
|
|
@@ -503,7 +522,7 @@ export async function braid(input, options) {
|
|
|
503
522
|
terminalOutputs,
|
|
504
523
|
events: [],
|
|
505
524
|
nodes: Object.fromEntries(results),
|
|
506
|
-
...(Object.values(workspaces.all()).some(
|
|
525
|
+
...(Object.values(workspaces.all()).some(reportWorkspace) ? { workspaces: workspaces.all() } : {}),
|
|
507
526
|
metadata: {
|
|
508
527
|
...execution,
|
|
509
528
|
startedAt,
|
package/dist/types.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ export interface ExecuteNode {
|
|
|
3
3
|
id: string;
|
|
4
4
|
prompt: string;
|
|
5
5
|
model?: string;
|
|
6
|
+
/** Read the live cwd without a worktree; defaults to worktree in Git, read-only elsewhere. */
|
|
7
|
+
workspace?: "read-only" | "worktree";
|
|
6
8
|
}
|
|
7
9
|
export interface DecisionNode {
|
|
8
10
|
type: "decision";
|
|
@@ -10,6 +12,8 @@ export interface DecisionNode {
|
|
|
10
12
|
prompt: string;
|
|
11
13
|
choices: readonly string[];
|
|
12
14
|
model?: string;
|
|
15
|
+
/** Read the live cwd without a worktree; defaults to worktree in Git, read-only elsewhere. */
|
|
16
|
+
workspace?: "read-only" | "worktree";
|
|
13
17
|
}
|
|
14
18
|
export interface MergeNode {
|
|
15
19
|
type: "merge";
|
|
@@ -108,7 +112,7 @@ export interface ModelRequest {
|
|
|
108
112
|
graph?: number;
|
|
109
113
|
};
|
|
110
114
|
workspace?: NodeWorkspace;
|
|
111
|
-
/**
|
|
115
|
+
/** Rejects read-only writes; adapters must wrap mutating file tools so cleanup waits for in-flight writes. */
|
|
112
116
|
withWorkspaceWrite?: <T>(operation: () => Promise<T>) => Promise<T>;
|
|
113
117
|
/** Local Git operations: inspection for workers, integration commands for merge nodes. */
|
|
114
118
|
git?: (args: string[], input?: string) => Promise<GitResult>;
|
|
@@ -189,7 +193,7 @@ export type ExecutionEvent = Readonly<{
|
|
|
189
193
|
export interface BraidOptions {
|
|
190
194
|
runner: ModelRunner;
|
|
191
195
|
defaultModel?: string;
|
|
192
|
-
/** Source checkout.
|
|
196
|
+
/** Source checkout. Nodes may opt into live read-only access; outside Git, all nodes are read-only. */
|
|
193
197
|
cwd?: string;
|
|
194
198
|
/** Positive integer. Defaults to 4. */
|
|
195
199
|
maxConcurrency?: number;
|
package/dist/validate.js
CHANGED
|
@@ -34,16 +34,21 @@ export function compileGraph(input) {
|
|
|
34
34
|
requireValid(isRecord(node), "Node must be an object");
|
|
35
35
|
requireValid(node.type === "execute" || node.type === "decision" || node.type === "merge", "Unknown node type");
|
|
36
36
|
fields(node, node.type === "decision"
|
|
37
|
-
? ["type", "id", "prompt", "model", "choices"]
|
|
38
|
-
:
|
|
37
|
+
? ["type", "id", "prompt", "model", "choices", "workspace"]
|
|
38
|
+
: node.type === "execute"
|
|
39
|
+
? ["type", "id", "prompt", "model", "workspace"]
|
|
40
|
+
: ["type", "id", "prompt", "model"], "Node");
|
|
39
41
|
requireValid(text(node.id), "Node id must be a non-empty string");
|
|
40
42
|
requireValid(!byId.has(node.id), `Duplicate node id '${node.id}'`);
|
|
41
43
|
requireValid(text(node.prompt) || (node.type === "merge" && node.prompt === undefined), `Node '${node.id}' needs a non-empty prompt`);
|
|
42
44
|
requireValid(node.model === undefined || text(node.model), `Invalid model on '${node.id}'`);
|
|
45
|
+
const workspace = node.type === "merge" ? undefined : node.workspace;
|
|
46
|
+
requireValid(workspace === undefined || workspace === "read-only" || workspace === "worktree", `Invalid workspace on '${node.id}'`);
|
|
43
47
|
const common = {
|
|
44
48
|
id: node.id,
|
|
45
49
|
prompt: node.prompt ?? "Review all predecessor changes, decide how to integrate them into the source repository, and account for every source with finish_merge.",
|
|
46
50
|
...(node.model !== undefined ? { model: node.model } : {}),
|
|
51
|
+
...(workspace !== undefined ? { workspace } : {}),
|
|
47
52
|
};
|
|
48
53
|
if (node.type === "decision") {
|
|
49
54
|
requireValid(Array.isArray(node.choices) && node.choices.length > 0, `Decision '${node.id}' needs at least one choice`);
|
package/dist/workspaces.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GitResult, MergeDisposition, MergeSource, ModelRequest, NodeWorkspace, SourceCheckoutStatus } from "./types.js";
|
|
2
|
-
/**
|
|
2
|
+
/** Writable workers share a snapshot; explicit read-only workers inspect the live cwd. */
|
|
3
3
|
export declare class GitWorkspaces {
|
|
4
4
|
private readonly cwd;
|
|
5
5
|
private readonly onWorkspace?;
|
|
@@ -9,12 +9,15 @@ export declare class GitWorkspaces {
|
|
|
9
9
|
private allocated;
|
|
10
10
|
constructor(cwd: string, onWorkspace?: ((workspace: NodeWorkspace) => void) | undefined);
|
|
11
11
|
private report;
|
|
12
|
+
private sourceRoot;
|
|
12
13
|
private snapshot;
|
|
13
14
|
prepare(request: ModelRequest): Promise<NodeWorkspace>;
|
|
14
15
|
all(): Record<string, NodeWorkspace>;
|
|
15
16
|
pending(): string[];
|
|
16
17
|
/** Checkpoint every file, including ignored node outputs, before releasing a worktree. */
|
|
17
18
|
private checkpoint;
|
|
19
|
+
/** Run only after declared nodes settle, so consumers retain their source paths. */
|
|
20
|
+
discardUnchanged(): Promise<void>;
|
|
18
21
|
private release;
|
|
19
22
|
/** Only archive/remove here. Choosing merge/cherry-pick/apply always belongs to the agent. */
|
|
20
23
|
archivePending(reason: string): Promise<void>;
|
package/dist/workspaces.js
CHANGED
|
@@ -91,7 +91,7 @@ async function gitPreview(cwd, args, limit) {
|
|
|
91
91
|
}
|
|
92
92
|
return { text: output.slice(0, limit), truncated: overflow || output.length > limit };
|
|
93
93
|
}
|
|
94
|
-
/**
|
|
94
|
+
/** Writable workers share a snapshot; explicit read-only workers inspect the live cwd. */
|
|
95
95
|
export class GitWorkspaces {
|
|
96
96
|
cwd;
|
|
97
97
|
onWorkspace;
|
|
@@ -112,7 +112,7 @@ export class GitWorkspaces {
|
|
|
112
112
|
// Observers cannot change permissions or fail node execution.
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
-
async
|
|
115
|
+
async sourceRoot() {
|
|
116
116
|
let ancestor = resolve(this.cwd);
|
|
117
117
|
while (true) {
|
|
118
118
|
try {
|
|
@@ -138,7 +138,12 @@ export class GitWorkspaces {
|
|
|
138
138
|
return undefined;
|
|
139
139
|
throw error;
|
|
140
140
|
}
|
|
141
|
-
|
|
141
|
+
return realpath(sourceRoot);
|
|
142
|
+
}
|
|
143
|
+
async snapshot() {
|
|
144
|
+
const sourceRoot = await this.sourceRoot();
|
|
145
|
+
if (!sourceRoot)
|
|
146
|
+
return undefined;
|
|
142
147
|
const commonDirectory = await realpath(resolve(sourceRoot, await git(sourceRoot, ["rev-parse", "--git-common-dir"])));
|
|
143
148
|
const cwdSuffix = relative(sourceRoot, await realpath(this.cwd));
|
|
144
149
|
// Git records canonical worktree paths. In particular, Windows tmpdir()
|
|
@@ -183,7 +188,7 @@ export class GitWorkspaces {
|
|
|
183
188
|
"-m", "Braid isolated workspace snapshot",
|
|
184
189
|
], options);
|
|
185
190
|
const snapshot = {
|
|
186
|
-
directory, commonDirectory, sourceRoot, cwdSuffix, snapshotCommit, hooksDirectory,
|
|
191
|
+
directory, commonDirectory, sourceRoot, cwdSuffix, snapshotTree: tree, snapshotCommit, hooksDirectory,
|
|
187
192
|
...(baseCommit ? { baseCommit } : {}),
|
|
188
193
|
};
|
|
189
194
|
this.allocated.add(snapshot);
|
|
@@ -200,6 +205,16 @@ export class GitWorkspaces {
|
|
|
200
205
|
}
|
|
201
206
|
async prepare(request) {
|
|
202
207
|
request.signal.throwIfAborted();
|
|
208
|
+
if (request.node.type !== "merge" && request.node.workspace === "read-only") {
|
|
209
|
+
const sourceRoot = await this.sourceRoot();
|
|
210
|
+
request.signal.throwIfAborted();
|
|
211
|
+
const workspace = {
|
|
212
|
+
nodeId: request.node.id, mode: "read-only", workingDirectory: this.cwd, state: "ready",
|
|
213
|
+
...(sourceRoot ? { sourceRoot } : {}),
|
|
214
|
+
};
|
|
215
|
+
this.report(workspace);
|
|
216
|
+
return workspace;
|
|
217
|
+
}
|
|
203
218
|
let pending = this.snapshots.get(request.execution.runId);
|
|
204
219
|
if (!pending) {
|
|
205
220
|
// Do not bind the shared snapshot to one node's cancellation signal.
|
|
@@ -279,7 +294,7 @@ export class GitWorkspaces {
|
|
|
279
294
|
}
|
|
280
295
|
await git(cwd, ["add", "--force", "--all", "--", "."], options);
|
|
281
296
|
const tree = await git(cwd, ["write-tree"], options);
|
|
282
|
-
const commit = await git(cwd, [
|
|
297
|
+
const commit = tree === location.snapshotTree ? workspace.snapshotCommit : await git(cwd, [
|
|
283
298
|
"-c", "user.name=Braid", "-c", "user.email=braid@localhost", "-c", "commit.gpgsign=false",
|
|
284
299
|
"commit-tree", tree, "-p", workspace.snapshotCommit, "-m", `Braid node checkpoint: ${workspace.nodeId}`,
|
|
285
300
|
], options);
|
|
@@ -290,6 +305,27 @@ export class GitWorkspaces {
|
|
|
290
305
|
workspace.checkpointRef = ref;
|
|
291
306
|
this.report(workspace);
|
|
292
307
|
}
|
|
308
|
+
/** Run only after declared nodes settle, so consumers retain their source paths. */
|
|
309
|
+
async discardUnchanged() {
|
|
310
|
+
const errors = [];
|
|
311
|
+
for (const id of this.pending()) {
|
|
312
|
+
const workspace = this.records.get(id);
|
|
313
|
+
// Incomplete preparation must follow the existing failure/recovery path.
|
|
314
|
+
// A failed invocation with a successfully prepared workspace is still ready.
|
|
315
|
+
if (workspace.state !== "ready")
|
|
316
|
+
continue;
|
|
317
|
+
try {
|
|
318
|
+
await this.checkpoint(workspace);
|
|
319
|
+
if (workspace.checkpointCommit === workspace.snapshotCommit)
|
|
320
|
+
await this.release(id, "discarded", "No changes from snapshot");
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
errors.push(error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (errors.length)
|
|
327
|
+
throw new AggregateError(errors, "Some workspaces could not be checked or removed; their paths are retained in workspaces");
|
|
328
|
+
}
|
|
293
329
|
async release(id, disposition, reason) {
|
|
294
330
|
const workspace = this.records.get(id);
|
|
295
331
|
const location = this.locations.get(id);
|
|
@@ -374,7 +410,8 @@ export class GitWorkspaces {
|
|
|
374
410
|
}))
|
|
375
411
|
throw new Error("Git arguments cannot override filesystem boundaries, execute external helpers, or select external strategies");
|
|
376
412
|
const workspace = request.workspace;
|
|
377
|
-
const cwd = workspace.mode === "
|
|
413
|
+
const cwd = workspace.mode === "read-only" ? workspace.workingDirectory
|
|
414
|
+
: workspace.mode === "merge" ? workspace.sourceRoot : workspace.worktreeRoot;
|
|
378
415
|
const location = this.locations.get(request.node.id);
|
|
379
416
|
const actualArgs = [command,
|
|
380
417
|
...(["diff", "show", "log"].includes(command) ? ["--no-ext-diff", "--no-textconv"] : []),
|
|
@@ -382,7 +419,9 @@ export class GitWorkspaces {
|
|
|
382
419
|
];
|
|
383
420
|
const execute = () => new Promise((resolve, reject) => {
|
|
384
421
|
const child = execFile("git", [
|
|
385
|
-
"-c", `core.hooksPath=${location.hooksDirectory}
|
|
422
|
+
...(location ? ["-c", `core.hooksPath=${location.hooksDirectory}`] : []),
|
|
423
|
+
...(workspace.mode === "read-only" ? ["--no-optional-locks"] : []),
|
|
424
|
+
"-c", "commit.gpgsign=false",
|
|
386
425
|
"-c", "core.fsmonitor=false", "--no-pager", "-C", cwd, ...actualArgs,
|
|
387
426
|
], {
|
|
388
427
|
env: { ...gitEnvironment(), GIT_TERMINAL_PROMPT: "0", GIT_EDITOR: "true", GIT_SEQUENCE_EDITOR: "true", GIT_MERGE_AUTOEDIT: "no" },
|
package/docs/compatibility.md
CHANGED
|
@@ -23,11 +23,17 @@ exactly one automatic continuation when a job finishes during `agent_settled`.
|
|
|
23
23
|
This covers the Pi 0.86/0.87 transcript and settling changes without provider
|
|
24
24
|
credentials or network model calls. Version 0.1.0 was originally validated with
|
|
25
25
|
Pi 0.85.1; the current checkout's pinned validation target is 0.87.1.
|
|
26
|
-
The Pi npm package
|
|
27
|
-
core
|
|
26
|
+
The Pi npm package declares an exact dependency on the matching `@chrok/braid`
|
|
27
|
+
release. npm installs the core automatically; Pi does not bundle another copy of
|
|
28
|
+
its runtime and does not need a source checkout.
|
|
28
29
|
|
|
29
30
|
Git must be installed for workspace execution inside a Git checkout. Non-Git
|
|
30
31
|
text-only runs do not require Git workspace management.
|
|
32
|
+
Execute/decision nodes can opt into `workspace: "read-only"`; omitted or
|
|
33
|
+
`"worktree"` values preserve the existing allocation behavior. Merge nodes reject
|
|
34
|
+
this field. Explicit read-only nodes report workspace metadata/events even when
|
|
35
|
+
the entire run is read-only; implicit non-Git runs keep their existing shapes.
|
|
36
|
+
Older versions reject the new field during validation.
|
|
31
37
|
|
|
32
38
|
## Versioning
|
|
33
39
|
|
|
@@ -36,8 +42,13 @@ preserve documented behavior; breaking API or semantic changes require a minor
|
|
|
36
42
|
version bump, a changelog entry, and migration guidance. New optional fields or
|
|
37
43
|
fixes that restore the documented contract may ship in a patch.
|
|
38
44
|
|
|
39
|
-
Supported core entry points are `@chrok/braid` and `@chrok/braid/adapters/openai`.
|
|
40
|
-
|
|
45
|
+
Supported core entry points are `@chrok/braid` and `@chrok/braid/adapters/openai`.
|
|
46
|
+
The root `@chrok/braid` entry point also exports adapter helpers:
|
|
47
|
+
`formatBudgetReminder`, `gitToolDefinition`, `finishMergeToolDefinition`,
|
|
48
|
+
`mergeInstructions`, `parseGitToolArguments`, and `parseFinishMergeArguments`.
|
|
49
|
+
These helpers share the core's compatibility policy; Git workspace implementation
|
|
50
|
+
classes remain internal. Internal files and Pi helper classes are not stable
|
|
51
|
+
public APIs. Documented result/error
|
|
41
52
|
fields, routing behavior, `ModelRunner`, and existing event meanings are part of
|
|
42
53
|
the public contract. Consumers should ignore new diagnostic fields and provide a
|
|
43
54
|
fallback for new event types; event sequence numbers order events within a run,
|
package/docs/releasing.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Releasing
|
|
2
2
|
|
|
3
3
|
Core and Pi are separate public npm packages built from one commit. Both use the
|
|
4
|
-
same version. The core has no runtime dependencies. Pi
|
|
5
|
-
|
|
4
|
+
same version. The core has no runtime dependencies. Pi declares an exact
|
|
5
|
+
`@chrok/braid` dependency and includes only its own compiled integration code.
|
|
6
6
|
|
|
7
7
|
## Prepare a release
|
|
8
8
|
|
|
@@ -10,11 +10,14 @@ Version tags (`v*`) cannot be moved or deleted. New GitHub releases are immutabl
|
|
|
10
10
|
prepare a draft and attach any assets before publishing. Published tag/asset
|
|
11
11
|
corrections require a new version. See [repository settings](repository-settings.md).
|
|
12
12
|
|
|
13
|
-
1. Update both `package.json` versions
|
|
13
|
+
1. Update both `package.json` versions, Pi's exact `@chrok/braid` dependency,
|
|
14
|
+
and the root workspace lockfile (`npm install --package-lock-only`). Use a minor version
|
|
14
15
|
for breaking 0.x changes, and describe migrations in the changelog.
|
|
15
|
-
2. Run `npm ci
|
|
16
|
+
2. Run `npm ci` and `npm run verify` from the repository root.
|
|
16
17
|
The package check verifies clean builds, public ESM imports, declarations,
|
|
17
18
|
licenses, and Pi registration in a temporary consumer outside the checkout.
|
|
19
|
+
A temporary local registry serves the unpublished core tarball; installing
|
|
20
|
+
only the Pi tarball must fetch core transitively through its version dependency.
|
|
18
21
|
3. Update the changelog and supported Pi version. Commit and review the changes;
|
|
19
22
|
require the CI matrix to pass before tagging that commit `vX.Y.Z`.
|
|
20
23
|
4. Inspect `npm pack --dry-run` and `npm pack --dry-run` from `integrations/pi`.
|
|
@@ -64,7 +67,11 @@ npm trust list @chrok/pi-braid
|
|
|
64
67
|
|
|
65
68
|
Publishing a non-prerelease GitHub release triggers `.github/workflows/release.yml`.
|
|
66
69
|
It validates the tag/version relationship, repeats all checks, and publishes
|
|
67
|
-
core then Pi using short-lived OIDC credentials.
|
|
70
|
+
core then Pi using short-lived OIDC credentials. After each publish (or retry
|
|
71
|
+
of an existing version), it waits for matching version/commit metadata and a
|
|
72
|
+
successful tarball download. Pi is not published until core is available. npm
|
|
73
|
+
processing is polled every 15 seconds for up to 10 minutes; a timeout fails the
|
|
74
|
+
workflow with retry instructions. It does not require `NPM_TOKEN`.
|
|
68
75
|
The workflow installs npm 11 and uses Node 24. See
|
|
69
76
|
[npm's trusted publishing documentation](https://docs.npmjs.com/trusted-publishers/).
|
|
70
77
|
|
|
@@ -57,8 +57,13 @@ Use a new version for a correction. See [the release guide](releasing.md).
|
|
|
57
57
|
before using it.
|
|
58
58
|
- Workflows from all external fork contributors require maintainer approval
|
|
59
59
|
before running. Inspect workflow and code changes before approving a run.
|
|
60
|
-
- CodeQL
|
|
61
|
-
|
|
60
|
+
- CodeQL advanced setup in [.github/workflows/codeql.yml](../.github/workflows/codeql.yml)
|
|
61
|
+
scans GitHub Actions and JavaScript/TypeScript with the default query suite and
|
|
62
|
+
remote threat model. It runs on pushes to `main`, pull requests targeting
|
|
63
|
+
`main` (including forks), and a weekly schedule, with a manual trigger available.
|
|
64
|
+
Default setup must remain disabled because it skips fork pull requests and
|
|
65
|
+
prevents advanced-setup analysis uploads. Fork PR scans use `pull_request`
|
|
66
|
+
and remain subject to the contributor approval policy above.
|
|
62
67
|
- Dependabot alerts/security updates, secret scanning, secret push protection,
|
|
63
68
|
and private vulnerability reporting are enabled. Dependency updates remain
|
|
64
69
|
configured in [.github/dependabot.yml](../.github/dependabot.yml).
|
|
@@ -68,7 +73,8 @@ sign-off nor a CLA is required for contributions.
|
|
|
68
73
|
|
|
69
74
|
## Dependency maintenance
|
|
70
75
|
|
|
71
|
-
Dependabot checks both npm manifests
|
|
76
|
+
Dependabot checks both npm workspace manifests through the root lockfile weekly.
|
|
77
|
+
The internal `@chrok/braid` dependency is updated by the coordinated release process. Pi host packages stay in a separate
|
|
72
78
|
group because even 0.x minor releases can change extension contracts. Other npm
|
|
73
79
|
minor/patch updates are grouped; GitHub Actions updates are grouped monthly and
|
|
74
80
|
retain full commit SHA pins. Grouping does not enable automatic merging.
|
package/docs/resource-limits.md
CHANGED
|
@@ -44,8 +44,10 @@ after exporting needed results. Reloading also cancels outstanding work.
|
|
|
44
44
|
Git worktree preparation, checkpointing, tracked writes, and cleanup add disk,
|
|
45
45
|
Git-process, and elapsed-time overhead. Cleanup may extend wall time beyond a
|
|
46
46
|
model deadline. Automatically appended merge agents are additional model calls
|
|
47
|
-
and share the graph's remaining time
|
|
48
|
-
|
|
47
|
+
and share the graph's remaining time; unchanged worktrees are released without
|
|
48
|
+
an automatic merge call, but still incur workspace and checkpoint overhead.
|
|
49
|
+
Benchmark results measured outside Git do not include this lifecycle. Recoverable
|
|
50
|
+
refs retain Git objects until removed.
|
|
49
51
|
|
|
50
52
|
An aborted runtime slot can be reused even if an uncooperative provider continues
|
|
51
53
|
working. Runners must forward `signal`; neither Braid nor JavaScript can forcibly
|
package/examples/code-review.ts
CHANGED
|
@@ -6,9 +6,9 @@ const diff = "- return cache[key];\n+ return cache[key] ?? await load(key);";
|
|
|
6
6
|
const graph: BraidInput = {
|
|
7
7
|
goal: `Review this proposed cache change:\n${diff}`,
|
|
8
8
|
nodes: [
|
|
9
|
-
{ type: "execute", id: "correctness", prompt: "Review cache semantics and concurrent misses." },
|
|
10
|
-
{ type: "execute", id: "tests", prompt: "Identify regression cases worth testing." },
|
|
11
|
-
{ type: "execute", id: "review", prompt: "Synthesize actionable findings from both reviews." },
|
|
9
|
+
{ type: "execute", id: "correctness", workspace: "read-only", prompt: "Review cache semantics and concurrent misses." },
|
|
10
|
+
{ type: "execute", id: "tests", workspace: "read-only", prompt: "Identify regression cases worth testing." },
|
|
11
|
+
{ type: "execute", id: "review", workspace: "read-only", prompt: "Synthesize actionable findings from both reviews." },
|
|
12
12
|
],
|
|
13
13
|
edges: [{ from: "correctness", to: "review" }, { from: "tests", to: "review" }],
|
|
14
14
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrok/braid",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "A small, framework-agnostic DAG runtime for isolated model invocations",
|
|
6
6
|
"type": "module",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "node scripts/build.mjs",
|
|
34
34
|
"check": "tsc --noEmit",
|
|
35
|
-
"test": "tsx --test test/*.test.ts",
|
|
35
|
+
"test": "tsx --test test/*.test.ts test/*.test.mjs",
|
|
36
36
|
"demo": "tsx examples/basic.ts",
|
|
37
|
-
"check:pi": "npm
|
|
38
|
-
"test:pi": "npm --
|
|
37
|
+
"check:pi": "npm run build && npm run check --workspace @chrok/pi-braid",
|
|
38
|
+
"test:pi": "npm run build && npm test --workspace @chrok/pi-braid",
|
|
39
39
|
"prepack": "npm run build",
|
|
40
|
-
"build:pi": "npm --
|
|
40
|
+
"build:pi": "npm run build --workspace @chrok/pi-braid",
|
|
41
41
|
"test:package": "node scripts/package-smoke.mjs",
|
|
42
42
|
"verify": "npm run check && npm test && npm run check:pi && npm run test:pi && npm run examples && npm run test:package",
|
|
43
43
|
"examples": "tsx examples/basic.ts && tsx examples/code-review.ts && tsx examples/failure-handling.ts && tsx examples/custom-runner.ts",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@types/node": "^22.0.0",
|
|
50
50
|
"tsx": "^4.23.15",
|
|
51
|
-
"typescript": "^5.0.0"
|
|
51
|
+
"typescript": "^5.0.0",
|
|
52
|
+
"@chrok/braid": "file:."
|
|
52
53
|
},
|
|
53
54
|
"repository": {
|
|
54
55
|
"type": "git",
|
|
@@ -68,5 +69,8 @@
|
|
|
68
69
|
"publishConfig": {
|
|
69
70
|
"access": "public",
|
|
70
71
|
"registry": "https://registry.npmjs.org"
|
|
71
|
-
}
|
|
72
|
+
},
|
|
73
|
+
"workspaces": [
|
|
74
|
+
"integrations/pi"
|
|
75
|
+
]
|
|
72
76
|
}
|