@thedevlabs-io/labd 0.3.1

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/AGENTS.md ADDED
@@ -0,0 +1,264 @@
1
+ # AGENTS.md - labd-cli
2
+
3
+ Guidance for AI coding agents (and humans) working in this repository. This is
4
+ the single source of truth for the project's conventions; `CLAUDE.md` imports
5
+ this file so Claude Code and other tools share one set of rules.
6
+
7
+ ## What this is
8
+
9
+ **labd-cli** is labd in a terminal - the third front door to the same assistant
10
+ that lives in the learning portal and in the VS Code extension.
11
+
12
+ The word "same" is load-bearing. This is **not** a second assistant that happens
13
+ to look similar. It talks to the same server at `agent.thedevlabs.io`, over the
14
+ same protocol, with the same prompt, the same memory, the same credit ledger, and
15
+ the same permission model. A learner who asks labd something in their editor and
16
+ then asks a follow-up in their terminal is talking to one assistant.
17
+
18
+ That is why the interesting constraint in this repo is what it must NOT contain:
19
+
20
+ - **no system prompt.** It lives in `@labd/protocol`, and the server builds it.
21
+ - **no model choice, and no way to learn which model answered.** The protocol has
22
+ no model field; upstream errors are summarised by the server, never forwarded.
23
+ - **no second definition of the permission matrix.** `needsApproval` comes from
24
+ the protocol. `ask` / `auto` / `agent` mean exactly what they mean in the editor.
25
+ - **no second definition of what commands are refused.** `classifyCommand` comes
26
+ from the protocol too.
27
+
28
+ If you are about to add assistant *behaviour* here - how it teaches, what it
29
+ refuses, what it is told about the project - stop. It belongs in the
30
+ [labd](https://github.com/thedevlabs-io/labd) repo, in `packages/protocol`, where
31
+ both other surfaces get it too. This repo gathers context, renders a terminal, and
32
+ executes tools.
33
+
34
+ ## Layout
35
+
36
+ ```
37
+ packages/core/ the agent loop, tools, permission checks, sessions, checkpoints
38
+ packages/tui/ terminal rendering - differential redraw, markdown, cards
39
+ packages/cli/ the binary: config, auth, extensions, and the four run modes
40
+ vendor/labd/ submodule - the labd repo, for @labd/protocol
41
+ test/ vitest; `e2e.test.ts` runs the built binary against a fake server
42
+ ```
43
+
44
+ The core/tui split is not tidiness. `packages/core` has no idea a terminal exists,
45
+ which is what lets the interactive session, `labd -p`, and RPC mode drive one loop -
46
+ and what makes the safety-critical parts testable without a screen.
47
+
48
+ ## The protocol comes from a submodule
49
+
50
+ `@labd/protocol` is a private workspace package in the labd repo, so it is
51
+ consumed here as a git submodule pinned to a commit:
52
+
53
+ ```bash
54
+ git submodule update --init # after cloning
55
+ ```
56
+
57
+ Why a submodule rather than a copy: the protocol defines the permission matrix,
58
+ the command denylist, and the path normalisation this CLI enforces. A copy would
59
+ drift, and the way it would drift is by disagreeing with the editor about what
60
+ labd is allowed to do. Bumping the pin is how this repo picks up a protocol change,
61
+ and it should be its own commit so a promotion says plainly that the contract moved.
62
+
63
+ **CI needs a credential for it.** The labd repo is private and the automatic
64
+ `GITHUB_TOKEN` is scoped to one repository, so CI cannot clone the submodule with
65
+ it - the failure reads as "Repository not found", which looks like a broken
66
+ submodule rather than a missing secret. The workflow therefore fetches it in its
67
+ own step and fails with an explanation. Add either a fine-grained PAT with read
68
+ access to `thedevlabs-io/labd` as the repository secret `SUBMODULE_TOKEN`, or a
69
+ deploy key on that repo with its private key as the secret.
70
+
71
+ ## The terminal is a different PLACE, not a second editor
72
+
73
+ `surface: 'cli'` exists for one reason. The editor's prompt says labd can see "the
74
+ lines they have on screen", their selection, and the errors the editor reports.
75
+ None of that is true in a terminal, and a CLI reporting `surface: 'vscode'` would
76
+ get a model confidently reasoning about a screen that is not there.
77
+
78
+ So the CLI's honest context is the **project**: its name, the branch and recent
79
+ commits, the conventions files, the names of any skills. Everything else labd reads
80
+ on purpose, with a tool, and says which file it read. Resist adding anything
81
+ screen-shaped - "the last file you edited" is a guess dressed as context.
82
+
83
+ The RULES for gathering it are the protocol's, not ours: which files are secrets
84
+ (`SECRETISH`), which directories are off limits, which conventions files to read
85
+ and how much of them, what a skill looks like, how a branch is described. This
86
+ repo keeps only the filesystem half - opening the files - so the editor and the
87
+ terminal cannot tell the model two different stories about the same project.
88
+
89
+ When a session ends - `/quit`, the end of `labd -p`, an RPC host closing stdin -
90
+ the conversation is handed to `/v1/conversations/close` with tool results
91
+ stubbed, exactly as the editor does when a chat is left. That call is what makes
92
+ "same memory" true here; do not make it optional.
93
+
94
+ ## Permissions - the reason this repo exists
95
+
96
+ pi.dev, the closest thing to prior art for a minimal terminal harness, is explicit
97
+ that it has **no permission system**: it runs with the invoking user's rights and
98
+ tells you to containerise it. That is a reasonable stance for a tool used by the
99
+ person who wrote it. labd is used by learners in repositories they did not write,
100
+ so the permission model is a feature rather than an omission, and these rules hold:
101
+
102
+ | mode | changes files | runs a command |
103
+ |---|---|---|
104
+ | `ask` | asks | asks |
105
+ | `auto` (default) | applies | asks |
106
+ | `agent` | applies | runs |
107
+
108
+ "Changes files" is all four writes: `edit_file`, `create_file`, `move_file`, and
109
+ `delete_file`. Each is snapshotted, so `/undo` puts the whole turn back.
110
+
111
+ `auto` is the default because `ask` made a real piece of work - reorganise this,
112
+ rename that and fix the callers - into thirty approvals, which is a reason to give
113
+ up halfway rather than a safety property. One exception, in `unattendedMode`: a
114
+ run with nobody watching (`labd -p`, `--mode rpc`) falls back to `ask` unless a
115
+ mode was actually chosen in a settings file or on the command line. A script has
116
+ no one to show a diff to.
117
+
118
+ - **The mode decides who clicks; it never decides what labd will do.**
119
+ `classifyCommand` refuses `sudo`, deploys, publishes, `git push`, disk wipes,
120
+ backgrounding, anything that never exits, anything that hides what it runs, and
121
+ anything naming a credentials file - in **all three** modes, `agent` included.
122
+ - **The mode is never sent to the server.** Not in `ChatRequest`, not in the
123
+ prompt. A setting that governs how much we check the model's work is exactly the
124
+ lever an instruction smuggled into a file would reach for. `e2e.test.ts` asserts
125
+ it never appears on the wire.
126
+ - **A project cannot set the mode.** `.labd/settings.json` may set a timeout or a
127
+ server URL; `mode` is read from the GLOBAL settings and the command line only.
128
+ A repository that could set `mode: "agent"` would be a repository that runs
129
+ commands on the machine of anyone who clones it and types `labd`. See
130
+ `config.ts`, and the test that pins it.
131
+ - **A project cannot load its own code until trusted.** `.labd/extensions` needs
132
+ `labd trust` first. `git clone && labd` must not run a stranger's extension.
133
+ - **A project cannot redirect the token until trusted.** `serverUrl` in
134
+ `.labd/settings.json` is honoured only for a trusted project; otherwise it is
135
+ ignored and the banner says so. The bearer token goes wherever that URL points,
136
+ so a checked-in URL is a checked-in token exfiltration for anyone who clones and
137
+ types `labd`. Same threat class as project extensions, same gate.
138
+ - **Extensions add commands and observers, never tools.** The tool list is the
139
+ protocol's: the server offers the model exactly the capabilities this client
140
+ declares, from one schema both surfaces share. A `registerTool` here would have
141
+ no route to the model and would need its own approval rule, which is a second
142
+ definition of the permission matrix. An extension tool is therefore a protocol
143
+ change, and belongs in labd first. `registerCommand` cannot shadow a built-in,
144
+ because `/mode` and `/undo` have to mean what the learner expects.
145
+ - **Unattended means declined.** `labd -p` has no way to ask, so every call that
146
+ needs approval is refused and the exit code is 2. There is deliberately no
147
+ `--yes`: the way to let an automated run write is to choose a mode in a settings
148
+ file, which is a decision with a record, not a flag buried in a shell script.
149
+
150
+ ## Writes land on disk, so checkpoints are not optional
151
+
152
+ The extension edits an unsaved buffer, so a wrong edit costs one keystroke. There
153
+ is no buffer here. Four rules follow, and `checkpoints.ts` explains each at the
154
+ point it applies:
155
+
156
+ - capture happens immediately before the write, off the text about to be replaced;
157
+ - **capture can never fail a write** - an unsnapshotted file is recorded as such
158
+ and refused later, rather than blocking the edit;
159
+ - conflicts are decided by **content hash**, never mtime, and a file the learner
160
+ edited since is left alone with a reason;
161
+ - a restore is itself reversible, because an undo that cannot be undone is a trap.
162
+
163
+ `run_command` is the exception nothing covers: a command's side effects are on disk
164
+ before it exits. Say so rather than implying otherwise - the tool description tells
165
+ the model to prefer `edit_file`, and the approval card tells the learner.
166
+
167
+ ## Sessions are trees
168
+
169
+ Borrowed from pi, and worth keeping. Every node has an `id` and a `parentId`; the
170
+ conversation is the path from the root to `head`. Branching moves the head and
171
+ keeps both branches in one append-only JSONL file. Three things fall out of it:
172
+ retrying an answer without losing the first, compaction that is lossy on the wire
173
+ and lossless on disk, and a crash costing at most the last line.
174
+
175
+ Do not "simplify" this into a list. The list version cannot do any of the three.
176
+
177
+ ## Terminal rules
178
+
179
+ - **Never assume a TTY.** Piped, redirected, or in CI, colour is off and the live
180
+ region is a no-op. Escape codes in a log file are worse than plain text.
181
+ - **The scrollback is not ours.** Finished output is printed and belongs to the
182
+ terminal; only the status line is a live region. That is what keeps search,
183
+ selection, and copy-paste working.
184
+ - **stdout is the answer. Everything else is stderr.** A pipeline must receive
185
+ only what labd meant as the answer.
186
+ - **Escape sequences are written as `\u001b` escapes**, never as literal bytes, so
187
+ source files stay greppable ASCII.
188
+
189
+ ## Distribution
190
+
191
+ Three artefacts, one source:
192
+
193
+ - **`dist/labd.js`** - ESM, zero runtime dependencies, what npm's `bin` points at.
194
+ - **`dist/labd.cjs`** - the same code as CommonJS. It exists because Node's
195
+ single-executable support runs its entry as CJS and refuses an ESM one outright.
196
+ This is why `bin/labd.ts` must never use top-level await.
197
+ - **`dist/bin/labd-<os>-<arch>`** - a standalone executable, built by injecting the
198
+ CJS bundle into a copy of the `node` binary. Not cross-compilable, so the release
199
+ workflow builds one per runner. ~115 MB, ~40 MB compressed: the honest price of
200
+ "works on a machine with nothing installed".
201
+
202
+ macOS needs the signing dance in `build-binaries.mjs`, and it is not optional -
203
+ injecting invalidates the signature, and macOS kills an invalidly-signed binary
204
+ rather than running it. The build also RUNS the binary before calling it done,
205
+ because every one of those steps fails by producing something that does not start
206
+ rather than by erroring.
207
+
208
+ `npm install -g git+ssh://...` cannot work: `dist/` is not committed and npm does
209
+ not fetch submodules for a git dependency. `scripts/prepare.mjs` says so rather
210
+ than letting the install appear to succeed with no binary - which is what it did
211
+ before, reporting "added 1 package" and installing nothing.
212
+
213
+ ## Sub-agents - deliberately outside
214
+
215
+ There is no `spawn_agent` tool, and adding one would be a permission mistake. Such
216
+ a tool has to decide what mode the children run in: inherit the parent's and one
217
+ approval becomes N unattended writes; give children their own and the model has
218
+ been handed the setting it is specifically not allowed to influence.
219
+
220
+ `--mode rpc` already makes every labd process a driveable agent, so orchestration
221
+ lives in a script the operator writes - where the children's mode is set by a
222
+ person, each child asks that script for approval over the wire, and there is no RPC
223
+ method for changing a mode. `examples/orchestrate.mjs` is the reference, and
224
+ `test/rpc-e2e.test.ts` runs two children at once to prove it.
225
+
226
+ ## Branches - promotion is by pull request, always
227
+
228
+ `develop` → `staging` → `production`, one step at a time, and **every step is a
229
+ pull request** - the same rule as the other Dev Labs repos, for the same reason:
230
+ the PR is the record of what shipped and when, and it is the only place a bad
231
+ promotion gets caught before it is live.
232
+
233
+ Never push directly to `staging` or `production`, and never move them with
234
+ `git branch -f` or a force push. `.githooks/pre-push` refuses the direct push;
235
+ turn it on once per clone:
236
+
237
+ ```bash
238
+ git config core.hooksPath .githooks
239
+ ```
240
+
241
+ ## Testing
242
+
243
+ `npm test` runs vitest. `npm run check` runs the typecheck too.
244
+
245
+ What is worth testing here is not "does it answer" - that needs a model - but the
246
+ properties that would fail silently:
247
+
248
+ - **containment** (`paths.test.ts`): traversal, absolute paths, symlinks out of the
249
+ tree, decorated paths. Every path labd touches is model output.
250
+ - **the permission properties** (`permissions.test.ts`): a project cannot set the
251
+ mode or the server URL, an untrusted project cannot load code, an extension
252
+ cannot shadow a built-in command, `agent` does not widen the denylist.
253
+ - **the session tree** (`session.test.ts`): branching, non-destructive compaction,
254
+ a truncated last line.
255
+ - **checkpoints** (`tools.test.ts`): a conflicting edit is refused, a created file
256
+ is deleted rather than blanked, a capture failure never blocks a write.
257
+ - **framing** (`rpc.test.ts`): splitting on `\n` only. There is a test that
258
+ demonstrates why `node:readline` is wrong here; if someone switches to it, that
259
+ test explains the bug.
260
+ - **the whole binary** (`e2e.test.ts`): a fake agent server, a real subprocess.
261
+ This is where "the mode never reaches the wire" and "unattended declines" are
262
+ actually proven.
263
+
264
+ `e2e.test.ts` needs `npm run build` first.
package/README.md ADDED
@@ -0,0 +1,276 @@
1
+ # labd-cli
2
+
3
+ labd in your terminal — the Dev Labs assistant, in the project you're standing in.
4
+
5
+ ```
6
+ $ labd
7
+ [o_o] labd in labd-cli on develop
8
+ labd changes files on its own - every answer can be put back in one go - and still asks before running a command.
9
+
10
+ > why is session.test.ts failing?
11
+
12
+ ● Read test/session.test.ts
13
+ ? Run a command
14
+ to see which of the eleven cases fails
15
+
16
+ npx vitest run test/session.test.ts
17
+
18
+ On auto, commands still ask - a command can do anything your shell can, and put back isn't one of them.
19
+ run this command? [y/N]
20
+ ```
21
+
22
+ This is the same assistant as the [VS Code extension](https://github.com/thedevlabs-io/labd)
23
+ and the one docked in the learning portal — same server, same prompt, same memory,
24
+ same permission model. Ask something in your editor, follow up in your terminal.
25
+
26
+ ## Install
27
+
28
+ **macOS / Linux** — a standalone executable. Nothing else needs to be installed:
29
+
30
+ ```bash
31
+ curl -fsSL https://raw.githubusercontent.com/thedevlabs-io/labd-cli/production/install.sh | sh
32
+ ```
33
+
34
+ **Windows** — same, in PowerShell:
35
+
36
+ ```powershell
37
+ irm https://raw.githubusercontent.com/thedevlabs-io/labd-cli/production/install.ps1 | iex
38
+ ```
39
+
40
+ **With npm**, if you already have Node 22+:
41
+
42
+ ```bash
43
+ npm install -g @thedevlabs-io/labd
44
+ ```
45
+
46
+ Then connect:
47
+
48
+ ```bash
49
+ labd login # approve a short code in your browser
50
+ ```
51
+
52
+ <details>
53
+ <summary>Other ways, and what each is for</summary>
54
+
55
+ | how | when |
56
+ |---|---|
57
+ | `install.sh` / `install.ps1` | a machine with nothing on it. Downloads a ~40 MB archive containing a single executable with the Node runtime inside it. |
58
+ | `npm install -g @thedevlabs-io/labd` | you have Node. Smaller download, and `npm update -g @thedevlabs-io/labd` upgrades it. |
59
+ | `npm pack` → share the `.tgz` | an air-gapped machine, or pinning an exact build. `npm install -g thedevlabs-io-labd-0.3.1.tgz`. |
60
+ | copy `dist/labd.js` | you built from source. It has **zero runtime dependencies**, so that one file plus Node 22+ is a working install anywhere. |
61
+ | clone + `npm link` | working on labd itself. |
62
+
63
+ `npm install -g git+ssh://…` does **not** work, and says so rather than half-installing: `dist/` is not committed, and npm does not fetch the git submodule that the build needs.
64
+
65
+ While this repository is private, the install scripts need a credential to read release assets. They use the GitHub CLI when it is signed in, or `GH_TOKEN` if you set one, and explain that rather than failing with a 404.
66
+
67
+ **Building from source:**
68
+
69
+ ```bash
70
+ git clone git@github.com:thedevlabs-io/labd-cli.git
71
+ cd labd-cli
72
+ git submodule update --init # the protocol lives in the labd repo
73
+ npm install
74
+ npm run build # dist/labd.js (and dist/labd.cjs, for binaries)
75
+ npm link # puts `labd` on your PATH
76
+ npm run build:binary # optional: a standalone executable for this platform
77
+ ```
78
+
79
+ </details>
80
+
81
+ ## Using it
82
+
83
+ ```bash
84
+ labd # a session in the current project
85
+ labd "why is this test failing" # one question, then exit
86
+ labd -p "..." | pbcopy # print mode, for pipes and scripts
87
+ cat build.log | labd -p "why?" # piped input joins the question
88
+ labd --mode rpc # JSON-RPC over stdio, to embed it
89
+ labd -c # pick up the most recent session here
90
+ ```
91
+
92
+ | flag | |
93
+ |---|---|
94
+ | `-p`, `--print` | answer and exit instead of starting a session |
95
+ | `--json` | newline-delimited JSON events instead of prose (implies `-p`) |
96
+ | `--mode <ask\|auto\|agent>` | the permission mode for this run; `--mode rpc` is the transport |
97
+ | `--think <low\|medium\|high>` | reasoning effort to ask for (the server decides what it honours) |
98
+ | `--resume <id>` | continue an earlier session; an id that matches nothing is an error, not a new session |
99
+ | `-c`, `--continue` | continue the most recent session in this project |
100
+ | `-C`, `--cwd <dir>` | run as though started in `<dir>` |
101
+ | `-e`, `--extension <path>` | load an extension file (repeatable; `/reload` keeps it) |
102
+ | `--no-extensions` | load none, now and on `/reload` |
103
+ | `--no-color` / `--color` | force colour off or on |
104
+ | `-v`, `--version` | |
105
+
106
+ Subcommands: `login`, `logout` (revokes the token server-side too), `sessions`
107
+ (earlier sessions here), `trust` (let this project load its own extensions and
108
+ name its own server).
109
+
110
+ Inside a session: `@path` tags a file into your question, Tab completes both those
111
+ and `/commands`, ctrl-c stops an answer without leaving.
112
+
113
+ | command | |
114
+ |---|---|
115
+ | `/mode [ask\|auto\|agent]` | how much labd does without asking |
116
+ | `/think [low\|medium\|high]` | how hard it thinks (a request — the server decides) |
117
+ | `/undo` | put back the files the last answer changed |
118
+ | `/redo` | reapply what `/undo` put back |
119
+ | `/tree` | every point in this session; branch from any of them |
120
+ | `/resume <id>` | continue from a point in `/tree` |
121
+ | `/sessions`, `/session` | earlier sessions here; where this one is stored |
122
+ | `/usage` | what's left of this month's allowance |
123
+ | `/trust` | let this project load its own extensions |
124
+ | `/reload` | reload extensions and the file list |
125
+ | `/quit` | leave - the session is saved, and `labd --resume <id>` continues it |
126
+
127
+ When you leave, the conversation is handed to the server to be distilled into
128
+ memory, exactly as the editor does when you start a new chat - which is what
129
+ makes "same memory" true in the terminal. Tool results are stripped before it
130
+ leaves, so no file contents travel for that.
131
+
132
+ ## The three modes
133
+
134
+ | mode | changes files | runs a command |
135
+ |---|---|---|
136
+ | `ask` | asks first | asks first |
137
+ | `auto` (default) | on its own | asks first |
138
+ | `agent` | on its own | on its own |
139
+
140
+ Changing a file means any of the four: editing one, creating one, moving one, or
141
+ deleting one. `auto` splits where it does because the two columns are not equally
142
+ reversible: every write is snapshotted and `/undo` puts the whole turn back, while
143
+ a command has already happened.
144
+
145
+ `labd -p` and `--mode rpc` are the exception. With nobody watching they ask -
146
+ which unattended means refusing - unless you have actually chosen a mode.
147
+
148
+ **Some things are refused in every mode, `agent` included** — anything needing
149
+ `sudo`, deploying, publishing, `git push`, wiping a disk, backgrounding a process,
150
+ anything that never exits, and anything that reads a credentials file. The mode
151
+ decides who clicks; it never decides what labd is willing to do.
152
+
153
+ Two more rules worth knowing before you point this at a repository you didn't write:
154
+
155
+ - **A project can't set the mode.** `.labd/settings.json` may set a timeout; `mode`
156
+ comes from your global settings or the command line only. Otherwise cloning a
157
+ repo and typing `labd` would be running whatever it decided to allow itself.
158
+ - **A project can't load its own code until you say so.** `.labd/extensions` needs
159
+ `labd trust` in that directory first.
160
+ - **A project can't point your token at another server until you say so.** A
161
+ `serverUrl` in `.labd/settings.json` is ignored - and the banner says so - until
162
+ the project is trusted.
163
+
164
+ ## Files it keeps
165
+
166
+ | path | |
167
+ |---|---|
168
+ | `~/.labd/auth.json` | your token, mode `0600`, never in the project |
169
+ | `~/.labd/settings.json` | global settings, including `mode` |
170
+ | `~/.labd/sessions/` | JSONL session trees, bucketed by project |
171
+ | `~/.labd/checkpoints/` | snapshots, so `/undo` works |
172
+ | `.labd/settings.json` | per-project settings (not `mode`) |
173
+ | `.labd/extensions/` | per-project extensions, loaded once trusted |
174
+
175
+ `settings.json` keys, global or per project: `timeout` as `commandTimeoutSeconds`
176
+ (5–600), `effort` (`low`/`medium`/`high`), `checkpoints` (`true` by default), and
177
+ `serverUrl` — honoured from a project file only once that project is trusted.
178
+ `mode` is read from the global file only.
179
+
180
+ `LABD_HOME`, `LABD_SERVER_URL`, and `LABD_TOKEN` override the corresponding thing;
181
+ the last is how you run labd in CI without pairing. `NO_COLOR` turns colour off,
182
+ `FORCE_COLOR` turns it on (`FORCE_COLOR=0` off) and wins over everything else, and
183
+ `LABD_NO_MOTION` skips the opening animation.
184
+
185
+ ## Sub-agents and orchestration
186
+
187
+ labd has no `spawn_agent` tool, and that is a decision rather than a gap. It does
188
+ have `--mode rpc`, which makes every labd process a driveable agent on
189
+ stdin/stdout — so an orchestrator is a script that spawns several of them:
190
+
191
+ ```bash
192
+ node examples/orchestrate.mjs "what does this file do?" src/a.ts src/b.ts src/c.ts
193
+ ```
194
+
195
+ [`examples/orchestrate.mjs`](examples/orchestrate.mjs) is ~140 lines: spawn, send
196
+ `labd/prompt`, read the notification stream, run three at a time.
197
+
198
+ **Why not a tool inside labd.** A `spawn_agent` tool has to decide what mode the
199
+ children run in, and every answer is bad. Inherit the parent's mode and one
200
+ approval becomes N unattended writes. Give children their own mode and the model
201
+ has been handed the one setting it is specifically not allowed to influence.
202
+ Outside, the mode of each child is set by whoever wrote the script, each child
203
+ asks *that script* for approval over the wire, and the orchestrator is code you
204
+ can read. (Pi reaches the same conclusion and suggests tmux.)
205
+
206
+ The protocol is small:
207
+
208
+ | you send | |
209
+ |---|---|
210
+ | `labd/prompt` `{question}` | run a turn; resolves with `{text, wrote, ran}`. A second prompt sent before the first resolves is queued behind it |
211
+ | `labd/session` | id, file, node count, head |
212
+ | `labd/capabilities` | mode, root, methods |
213
+ | labd sends | |
214
+ | `labd/ready` | first, on start: protocol, framing, session id, mode, root |
215
+ | `labd/text`, `labd/status`, `labd/done`, `labd/error` | notifications as the turn runs |
216
+ | `labd/tool` `{name, status, input, detail, preview, output}` | each tool step; `preview` is the diff or the command, `output` what came back |
217
+ | `labd/command_output` `{line}` | a running command's own output, as it happens |
218
+ | `labd/approve` `{call, preview}` | **a request to you** — answer `{approved: true\|false}` |
219
+
220
+ Closing stdin ends the session: any turn in flight finishes, the conversation is
221
+ handed over for memory, and the process exits 0.
222
+
223
+ Two things to get right, both of which have tests in this repo:
224
+
225
+ - **Split records on `\n` and nothing else.** Not `readline`: it also splits on
226
+ U+2028 and U+2029, which occur inside real JSON strings, and it will cut a valid
227
+ record in half.
228
+ - **Answer `labd/approve`.** Ignore it and the child refuses after two minutes,
229
+ which is the safe direction but a slow one. There is deliberately no RPC method
230
+ for *changing* the mode: a host that could raise it would be a host that turns
231
+ approvals off for the person who launched labd.
232
+
233
+ ## Extensions
234
+
235
+ A module that gets an API object, and can add slash commands and watch what labd
236
+ does:
237
+
238
+ ```js
239
+ // ~/.labd/extensions/branches.mjs
240
+ export default function (labd) {
241
+ labd.registerCommand('branches', {
242
+ describe: 'list local branches',
243
+ run: async () => execSync('git branch', { cwd: labd.root }).toString(),
244
+ });
245
+
246
+ labd.on('tool', (step) => { /* observe; handlers cannot veto */ });
247
+ labd.on('turn', ({ question }) => {});
248
+ }
249
+ ```
250
+
251
+ A command can't shadow a built-in (`/mode` has to mean `/mode`). Extensions
252
+ cannot add **tools** the model can call: the tool list is the protocol's, shared
253
+ with the editor, so a new tool is a change to the labd repo rather than to a file
254
+ in `~/.labd/extensions`.
255
+
256
+ ## Development
257
+
258
+ ```bash
259
+ npm run build # bundle to dist/labd.js
260
+ npm test # vitest (build first — the e2e suite spawns the binary)
261
+ npm run check # typecheck + tests
262
+ ```
263
+
264
+ Conventions, and the reasoning behind the permission model, are in
265
+ [AGENTS.md](AGENTS.md). Promotion is `develop` → `staging` → `production`, one
266
+ pull request per step; turn the guard on with
267
+ `git config core.hooksPath .githooks`.
268
+
269
+ ## Prior art
270
+
271
+ The shape of this owes a lot to [pi](https://pi.dev) (`earendil-works/pi`) — the
272
+ layered packages, sessions as trees, the print/RPC/SDK modes, and the framing
273
+ detail that stdio JSONL must be split on `\n` and nothing else. Where labd-cli
274
+ differs is the thing pi deliberately leaves out: pi has no permission system and
275
+ tells you to containerise it, which is right for a tool you built for yourself and
276
+ wrong for one a learner points at a repository they just cloned.