@chrok/pi-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/LICENSE +21 -0
- package/README.md +344 -0
- package/dist/integrations/pi/command.js +190 -0
- package/dist/integrations/pi/display.js +417 -0
- package/dist/integrations/pi/index.js +232 -0
- package/dist/integrations/pi/jobs.js +194 -0
- package/dist/integrations/pi/read-tools.js +54 -0
- package/dist/integrations/pi/runner.js +342 -0
- package/dist/integrations/pi/workspaces.js +2 -0
- package/dist/integrations/pi/write-tools.js +74 -0
- package/dist/src/adapters/openai.js +240 -0
- package/dist/src/budgets.js +19 -0
- package/dist/src/index.js +2 -0
- package/dist/src/merge-tools.js +96 -0
- package/dist/src/runtime.js +532 -0
- package/dist/src/types.js +1 -0
- package/dist/src/validate.js +110 -0
- package/dist/src/workspaces.js +494 -0
- package/package.json +64 -0
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,344 @@
|
|
|
1
|
+
# Braid for Pi (`@chrok/pi-braid`)
|
|
2
|
+
|
|
3
|
+
This optional Pi integration runs Braid graphs as background jobs. It registers:
|
|
4
|
+
|
|
5
|
+
- `braid` — submit a complete DAG and immediately receive a `jobId`.
|
|
6
|
+
- `braid_status` — retrieve progress and results with `{ "jobId": "..." }`, or
|
|
7
|
+
omit the ID to list jobs in the current session.
|
|
8
|
+
- `braid_cancel` — cancel a job with `{ "jobId": "..." }`.
|
|
9
|
+
- `/braid [jobId]` — open a live flow panel in interactive Pi.
|
|
10
|
+
|
|
11
|
+
Submission and completion reminders use short session handles such as `job-1`.
|
|
12
|
+
Status, cancellation and the panel accept either that exact handle or the original
|
|
13
|
+
UUID. Unknown IDs report available handles; IDs are never guessed or fuzzy-matched.
|
|
14
|
+
|
|
15
|
+
The parent can continue independent work or finish its response while a job runs.
|
|
16
|
+
On completion, failure, or cancellation, the extension sends a custom
|
|
17
|
+
`system-reminder` containing the job ID and a request to retrieve its results.
|
|
18
|
+
Pi queues it as a follow-up during streaming; when idle, it starts a new agent
|
|
19
|
+
turn automatically. The agent should wait for this reminder rather than poll.
|
|
20
|
+
Stopping the foreground response does not stop background jobs.
|
|
21
|
+
|
|
22
|
+
Jobs live in memory for the current Pi session. Quitting, reloading extensions,
|
|
23
|
+
or switching/forking sessions aborts outstanding work and suppresses its
|
|
24
|
+
reminders. Job IDs cannot be retrieved after that lifecycle ends. They are not
|
|
25
|
+
persistent processes outside Pi.
|
|
26
|
+
|
|
27
|
+
## Live flow panel
|
|
28
|
+
|
|
29
|
+
Run `/braid` to open the newest job, or `/braid <jobId>` to open a specific job.
|
|
30
|
+
The bordered panel keeps the job header and keyboard controls visible while
|
|
31
|
+
you scroll the flow and event log. It refreshes as nodes start, finish, fail,
|
|
32
|
+
and pass outputs downstream.
|
|
33
|
+
Use Left/Right to select jobs, Up/Down or Page Up/Page Down to scroll, `c` to
|
|
34
|
+
cancel the selected job, and Escape or `q` to close the panel. Closing the panel
|
|
35
|
+
leaves jobs running. In RPC or noninteractive modes, use `braid_status`.
|
|
36
|
+
|
|
37
|
+
The panel renders a Mermaid flowchart, node states, elapsed times, context-token
|
|
38
|
+
estimates or provider-reported usage, context-window sizes, filesystem tool-call
|
|
39
|
+
counts, and the execution log. Active nodes are marked `▶ ACTIVE`. The status
|
|
40
|
+
tool also renders a flowchart; expand its result to see more log events.
|
|
41
|
+
|
|
42
|
+
`/braid` now opens this panel; it no longer arms the next prompt. To request
|
|
43
|
+
Braid explicitly, ask the agent to analyze the task using Braid.
|
|
44
|
+
|
|
45
|
+
Braid owns graph validation, scheduling, joins, routing, skip/failure propagation,
|
|
46
|
+
timeouts, Git worktree/checkpoint/merge lifecycle, and result metadata. Pi owns
|
|
47
|
+
model lookup, credentials/OAuth, provider transport, filesystem tool execution,
|
|
48
|
+
and token/cost accounting. The first
|
|
49
|
+
`braid_status` retrieval of a finished job reports its accumulated Pi usage;
|
|
50
|
+
subsequent retrievals do not count the same usage again.
|
|
51
|
+
|
|
52
|
+
## When Pi will use Braid
|
|
53
|
+
|
|
54
|
+
Tool selection is made by the parent Pi model. It is not possible for an
|
|
55
|
+
extension to force a model tool call safely for every prompt. This adapter adds
|
|
56
|
+
an explicit per-turn planning policy to Pi's system prompt and tool metadata:
|
|
57
|
+
|
|
58
|
+
- for code reviews, bug investigations, design comparisons, test planning, or
|
|
59
|
+
changes spanning multiple files, call Braid first when two or more concerns
|
|
60
|
+
can be handled independently; nodes can inspect the project and edit isolated
|
|
61
|
+
worktrees in Git repositories;
|
|
62
|
+
- do not use Braid for simple one-step answers, trivial direct edits, or shell
|
|
63
|
+
work; keep tests and shell commands in the parent agent;
|
|
64
|
+
- the user does not need to say “Braid” or design the graph;
|
|
65
|
+
- when Braid fits, the model should construct and submit the complete graph
|
|
66
|
+
immediately, continue independent work, and retrieve the terminal outputs after
|
|
67
|
+
the completion reminder.
|
|
68
|
+
|
|
69
|
+
This is a recommendation to the model, not hard enforcement. If a model still
|
|
70
|
+
ignores the policy, use a short instruction such as “decompose this with Braid”
|
|
71
|
+
or strengthen the project/system prompt for that model. The adapter explicitly
|
|
72
|
+
asks the model to make the delegation choice before directly inspecting the
|
|
73
|
+
repository. Do not add a generic `always call braid` rule: that would waste
|
|
74
|
+
model calls and bypass direct tools.
|
|
75
|
+
Merge agents review and integrate node changes; the parent reviews results and runs tests.
|
|
76
|
+
Each node gets a new Pi AI context containing only the Braid goal, its node prompt,
|
|
77
|
+
labelled direct predecessor outputs, and workspace metadata. It receives Pi's
|
|
78
|
+
`read` and `ls`, plus `grep` when local `rg` is available and `find` when
|
|
79
|
+
local `fd`/`fdfind` is available. Missing search dependencies are reported in the
|
|
80
|
+
node prompt, with `ls`/`read` as alternatives. Dependencies are checked before
|
|
81
|
+
exposing search tools and again before executing them; missing tools are not
|
|
82
|
+
installed by Braid. Git worktrees additionally receive `write` and `edit`.
|
|
83
|
+
It receives no parent transcript, shell tools, test runner, skills, or arbitrary
|
|
84
|
+
code execution. Decision nodes additionally receive `decide`. Git nodes receive
|
|
85
|
+
local Git inspection; merge nodes also receive Git integration commands and
|
|
86
|
+
`finish_merge`. Merge agents receive bounded changed-file lists, diff statistics
|
|
87
|
+
and previews, plus the source checkout's dirty status. The model-facing `git`
|
|
88
|
+
tool has a role-specific `command` enum and separate `args`; `finish_merge` lists
|
|
89
|
+
only the current source IDs and diagnoses missing, duplicate or unexpected IDs.
|
|
90
|
+
|
|
91
|
+
## Install from npm
|
|
92
|
+
|
|
93
|
+
Requires Node.js 22.19+ and Pi 0.85.1 (the tested version):
|
|
94
|
+
|
|
95
|
+
```sh
|
|
96
|
+
pi install npm:@chrok/pi-braid
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Add `-l` for a project-local installation. Run `/reload` after installation.
|
|
100
|
+
The package includes compiled Braid core code from the matching release; it does
|
|
101
|
+
not depend on a source checkout. Pi supplies its core peer packages at runtime.
|
|
102
|
+
Their wildcard ranges follow Pi's packaging convention, not universal version
|
|
103
|
+
compatibility. Development and CI pin Pi 0.85.1.
|
|
104
|
+
|
|
105
|
+
## Install this local checkout in Pi
|
|
106
|
+
|
|
107
|
+
From the repository root:
|
|
108
|
+
|
|
109
|
+
```sh
|
|
110
|
+
npm ci
|
|
111
|
+
npm ci --prefix integrations/pi
|
|
112
|
+
npm run build:pi
|
|
113
|
+
pi install ./integrations/pi
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Run these commands from the repository root. To install for only one project,
|
|
117
|
+
add `-l`:
|
|
118
|
+
|
|
119
|
+
```sh
|
|
120
|
+
pi install -l ./integrations/pi
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
For local installations, rebuild with `npm run build:pi` after source edits,
|
|
124
|
+
then reload Pi. In Pi, run `/reload`. Restart Pi if the package was installed into an
|
|
125
|
+
already running process and the tool does not appear. Inspect installation with:
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
pi list
|
|
129
|
+
pi config
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The extension loads its own compiled `dist/` and the Pi host dependencies.
|
|
133
|
+
`npm ci --prefix integrations/pi` installs the pinned development environment;
|
|
134
|
+
use `npm install --prefix integrations/pi` when intentionally updating its lockfile. The adapter uses the `grok-mermaid` terminal renderer for Mermaid flowcharts.
|
|
135
|
+
The local install is trusted code: Pi extensions execute with the process's full
|
|
136
|
+
permissions.
|
|
137
|
+
|
|
138
|
+
## Test the adapter without spending money
|
|
139
|
+
|
|
140
|
+
After both `npm ci` commands above, verify the core and adapter:
|
|
141
|
+
|
|
142
|
+
```sh
|
|
143
|
+
npm run check
|
|
144
|
+
npm test
|
|
145
|
+
npm run build
|
|
146
|
+
npm run check:pi
|
|
147
|
+
npm run test:pi
|
|
148
|
+
npm run demo
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The Pi adapter tests use a fake model registry and assert exact model lookup,
|
|
152
|
+
fresh contexts, tool isolation, decision handling, filesystem tool execution,
|
|
153
|
+
continuation behavior, usage aggregation, context-token progress, and tool counts.
|
|
154
|
+
Renderer tests cover Mermaid topology, live active-node highlighting,
|
|
155
|
+
handoff/failure logs, expanded per-node output, and bounded event previews.
|
|
156
|
+
Background-job tests cover immediate submission, foreground independence,
|
|
157
|
+
completion reminders, explicit cancellation, shutdown, usage accounting, and
|
|
158
|
+
large-result retrieval. Panel tests cover navigation, scrolling, and cleanup.
|
|
159
|
+
They make no provider requests.
|
|
160
|
+
|
|
161
|
+
## Node filesystem capabilities
|
|
162
|
+
|
|
163
|
+
Core owns workspace preparation, checkpointing, serialization, and cleanup for
|
|
164
|
+
all integrations. Pi exposes `read` and `ls` in all directories, plus search tools whose local dependencies are available.
|
|
165
|
+
In Git, execute and decision nodes also get `write`/`edit` restricted to their own
|
|
166
|
+
detached worktree, plus local Git inspection. Outside Git, filesystem tools stay
|
|
167
|
+
read-only. Nodes never receive shell commands or a test runner.
|
|
168
|
+
|
|
169
|
+
The initial snapshot includes tracked staged/unstaged changes, deletions, and
|
|
170
|
+
non-ignored untracked files. It preserves the source index and files. Ignored
|
|
171
|
+
files are not copied; submodules are not initialized or recursively snapshotted,
|
|
172
|
+
and Pi rejects writes inside them to keep checkpoint recovery complete.
|
|
173
|
+
Every worker shares that baseline until a merge ends, after which new workers
|
|
174
|
+
snapshot the current source checkout. Uncommitted predecessor changes are not
|
|
175
|
+
implicitly applied to downstream workers. Their paths and checkpoint refs are
|
|
176
|
+
available as context for inspection.
|
|
177
|
+
|
|
178
|
+
A `merge` node accepts multiple predecessors and an optional prompt/model. It
|
|
179
|
+
operates in the source checkout, with guarded `write`/`edit` and local `git`
|
|
180
|
+
commands (`add`, `commit`, `merge`, `cherry-pick`, `apply`, `restore`, plus
|
|
181
|
+
inspection). The agent decides which changes to use and how to integrate them.
|
|
182
|
+
Core never automatically merges or cherry-picks. The agent must call
|
|
183
|
+
`finish_merge` with `integrated`, `discarded`, or `archived` and a reason for every
|
|
184
|
+
source. Tool errors and conflicts go back to the agent for recovery. Failed
|
|
185
|
+
predecessors pass errors and partial work along unconditional edges.
|
|
186
|
+
|
|
187
|
+
Core removes processed source worktrees after the merge agent finishes. If any
|
|
188
|
+
worktrees remain after declared nodes settle, core appends a final merge agent.
|
|
189
|
+
Its model, tool calls, budgets, events, and usage behave like any other node.
|
|
190
|
+
Missing finish calls, unresolved conflicts, or archived sources fail the merge.
|
|
191
|
+
Cancellation, timeout, and failure archive remaining changes and clean worktrees;
|
|
192
|
+
they do not start new merge agents after graph cancellation.
|
|
193
|
+
|
|
194
|
+
`braid_status` includes core's `workspaces` map with workspace paths, states,
|
|
195
|
+
reasons, `checkpointRef`, and pre-merge `backupRef`. The panel distinguishes active
|
|
196
|
+
worktrees from cleaned workspaces. Worktrees use
|
|
197
|
+
`os.tmpdir()/braid-workspaces-*/<unique-id>`; after removal their contents remain
|
|
198
|
+
recoverable from `refs/braid/checkpoints/*`. Use `git show <checkpointRef>:<path>`
|
|
199
|
+
or `git diff <snapshotCommit> <checkpointRef>` to inspect archived changes.
|
|
200
|
+
Remove individual recovery refs with `git update-ref -d <ref>` once reviewed.
|
|
201
|
+
|
|
202
|
+
A failed merge does not reset partial changes or conflict state in the source
|
|
203
|
+
checkout. Its `backupRef` preserves the pre-agent snapshot. Cleanup errors report
|
|
204
|
+
retained paths instead of silently claiming success. A process crash cannot run
|
|
205
|
+
cleanup. The merge mutex coordinates runs in the same process only; avoid parent
|
|
206
|
+
edits to the source checkout while a merge agent is running.
|
|
207
|
+
|
|
208
|
+
File writes reject external paths, Git metadata, symlinks, hard links, and special
|
|
209
|
+
files. Read access follows Pi's normal permissions. This does not replace an OS
|
|
210
|
+
sandbox against concurrent filesystem attacks. For programmatic use, pass
|
|
211
|
+
`createPiRunner(...)` to core `braid(..., { cwd, runner })`; calling the runner
|
|
212
|
+
directly without a core workspace gives read-only capabilities.
|
|
213
|
+
|
|
214
|
+
Tool and time budgets are unlimited by default in Pi. To set finite hard limits,
|
|
215
|
+
pass any of these fields in the `braid` tool's `options`:
|
|
216
|
+
|
|
217
|
+
| Option | Meaning |
|
|
218
|
+
| --- | --- |
|
|
219
|
+
| `maxToolRounds` | Maximum assistant responses containing tool calls, per node |
|
|
220
|
+
| `maxToolCalls` | Maximum total requested tool calls, per node |
|
|
221
|
+
| `nodeTimeoutMs` | Time allowed for each node after it starts, in milliseconds |
|
|
222
|
+
| `graphTimeoutMs` | Time allowed for the entire graph, including queueing, in milliseconds |
|
|
223
|
+
|
|
224
|
+
For example, `options: { maxToolRounds: 20, maxToolCalls: 60, nodeTimeoutMs: 120000 }`.
|
|
225
|
+
Omit a field for no limit; programmatic runner/job options also accept `Infinity`.
|
|
226
|
+
Tool limits must be positive safe integers. Counts include `decide`, `git`,
|
|
227
|
+
`finish_merge`, and rejected
|
|
228
|
+
tool requests. A batch exceeding either tool limit is rejected before execution
|
|
229
|
+
and fails the node; a final text response is still allowed at the exact limit.
|
|
230
|
+
|
|
231
|
+
When any budget is finite, the worker's system prompt contains a `system-reminder`
|
|
232
|
+
before its first model call and refreshes it before each continuation. It reports
|
|
233
|
+
finite tool limits and remaining rounds/calls, and remaining node/graph time.
|
|
234
|
+
Workers must reserve a call for `decide` or `finish_merge` when required and finish
|
|
235
|
+
within the remaining budgets. Graph time is shared across all nodes; a queued
|
|
236
|
+
node receives the remaining graph time, not a fresh graph timeout. Reminders do
|
|
237
|
+
not extend deadlines or interrupt an in-flight model response.
|
|
238
|
+
|
|
239
|
+
## Test real Pi models
|
|
240
|
+
|
|
241
|
+
Authenticate Pi normally first, for example with `/login`, an environment key,
|
|
242
|
+
or an existing `~/.pi/agent/auth.json`. Select a model with `/model`. Braid uses
|
|
243
|
+
that exact current model by default. Per-node overrides use an exact
|
|
244
|
+
`provider/modelId`, for example `anthropic/claude-sonnet-4-5` or
|
|
245
|
+
`openai/gpt-5-mini`.
|
|
246
|
+
|
|
247
|
+
Ask Pi to make one very small test call (this explicitly names Braid so it
|
|
248
|
+
also verifies the tool wiring):
|
|
249
|
+
|
|
250
|
+
```text
|
|
251
|
+
Use the braid tool with this graph:
|
|
252
|
+
- goal: "Return the word PASS."
|
|
253
|
+
- one execute node: id "check", prompt "Return exactly PASS."
|
|
254
|
+
- no edges
|
|
255
|
+
Use the currently selected Pi model. Do not use any other tool.
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Pi should call `braid` and show a completed result whose terminal output is
|
|
259
|
+
keyed by `check`. This is a billable model request. A slightly richer routing
|
|
260
|
+
test is:
|
|
261
|
+
|
|
262
|
+
```text
|
|
263
|
+
Use braid with this complete graph. Goal: "Test routing."
|
|
264
|
+
Nodes:
|
|
265
|
+
1. decision id route, prompt "Select go and then explain briefly", choices ["go", "stop"]
|
|
266
|
+
2. execute id left, prompt "Return LEFT"
|
|
267
|
+
3. execute id right, prompt "Return RIGHT"
|
|
268
|
+
4. execute id join, prompt "List your predecessor IDs and outputs"
|
|
269
|
+
Edges:
|
|
270
|
+
- route -> left labelled choice go
|
|
271
|
+
- route -> right labelled choice stop
|
|
272
|
+
- left -> join
|
|
273
|
+
Ask the decision node to choose go. Use the current Pi model.
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Expected behavior:
|
|
277
|
+
|
|
278
|
+
- `route` completes with decision `go`.
|
|
279
|
+
- `left` runs.
|
|
280
|
+
- `right` is `skipped` with reason `inactive`.
|
|
281
|
+
- `join` receives only `left` as a labelled predecessor.
|
|
282
|
+
- `join` appears in `terminalOutputs`.
|
|
283
|
+
- The Pi TUI shows a compact graph summary rather than the full input JSON.
|
|
284
|
+
- Submission returns a job ID immediately. Open `/braid` to see active nodes
|
|
285
|
+
and the execution log update with starts and handoffs.
|
|
286
|
+
- Completion sends a reminder and resumes an idle agent; `braid_status` retrieves
|
|
287
|
+
the finished result.
|
|
288
|
+
- The result shows completion/failure, node counts, terminal IDs, decisions, failures, skips, and the execution log. Expand the result row to see per-node output and more events.
|
|
289
|
+
|
|
290
|
+
To test per-node model selection, add `model: "provider/modelId"` to one node.
|
|
291
|
+
Use an exact ID shown by `/model` or `pi --list-models`; an unrecognized model
|
|
292
|
+
fails that node cleanly and does not invoke another provider.
|
|
293
|
+
|
|
294
|
+
## Cancellation and limits
|
|
295
|
+
|
|
296
|
+
The Pi extension has no node or graph time limit by default. Use `braid_cancel`
|
|
297
|
+
or press `c` in the flow panel to abort running nodes and mark queued nodes
|
|
298
|
+
`cancelled`. Escape stops the foreground response or closes the panel without
|
|
299
|
+
cancelling jobs. To set a node or graph timeout, supply milliseconds in the
|
|
300
|
+
submission tool input under `options`; each omitted timeout remains unlimited:
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{
|
|
304
|
+
"maxConcurrency": 2,
|
|
305
|
+
"nodeTimeoutMs": 30000,
|
|
306
|
+
"graphTimeoutMs": 120000
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The adapter sets `maxRetries: 0`, `cacheRetention: "none"`, and gives each node a
|
|
311
|
+
fresh provider context. Tool budgets are unlimited unless `maxToolRounds` or
|
|
312
|
+
`maxToolCalls` is set. Missing paths, invalid arguments, disallowed writes, and
|
|
313
|
+
unavailable tools are returned as tool errors so the node can recover. Provider
|
|
314
|
+
authentication and calls may still incur normal provider costs. Braid cannot
|
|
315
|
+
forcibly stop synchronous JavaScript or a remote provider that ignores
|
|
316
|
+
cancellation. Cancellation prevents further tool calls but does not roll back
|
|
317
|
+
writes already made. Core waits for tracked writes, preserves checkpoints, and
|
|
318
|
+
removes worker worktrees before completing cancellation.
|
|
319
|
+
|
|
320
|
+
The tool output is capped at 50KB/2000 lines to protect Pi context. When exceeded,
|
|
321
|
+
the adapter writes a full JSON result to a temporary file (mode 600 on POSIX;
|
|
322
|
+
inherited ACLs on Windows) and includes
|
|
323
|
+
its path in the tool output.
|
|
324
|
+
|
|
325
|
+
For a test of proactive selection, start a fresh Pi turn with a task such as:
|
|
326
|
+
|
|
327
|
+
```text
|
|
328
|
+
Compare these three proposed designs, identify independent risks for each,
|
|
329
|
+
and finish with a recommendation. You may use the available execution
|
|
330
|
+
primitives when they improve the result.
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
The adapter's policy tells Pi to consider Braid because this task has multiple
|
|
334
|
+
independent reasoning branches and a final synthesis. Whether it actually calls
|
|
335
|
+
the tool remains model-dependent; inspect the transcript for the `braid` tool
|
|
336
|
+
submission, completion reminder, and the live graph/handoff log in `/braid`.
|
|
337
|
+
|
|
338
|
+
## Live end-to-end tests
|
|
339
|
+
|
|
340
|
+
The reusable RPC suite is in [test/live/README.md](test/live/README.md). It uses
|
|
341
|
+
your local Pi installation and configured credentials with real provider calls.
|
|
342
|
+
Run it explicitly; it is separate from the deterministic tests and incurs model
|
|
343
|
+
usage. It saves prompts, actual Braid parameters, node/tool transcripts, Git/file
|
|
344
|
+
assertions and a Markdown report in a temporary output directory.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, } from "@earendil-works/pi-tui";
|
|
2
|
+
import { renderGraphResult } from "./display.js";
|
|
3
|
+
// Match the overlay height exactly so Pi never clips the footer or bottom border.
|
|
4
|
+
const panelHeight = (rows) => Math.max(1, Math.floor(rows * 0.9));
|
|
5
|
+
const plain = (value) => stripTerminalSequences(value).replace(/\p{Cc}/gu, " ");
|
|
6
|
+
/** A live, scrollable panel. Closing the panel leaves jobs running. */
|
|
7
|
+
export class BraidPanel {
|
|
8
|
+
jobs;
|
|
9
|
+
tui;
|
|
10
|
+
theme;
|
|
11
|
+
done;
|
|
12
|
+
selected;
|
|
13
|
+
offset = 0;
|
|
14
|
+
maxOffset = 0;
|
|
15
|
+
disposed = false;
|
|
16
|
+
unsubscribe;
|
|
17
|
+
ticker;
|
|
18
|
+
constructor(jobs, tui, theme, done, jobId) {
|
|
19
|
+
this.jobs = jobs;
|
|
20
|
+
this.tui = tui;
|
|
21
|
+
this.theme = theme;
|
|
22
|
+
this.done = done;
|
|
23
|
+
this.selected = jobId ? jobs.get(jobId)?.jobId ?? jobId : undefined;
|
|
24
|
+
this.unsubscribe = jobs.subscribe(() => this.refresh());
|
|
25
|
+
this.ticker = setInterval(() => this.refresh(), 1000);
|
|
26
|
+
this.ticker.unref();
|
|
27
|
+
}
|
|
28
|
+
refresh() {
|
|
29
|
+
if (!this.disposed)
|
|
30
|
+
this.tui.requestRender();
|
|
31
|
+
}
|
|
32
|
+
render(width) {
|
|
33
|
+
const rows = panelHeight(this.tui.terminal.rows);
|
|
34
|
+
if (width < 8 || rows < 4) {
|
|
35
|
+
return [truncateToWidth("Braid · Esc close", width, "")];
|
|
36
|
+
}
|
|
37
|
+
const innerWidth = width - 2;
|
|
38
|
+
const contentWidth = width - 4;
|
|
39
|
+
const border = (value) => this.theme.fg("borderMuted", value);
|
|
40
|
+
const rule = (left, right, label = "") => {
|
|
41
|
+
const title = truncateToWidth(label ? ` ${label} ` : "", innerWidth, "");
|
|
42
|
+
return (border(left) +
|
|
43
|
+
this.theme.fg("accent", this.theme.bold(title)) +
|
|
44
|
+
border("─".repeat(innerWidth - visibleWidth(title)) + right));
|
|
45
|
+
};
|
|
46
|
+
const row = (value) => border("│") +
|
|
47
|
+
" " +
|
|
48
|
+
truncateToWidth(value, contentWidth, "…", true) +
|
|
49
|
+
" " +
|
|
50
|
+
border("│");
|
|
51
|
+
const jobs = this.jobs.list();
|
|
52
|
+
const summary = jobs.find((job) => job.jobId === this.selected) ?? jobs[0];
|
|
53
|
+
const current = summary ? this.jobs.get(summary.jobId) : undefined;
|
|
54
|
+
this.selected = current?.jobId;
|
|
55
|
+
const index = jobs.findIndex((job) => job.jobId === current?.jobId);
|
|
56
|
+
const statusColor = current?.status === "running"
|
|
57
|
+
? "warning"
|
|
58
|
+
: current?.status === "completed"
|
|
59
|
+
? "success"
|
|
60
|
+
: current?.status === "failed"
|
|
61
|
+
? "error"
|
|
62
|
+
: "muted";
|
|
63
|
+
const heading = current
|
|
64
|
+
? `Job ${index + 1} of ${jobs.length} · ${this.theme.fg(statusColor, current.status)}`
|
|
65
|
+
: this.theme.fg("muted", "No background jobs");
|
|
66
|
+
// Reserve a body row and the close hint even in a very short terminal.
|
|
67
|
+
if (rows < 10) {
|
|
68
|
+
const compact = [
|
|
69
|
+
heading,
|
|
70
|
+
...(current
|
|
71
|
+
? [plain(current.goal)]
|
|
72
|
+
: ["Ask the agent to run a Braid graph."]),
|
|
73
|
+
];
|
|
74
|
+
return [
|
|
75
|
+
rule("╭", "╮", "Braid"),
|
|
76
|
+
...compact.slice(0, rows - 3).map(row),
|
|
77
|
+
row("Esc close"),
|
|
78
|
+
rule("╰", "╯"),
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
const header = [
|
|
82
|
+
rule("╭", "╮", "Braid"),
|
|
83
|
+
row(heading),
|
|
84
|
+
row(current ? plain(current.goal) : "Ask the agent to run a Braid graph."),
|
|
85
|
+
row(this.theme.fg("dim", current
|
|
86
|
+
? current.jobId
|
|
87
|
+
: "Jobs will appear here as soon as they are submitted.")),
|
|
88
|
+
rule("├", "┤"),
|
|
89
|
+
];
|
|
90
|
+
const content = current
|
|
91
|
+
? [
|
|
92
|
+
...(current.error
|
|
93
|
+
? [this.theme.fg("error", plain(current.error))]
|
|
94
|
+
: []),
|
|
95
|
+
...renderGraphResult({
|
|
96
|
+
...(current.result ?? current.live),
|
|
97
|
+
progress: current.live.progress,
|
|
98
|
+
...(current.workspaces ? { workspaces: current.workspaces } : {}),
|
|
99
|
+
...(current.fullOutputPath
|
|
100
|
+
? { fullOutputPath: current.fullOutputPath }
|
|
101
|
+
: {}),
|
|
102
|
+
}, true, false, this.theme).render(contentWidth),
|
|
103
|
+
]
|
|
104
|
+
: ["No Braid jobs in this session."];
|
|
105
|
+
const height = rows - header.length - 4;
|
|
106
|
+
this.maxOffset = Math.max(0, content.length - height);
|
|
107
|
+
this.offset = Math.min(this.offset, this.maxOffset);
|
|
108
|
+
const body = content.slice(this.offset, this.offset + height);
|
|
109
|
+
while (body.length < height)
|
|
110
|
+
body.push("");
|
|
111
|
+
const range = `Lines ${this.offset + 1}–${Math.min(content.length, this.offset + height)}/${content.length}`;
|
|
112
|
+
const help = contentWidth >= 72
|
|
113
|
+
? "←/→ jobs ↑/↓ scroll PgUp/PgDn page c cancel Esc close"
|
|
114
|
+
: contentWidth >= 42
|
|
115
|
+
? "←/→ jobs · ↑/↓ scroll · c cancel · Esc close"
|
|
116
|
+
: "↑/↓ scroll · Esc close";
|
|
117
|
+
return [
|
|
118
|
+
...header,
|
|
119
|
+
...body.map(row),
|
|
120
|
+
rule("├", "┤"),
|
|
121
|
+
row(this.theme.fg("dim", `${range}${contentWidth >= 60 ? " · Jobs keep running when this panel closes" : ""}`)),
|
|
122
|
+
row(this.theme.fg("muted", help)),
|
|
123
|
+
rule("╰", "╯"),
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
handleInput(data) {
|
|
127
|
+
if (matchesKey(data, "escape") ||
|
|
128
|
+
data === "q" ||
|
|
129
|
+
matchesKey(data, "ctrl+c")) {
|
|
130
|
+
this.dispose();
|
|
131
|
+
this.done();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const jobs = this.jobs.list();
|
|
135
|
+
const index = Math.max(0, jobs.findIndex((job) => job.jobId === this.selected));
|
|
136
|
+
if (matchesKey(data, "left") || matchesKey(data, "right")) {
|
|
137
|
+
const step = matchesKey(data, "right") ? 1 : -1;
|
|
138
|
+
this.selected = jobs[(index + step + jobs.length) % jobs.length]?.jobId;
|
|
139
|
+
this.offset = 0;
|
|
140
|
+
}
|
|
141
|
+
else if (matchesKey(data, "up"))
|
|
142
|
+
this.offset = Math.max(0, this.offset - 1);
|
|
143
|
+
else if (matchesKey(data, "down"))
|
|
144
|
+
this.offset = Math.min(this.maxOffset, this.offset + 1);
|
|
145
|
+
else if (matchesKey(data, "pageUp"))
|
|
146
|
+
this.offset = Math.max(0, this.offset - 10);
|
|
147
|
+
else if (matchesKey(data, "pageDown"))
|
|
148
|
+
this.offset = Math.min(this.maxOffset, this.offset + 10);
|
|
149
|
+
else if (data === "c" && this.selected)
|
|
150
|
+
this.jobs.cancel(this.selected);
|
|
151
|
+
this.refresh();
|
|
152
|
+
}
|
|
153
|
+
invalidate() { }
|
|
154
|
+
dispose() {
|
|
155
|
+
if (this.disposed)
|
|
156
|
+
return;
|
|
157
|
+
this.disposed = true;
|
|
158
|
+
clearInterval(this.ticker);
|
|
159
|
+
this.unsubscribe();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
export function registerBraidCommand(pi, jobs) {
|
|
163
|
+
pi.registerCommand("braid", {
|
|
164
|
+
description: "Open the live background-job flow panel: /braid [jobId]",
|
|
165
|
+
handler: async (args, ctx) => {
|
|
166
|
+
if (ctx.mode !== "tui")
|
|
167
|
+
throw new Error("The Braid panel requires interactive Pi. Use braid_status for job status.");
|
|
168
|
+
const jobId = args.trim() || undefined;
|
|
169
|
+
if (jobId && !jobs.get(jobId))
|
|
170
|
+
throw jobs.unknownJob(jobId);
|
|
171
|
+
let panel;
|
|
172
|
+
try {
|
|
173
|
+
await ctx.ui.custom((tui, theme, _keys, done) => {
|
|
174
|
+
panel = new BraidPanel(jobs, tui, theme, done, jobId);
|
|
175
|
+
return panel;
|
|
176
|
+
}, {
|
|
177
|
+
overlay: true,
|
|
178
|
+
overlayOptions: {
|
|
179
|
+
anchor: "center",
|
|
180
|
+
width: "95%",
|
|
181
|
+
maxHeight: "90%",
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
panel?.dispose();
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|