@bivy/bivy 0.19.4-staging.5 → 0.19.4-staging.7

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/README.md CHANGED
@@ -4,22 +4,27 @@
4
4
  [![license: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-2b6cb0)](LICENSE)
5
5
  [![node](https://img.shields.io/badge/node-%E2%89%A520-2b6cb0)](https://nodejs.org)
6
6
 
7
- **Build, try, and review apps from your phone. Your coding agent and dev
8
- environment stay on your own machines.**
9
-
10
- Bivy is an open-source remote development environment built around coding
11
- agents. Ask for a change, try the running app, and point to or draw on what
12
- needs fixing. Send that feedback to the agent and try again—all in the same
13
- session, without going back to your computer.
14
-
15
- Live previews bring your app to your phone or browser without deploying it to
16
- Render, building a Docker image, or setting up a separate preview server.
17
- Share a preview link so someone else can try it and leave notes, too.
18
-
19
- Keep Claude Code, Codex, Pi, OpenCode, or another supported agent. Keep your
20
- repos, tools, databases, and development environment. Bivy connects them to
21
- chat, a terminal, app previews, and code review wherever you are. Start work
22
- yourself, or let an issue, CI failure, message, schedule, or webhook start it.
7
+ **Local coding agents that show their work. Anywhere.**
8
+
9
+ Bivy is the open-source workspace for coding agents. It runs Claude Code, Codex,
10
+ Pi, OpenCode and other agents on your own machine. Reach them from a browser or
11
+ your phone, with the live session, its terminal, its approvals, and the app
12
+ they're building right in the chat. Try it, mark what's wrong, and send it back.
13
+
14
+ Bivy is not another coding agent and not a cloud development machine. The agent
15
+ stays local and does the coding with your model provider. Your repos, tools,
16
+ databases, and services stay where they are. Bivy gives you remote access to all
17
+ of it, plus live previews, automations, and review.
18
+
19
+ - **Live previews.** The running app beside the chat. Mark it, send it back,
20
+ share it with someone who has no Bivy account.
21
+ - **Automations.** GitHub issues, failed CI, Linear, Slack, schedules, and
22
+ webhooks start the work. It comes back as a preview link or a pull request.
23
+ - **Any agent.** Hit a usage limit? Fork the session to another agent, or let
24
+ Bivy retry when the limit resets.
25
+ - **Your machine, anywhere.** Start at your desk, pick the session up on your
26
+ phone. The machine dials out, so there are no ports to open, and session
27
+ traffic is end-to-end encrypted.
23
28
 
24
29
  **[Start free on Bivy Cloud](https://app.bivy.sh)** ·
25
30
  **[Quickstart](docs/quickstart.md)** ·
@@ -47,30 +52,34 @@ agents. Connect a Mac, Linux computer, or existing server and bring your own
47
52
  agent subscription, model API key, or local model. You can also self-host the
48
53
  entire remote-access stack.
49
54
 
50
- > **Bivy is 0.x software.** Claude Code, Codex, Pi, and OpenCode are the
55
+ > **Bivy is 0.x software.** Claude Code, Codex, Pi, OpenCode, and Grok are the
51
56
  > release-tested paths. Credential sync, resume, handoffs, approvals, and
52
57
  > sandboxing depend on the runtime. See the
53
58
  > [runtime support matrix](docs/runtime-support-matrix.md).
54
59
 
55
- ## Build → preview → mark up → iterate
60
+ ## Don't just read the diff. Try the app.
56
61
 
57
- Remote chat lets you ask an agent for a change. Bivy also lets you **use what
58
- it built and show it what to fix**, without a separate deployment step.
62
+ Bivy lets you **use what the agent built and show it what to fix**, without a
63
+ separate deployment step.
59
64
 
60
65
  1. **Ask for a change.** The agent works in your repo with your existing tools
61
66
  and local services.
62
- 2. **Try the app.** Open a live preview from the session. Use the running app,
63
- not just a screenshot of what the agent says it finished. Bivy can discover
64
- dev servers running in the session workspace; tap **Preview** to open one.
65
- 3. **Mark up what needs work.** Point at an element or circle and draw on the
66
- preview. Add a note, then send the draft to the agent with the element and
67
- page context. Marked-up screenshots and before/after comparison are available
68
- when agent screenshots are enabled.
69
- 4. **Iterate and review.** Try the next version, inspect the diff, review checks,
70
- and decide when the work is ready—not just when the agent stops.
71
- 5. **Bring someone else into the review.** Copy a public preview link so they
72
- can use the app without a Bivy account and leave notes on specific elements.
73
- Bring their notes into the session when you're ready to act on them.
67
+ 2. **Try the app.** Bivy finds the dev server the agent starts and opens it
68
+ beside the chat: web apps, terminals, and desktop apps. Use the running app,
69
+ not a screenshot of what the agent says it finished.
70
+ 3. **Mark what needs work.** Press and hold anything in the preview to mark it,
71
+ or drag to circle it, then say what you want. **Mark another** keeps
72
+ separate notes separate. The marks go to the agent with the element and page
73
+ context. No screenshots to paste.
74
+ 4. **Follow each note.** Every mark stays in the chat as a pin that resolves
75
+ itself: **Changed** when a later run changes what you marked, **Element
76
+ gone** when it leaves the page, **Done** when you say so.
77
+ 5. **Review the next version.** A new version waits on the preview pill instead
78
+ of reloading under you. Take it when you're ready, compare before and after,
79
+ inspect the diff and checks, and decide when the work is done.
80
+ 6. **Get a second opinion.** Share a preview link for 1 hour, 1 day, or 7 days.
81
+ Teammates and clients try the running app and leave notes without a Bivy
82
+ account. Make it view-only, or **Stop sharing** to end every link at once.
74
83
 
75
84
  On Bivy Cloud, preview delivery is built in: no per-app domains, certificates,
76
85
  public ports, or tunnels to configure. Self-hosters configure preview delivery
@@ -79,27 +88,13 @@ build or dev-server setup, and **the machine serving it must stay awake and
79
88
  online**. These are development previews, not production hosting.
80
89
 
81
90
  **Share deliberately:** anyone with a preview link can use that app, including
82
- its live backend. Links expire after 24 hours and can be revoked sooner.
91
+ its live backend, until the link expires or you stop sharing.
83
92
  Reviewer notes aren't sent to the agent automatically; you send them or explicitly
84
93
  allow agent access. Preview traffic uses HTTPS through the preview relay, not
85
94
  session end-to-end encryption; the relay operator can see it.
86
95
 
87
96
  [App previews, visual feedback, and sharing →](docs/apps.md)
88
97
 
89
- ## Your agents and machines, one workspace
90
-
91
- | Capability | What it means for you |
92
- |---|---|
93
- | **One workspace, multiple agents** | Use different agents and models for different tasks without maintaining a separate workflow for each. |
94
- | **Your machines and environment** | Work beside your existing repos, dev servers, databases, private networks, toolchains, and GPUs. |
95
- | **Automations and triggers** | Let issues, failed CI, messages, schedules, and webhooks start work instead of copying requests into a chat. |
96
- | **Encrypted key and OAuth sync** | Reuse Bivy-managed provider credentials across enrolled machines and compatible runtimes, with less repeated setup. |
97
- | **Live sessions from anywhere** | Build and review from your phone, browser, or terminal; answer questions and approve supported actions in the same session. |
98
- | **Live app previews** | Try the running app without deploying it to a separate hosting service. |
99
- | **Visual feedback and sharing** | Point, draw, and send specific feedback to the agent; share a preview link for outside review. |
100
- | **Reviewable results** | Inspect the app, changes, declared checks, artifacts, and pull requests—not just an agent's claim that it finished. |
101
- | **Hosted convenience or self-hosting** | Use Bivy Cloud for managed remote access, or run the same open-source core yourself. |
102
-
103
98
  ## One workflow, from trigger to review
104
99
 
105
100
  ```text
@@ -131,34 +126,70 @@ creates runs when an event matches.
131
126
  Manual and automated work use the same kind of live session. You can join a run
132
127
  when it needs help rather than wait for a black-box job to finish.
133
128
 
134
- ### Work in the environment you already have
129
+ ### Let events start the work
135
130
 
136
- A clean cloud sandbox isn't always enough. Your agent may need the database
137
- running on localhost, an uncommitted change, an internal API behind your VPN,
138
- or a model running on your GPU. Bivy runs the agent where those things already
139
- exist, subject to that machine's permissions and the runtime's protection.
131
+ Automations turn recurring or incoming work into sessions you can join,
132
+ supervise, and review. An agent picks the task up on your machine and posts
133
+ back a preview link or a pull request, so you try the result where the task
134
+ lives and reply in the same session. Choose the repository, machine, agent,
135
+ model, approval mode, sandbox setting, and maximum attempts.
140
136
 
141
- Connect several machines to the same account: a laptop for interactive work,
142
- a Linux server for background jobs, or a GPU box for local inference. Choose
143
- the machine for each session or pin it in an automation. Repository runs can
144
- use isolated Git worktrees without rebuilding the whole development environment.
137
+ | Trigger | Example workflow |
138
+ |---|---|
139
+ | **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |
140
+ | **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |
141
+ | **Linear** | Label an issue to start work without copying its description into an agent. |
142
+ | **Slack** | Send a request from the conversation where the work came up. |
143
+ | **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |
144
+ | **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |
145
145
 
146
- **The execution machine must stay awake and online.** To close your laptop and
147
- leave work running, run the agent on a different, always-on machine.
146
+ Configure automations in the app or version them with your repository in
147
+ `.bivy/automations.yaml`:
148
148
 
149
- [Environment and multi-machine recipes →](docs/capability-recipes.md)
149
+ ```bash
150
+ bivy automation init
151
+ # Edit the generated definition for your repository and workflow.
152
+ bivy automation validate
153
+ bivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture
154
+ bivy automation apply
155
+ ```
150
156
 
151
- ### Use multiple agents, not multiple disconnected workflows
157
+ Or delegate a one-off job without creating an automation:
152
158
 
153
- Run Claude Code for one task, Codex for another, and Pi or OpenCode where they
154
- fit. Bivy supplies the shared session, remote-access, automation, and review
155
- surfaces; your chosen agent still does the coding and uses your model provider.
159
+ ```bash
160
+ bivy runs start "Review outdated dependencies and propose a small, tested update."
161
+ bivy runs wait <id>
162
+ ```
163
+
164
+ Runs keep routing and lifecycle evidence, check results, and output references
165
+ in a reviewable Receipt. For unattended issue work, Bivy runs declared repository
166
+ checks after the agent's turn; failed required checks fail the run even if the
167
+ agent reports success. A completed process alone is not proof that the task
168
+ succeeded.
156
169
 
170
+ [Automation recipes →](docs/capability-recipes.md#let-events-start-runs) ·
171
+ [Automations as code →](docs/automations-as-code.md) ·
172
+ [Run outcomes and reliability limits →](docs/automation-runs.md)
173
+
174
+ ### Use every agent you pay for. Switch mid-task.
175
+
176
+ Run Claude Code for one task, Codex for another, and Pi or OpenCode where they
177
+ fit, side by side in one session list. Bivy supplies the shared session,
178
+ remote-access, automation, and review surfaces; your chosen agent still does
179
+ the coding and uses your model provider.
180
+
181
+ - **Hit a usage limit? Keep going.** When a turn fails on a limit, fork the
182
+ session, with its code and conversation, to another agent and model, or let
183
+ Bivy retry when the limit resets (the provider has to report a reset time).
184
+ - **Get a second opinion.** `bivy delegate` hands a task to another agent, on
185
+ the same machine or a different one, and brings back its answer, branch, and
186
+ PR. `--to codex,claude@linux` sends one task to several agents to compare.
157
187
  - Choose an agent and, where supported, a model for each session or run.
158
188
  - Import existing Claude Code and Codex sessions.
159
189
  - Fork or move work to another agent or machine when a different setup fits
160
190
  better. Continuation fidelity varies: some paths preserve native history,
161
- while others replay portable turns or seed the destination with context.
191
+ while others replay portable turns or hand over a summary with the earlier
192
+ transcript as a local file.
162
193
  - Use agent-native logins, Bivy-managed credentials, or local inference.
163
194
  Bivy's custom OpenAI-compatible endpoint registry currently feeds Pi;
164
195
  other agents may need their own provider configuration.
@@ -199,48 +230,22 @@ hosted-provisioning custody grants are separate from ordinary encrypted sync.
199
230
  [Credentials guide →](docs/credentials-guide.md) ·
200
231
  [Key storage →](docs/key-management.md)
201
232
 
202
- ### Let events start the work
203
-
204
- Automations turn recurring or incoming work into sessions you can join,
205
- supervise, and review. Choose the repository, machine, agent, model, approval
206
- mode, sandbox setting, and maximum attempts.
207
-
208
- | Trigger | Example workflow |
209
- |---|---|
210
- | **GitHub issues and mentions** | Label an issue `bivy` or `bivy/<machine>`, or mention your Bivy GitHub App, to work toward a pull request. |
211
- | **Failed CI** | Match a failed workflow, ask the agent to reproduce it, make a fix, and run the affected checks. |
212
- | **Linear** | Label an issue to start work without copying its description into an agent. |
213
- | **Slack** | Send a request from the conversation where the work came up. |
214
- | **Schedules** | Run a weekly dependency review, recurring maintenance, or a one-time task. |
215
- | **Signed webhooks** | Connect alerts, internal tools, or your own event sources. |
216
-
217
- Configure automations in the app or version them with your repository in
218
- `.bivy/automations.yaml`:
219
-
220
- ```bash
221
- bivy automation init
222
- # Edit the generated definition for your repository and workflow.
223
- bivy automation validate
224
- bivy automation test --event .bivy/events/failed-ci.yaml # supply a local event fixture
225
- bivy automation apply
226
- ```
233
+ ### Work in the environment you already have
227
234
 
228
- Or delegate a one-off job without creating an automation:
235
+ A clean cloud sandbox isn't always enough. Your agent may need the database
236
+ running on localhost, an uncommitted change, an internal API behind your VPN,
237
+ or a model running on your GPU. Bivy runs the agent where those things already
238
+ exist, subject to that machine's permissions and the runtime's protection.
229
239
 
230
- ```bash
231
- bivy runs start "Review outdated dependencies and propose a small, tested update."
232
- bivy runs wait <id>
233
- ```
240
+ Connect several machines to the same account: a laptop for interactive work,
241
+ a Linux server for background jobs, or a GPU box for local inference. Choose
242
+ the machine for each session or pin it in an automation. Repository runs can
243
+ use isolated Git worktrees without rebuilding the whole development environment.
234
244
 
235
- Runs keep routing and lifecycle evidence, check results, and output references
236
- in a reviewable Receipt. For unattended issue work, Bivy runs declared repository
237
- checks after the agent's turn; failed required checks fail the run even if the
238
- agent reports success. A completed process alone is not proof that the task
239
- succeeded.
245
+ **The execution machine must stay awake and online.** To close your laptop and
246
+ leave work running, run the agent on a different, always-on machine.
240
247
 
241
- [Automation recipes →](docs/capability-recipes.md#let-events-start-runs) ·
242
- [Automations as code →](docs/automations-as-code.md) ·
243
- [Run outcomes and reliability limits →](docs/automation-runs.md)
248
+ [Environment and multi-machine recipes →](docs/capability-recipes.md)
244
249
 
245
250
  ### Start at your desk. Continue anywhere.
246
251
 
@@ -269,6 +274,29 @@ in your browser; adding it to your home screen is optional.
269
274
  [Remote access →](docs/remote-access.md) ·
270
275
  [Voice, files, and terminal recipes →](docs/capability-recipes.md)
271
276
 
277
+ ### Agents can reach you, too
278
+
279
+ Every session gets the `bivy` CLI, so any agent with a shell can talk back
280
+ through the app, not only agents with their own built-in tools:
281
+
282
+ ```bash
283
+ bivy notify "Tests are green, PR is up" # chat card + phone push when you're away
284
+ bivy ask "Ship to staging?" --option Yes --option No # waits for your answer
285
+ bivy attach report.png --caption "Before/after" # show a file in the chat
286
+ bivy suggest "Add a dark theme to settings" # a next task you start in one tap
287
+ bivy context --json # session, workspace, machine, apps
288
+ ```
289
+
290
+ `bivy help --json` lists every command, and with `BIVY_OUTPUT=json` failures
291
+ come back as structured errors with distinct exit codes, so an agent can tell
292
+ what went wrong and what to run next. `bivy guide` prints short playbooks for
293
+ agents, and the same commands are served as MCP tools for agents that prefer
294
+ them. Add your own standing instructions for
295
+ every session in **Settings → Agent instructions**.
296
+
297
+ [Commands for agents inside a session →](docs/cli-reference.md#inside-an-agent-session) ·
298
+ [Agent instructions →](docs/agent-instructions.md)
299
+
272
300
  ## Get started
273
301
 
274
302
  ### Install
@@ -404,9 +432,9 @@ for the task, especially before enabling unattended work.
404
432
 
405
433
  ## Agents and everyday commands
406
434
 
407
- **Claude Code, Codex, Pi, and OpenCode are release-tested.** Additional adapters
408
- include Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub Copilot,
409
- Grok, Amp, Auggie, Droid, Continue, Kilo Code, and Rovo Dev. Installation,
435
+ **Claude Code, Codex, Pi, OpenCode, and Grok are release-tested.** Additional
436
+ adapters include Gemini CLI, Qwen Code, Goose, Aider, Cline, Crush, Cursor, GitHub
437
+ Copilot, Amp, Auggie, Droid, Continue, Kilo Code, and Rovo Dev. Installation,
410
438
  resume, model selection, and tool protection vary—see the
411
439
  [support matrix](docs/runtime-support-matrix.md) and [agent guides](docs/agents/README.md).
412
440
 
package/bin/bivy.mjs CHANGED
@@ -511,7 +511,19 @@ function nativeAgentAuthDetected(choice) {
511
511
  }
512
512
 
513
513
  function url(config) {
514
- return `http://localhost:${config.port}`;
514
+ // BIVY_NODE_URL: set by `bivy mcp-serve` for the CLI it runs, so tools reach
515
+ // the node that started the agent whatever data dir this process resolves.
516
+ const override = process.env.BIVY_NODE_URL?.trim().replace(/\/+$/, "");
517
+ return override || `http://localhost:${config.port}`;
518
+ }
519
+
520
+ // The credential for a request about `sessionId`: the agent's own session token
521
+ // ($BIVY_SESSION_TOKEN) when it is that session, else a device token when this
522
+ // user can mint one, else none (loopback on a single-user host needs none).
523
+ async function sessionAuth(config, sessionId) {
524
+ const own = resolveAttachSessionId({ env: process.env });
525
+ if (process.env.BIVY_SESSION_TOKEN && sessionId && sessionId === own) return process.env.BIVY_SESSION_TOKEN;
526
+ try { return await localDeviceToken(config); } catch { return undefined; }
515
527
  }
516
528
 
517
529
  // The host the node will bind (mirrors src/server.ts). We probe on this same
@@ -2424,8 +2436,7 @@ async function cmdAttach(args = []) {
2424
2436
  // (loopback bypasses auth), but include one when available so multi-user
2425
2437
  // hosts work too.
2426
2438
  async function sessionPost(config, sessionId, action, body) {
2427
- let token;
2428
- try { token = await localDeviceToken(config); } catch { token = undefined; }
2439
+ const token = await sessionAuth(config, sessionId);
2429
2440
  return fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/${action}`, {
2430
2441
  method: "POST",
2431
2442
  headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
@@ -2437,7 +2448,7 @@ async function sessionPost(config, sessionId, action, body) {
2437
2448
  function sessionHttpError(what, status, body) {
2438
2449
  const message = `${what} failed (${status}): ${body?.error || "unknown error"}`;
2439
2450
  if (status === 404) return { code: "session_not_found", message, hint: "The session must be open on this node.", next: "bivy context", exit: EXIT.notFound };
2440
- if (status === 401 || status === 403) return { code: "denied", message, exit: EXIT.denied };
2451
+ if (status === 401 || status === 403) return { code: "denied", message, hint: "A session's token only reaches that session; run it from the session's own shell, or from a terminal signed in to this node.", exit: EXIT.denied };
2441
2452
  if (status === 400) return { code: "invalid", message, exit: EXIT.usage };
2442
2453
  return { code: "failed", message, exit: EXIT.failed };
2443
2454
  }
@@ -2472,8 +2483,7 @@ async function cmdContext(args = []) {
2472
2483
  let node = { session: null };
2473
2484
  if (sessionId) {
2474
2485
  const config = loadConfig();
2475
- let token;
2476
- try { token = await localDeviceToken(config); } catch { token = undefined; }
2486
+ const token = await sessionAuth(config, sessionId);
2477
2487
  try {
2478
2488
  const res = await fetch(`${url(config)}/api/session/${encodeURIComponent(sessionId)}/context`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
2479
2489
  const body = await res.json().catch(() => ({}));
@@ -2631,8 +2641,7 @@ answer later and 'bivy ask status <id>' checks without waiting.
2631
2641
  if (!Number.isFinite(timeout) || timeout < 1) return fail({ code: "usage", message: "--timeout takes seconds or a duration like 90s, 10m or 2h.", exit: EXIT.usage });
2632
2642
  const config = loadConfig();
2633
2643
  if (!(await ensureNodeRunning(config))) return fail({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable });
2634
- let token;
2635
- try { token = await localDeviceToken(config); } catch { token = undefined; }
2644
+ const token = await sessionAuth(config, sessionId);
2636
2645
  const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/ask`;
2637
2646
  const call = async (suffix, body) => {
2638
2647
  const res = await fetch(`${base}${suffix}`, {
@@ -2686,11 +2695,10 @@ answer later and 'bivy ask status <id>' checks without waiting.
2686
2695
 
2687
2696
  // One JSON request to the local node for the commands below: starts the node if
2688
2697
  // needed and returns { status, body }, or fails in the shared error shape.
2689
- async function nodeRequest(method, pathname, body, { json = false } = {}) {
2698
+ async function nodeRequest(method, pathname, body, { json = false, sessionId } = {}) {
2690
2699
  const config = loadConfig();
2691
2700
  if (!(await ensureNodeRunning(config))) return cliError({ code: "node_unreachable", message: `Could not reach the Bivy node at ${url(config)}.`, next: "bivy status", exit: EXIT.unavailable }, { json, paint: c.red });
2692
- let token;
2693
- try { token = await localDeviceToken(config); } catch { token = undefined; }
2701
+ const token = await sessionAuth(config, sessionId);
2694
2702
  const res = await fetch(`${url(config)}${pathname}`, {
2695
2703
  method,
2696
2704
  headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
@@ -2719,7 +2727,7 @@ async function cmdFork(args = []) {
2719
2727
  const modelArg = parsed.one("--model");
2720
2728
  const slash = modelArg?.indexOf("/") ?? -1;
2721
2729
  const model = modelArg ? (slash > 0 ? { provider: modelArg.slice(0, slash), id: modelArg.slice(slash + 1) } : { provider: "", id: modelArg }) : undefined;
2722
- const { status, body } = await nodeRequest("POST", "/api/session/fork", { sessionId, ...(model ? { model } : {}) }, { json });
2730
+ const { status, body } = await nodeRequest("POST", "/api/session/fork", { sessionId, ...(model ? { model } : {}) }, { json, sessionId });
2723
2731
  if (status === 404) return fail({ code: "node_outdated", message: "This node is older than 'bivy fork'.", next: "bivy update", exit: EXIT.unavailable });
2724
2732
  if (status >= 400 || body.error) return fail({ code: /not found/i.test(body.error ?? "") ? "session_not_found" : "failed", message: `Fork failed: ${body.error || `HTTP ${status}`}`, exit: /not found/i.test(body.error ?? "") ? EXIT.notFound : EXIT.failed });
2725
2733
  if (json) { console.log(JSON.stringify({ sessionId: body.sessionId, fidelity: body.fidelity, ...(body.missing?.length ? { missing: body.missing } : {}) })); return; }
@@ -2818,6 +2826,62 @@ async function cmdInstructions(args = []) {
2818
2826
  fail({ code: "usage", message: usage, exit: EXIT.usage });
2819
2827
  }
2820
2828
 
2829
+ // `bivy automation apply` inside an agent session: the node shows the user what
2830
+ // would change as an approval card and applies it only once they approve, with
2831
+ // this machine's account credential. `proposal <id> [--wait]` follows one.
2832
+ async function cmdAutomationProposal(args = []) {
2833
+ const usage = "Usage (in an agent session): bivy automation apply [path] [--prune] [--timeout 90] [--json]\n bivy automation proposal <id> [--wait] [--timeout 90] [--json]";
2834
+ if (args.includes("-h") || args.includes("--help")) {
2835
+ console.log(`${usage}\n\nInside an agent session, apply goes through this machine's approval mode: usually the user gets an approval card listing what would change and decides; with "never ask" it applies at once. The chat records what was applied. Waits up to --timeout seconds (default 90) for the answer; if it hasn't come, prints the proposal id to check later with 'bivy automation proposal <id> --wait'. The card stays open for 30 minutes. Validate and plan first ('bivy automation validate', 'plan').`);
2836
+ return;
2837
+ }
2838
+ const json = wantsJson(args);
2839
+ const fail = (error) => cliError(error, { json, paint: c.red });
2840
+ const parsed = parseSessionArgs(args, ["--timeout", "--session"]);
2841
+ if (parsed.error) return fail({ code: "usage", message: parsed.error, exit: EXIT.usage });
2842
+ const sessionId = resolveAttachSessionId({ sessionFlag: parsed.one("--session"), env: process.env });
2843
+ if (!sessionId) return fail({ code: "no_session", message: "No session id.", next: "bivy sessions --json", exit: EXIT.usage });
2844
+ const timeout = parsed.one("--timeout") === undefined ? 90 : durationSeconds(parsed.one("--timeout"));
2845
+ if (!Number.isFinite(timeout) || timeout < 0) return fail({ code: "usage", message: "--timeout takes seconds or a duration like 90s or 10m.", exit: EXIT.usage });
2846
+ const base = `/api/session/${encodeURIComponent(sessionId)}/automations/apply`;
2847
+ const [action, target] = parsed.positional;
2848
+ let proposal;
2849
+ if (action === "proposal") {
2850
+ if (!target) return fail({ code: "usage", message: usage, exit: EXIT.usage });
2851
+ const { status, body } = await nodeRequest("GET", `${base}/${encodeURIComponent(target)}`, undefined, { json, sessionId });
2852
+ if (status >= 400) return fail({ code: status === 404 ? "not_found" : "failed", message: body.error || `HTTP ${status}`, exit: status === 404 ? EXIT.notFound : EXIT.failed });
2853
+ proposal = body;
2854
+ if (!parsed.has("--wait")) { if (json) console.log(JSON.stringify(proposal)); else console.log(`Proposal ${proposal.id}: ${proposal.status}`); return; }
2855
+ } else {
2856
+ const file = path.resolve(process.cwd(), target ?? ".bivy/automations.yaml");
2857
+ const { status, body } = await nodeRequest("POST", base, { file, prune: parsed.has("--prune") }, { json, sessionId });
2858
+ if (status === 404 && !body.error?.includes("Session")) return fail({ code: "node_outdated", message: "This node is older than proposing automations from a session.", next: "bivy update", exit: EXIT.unavailable });
2859
+ if (status >= 400) return fail(sessionHttpError("Proposing the automations", status, body));
2860
+ proposal = body;
2861
+ if (!json) {
2862
+ console.error(`Changes to apply (${proposal.file}):`);
2863
+ for (const change of proposal.changes) console.error(` ${change.action === "create" ? "+" : change.action === "remove" ? "-" : "~"} ${change.id}`);
2864
+ }
2865
+ }
2866
+ const deadline = Date.now() + timeout * 1000;
2867
+ let polls = 0;
2868
+ while (proposal.status === "pending" && Date.now() < deadline) {
2869
+ await new Promise((resolve) => setTimeout(resolve, polls === 0 ? 500 : 2000));
2870
+ if (polls++ === 1 && !json) console.error("Waiting for the user to approve them…");
2871
+ const { status, body } = await nodeRequest("GET", `${base}/${encodeURIComponent(proposal.id)}`, undefined, { json, sessionId });
2872
+ if (status < 400) proposal = body;
2873
+ }
2874
+ if (json) console.log(JSON.stringify(proposal));
2875
+ if (proposal.status === "applied") { if (!json) console.log(proposal.output || "Applied."); return; }
2876
+ if (proposal.status === "pending") {
2877
+ if (!json) console.log(`The user hasn't answered yet. Check later with: bivy automation proposal ${proposal.id} --wait`);
2878
+ process.exit(EXIT.timeout);
2879
+ }
2880
+ const message = { rejected: "The user declined these automation changes.", expired: "Nobody answered within 30 minutes; nothing was applied.", failed: `Approved, but applying failed: ${proposal.error || "unknown error"}` }[proposal.status] ?? proposal.status;
2881
+ if (!json) console.error(message);
2882
+ process.exit(proposal.status === "expired" ? EXIT.timeout : EXIT.failed);
2883
+ }
2884
+
2821
2885
  // Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
2822
2886
  // self-contained task from this session to another agent, optionally on another
2823
2887
  // account machine, and get its answer back. Agent-agnostic — any agent with a
@@ -2854,8 +2918,7 @@ async function cmdDelegate(args = []) {
2854
2918
  if (!sessionId) { console.error(c.red("No session id. Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.")); process.exit(1); return; }
2855
2919
  const config = loadConfig();
2856
2920
  if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
2857
- let token;
2858
- try { token = await localDeviceToken(config); } catch { token = undefined; }
2921
+ const token = await sessionAuth(config, sessionId);
2859
2922
  const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/delegated-runs`;
2860
2923
  const call = async (path, body) => {
2861
2924
  const res = await fetch(`${base}${path}`, {
@@ -3136,9 +3199,9 @@ async function appMenu(rest, { json = false } = {}) {
3136
3199
  async function appRequest(action, body, { print = true } = {}) {
3137
3200
  const config = loadConfig();
3138
3201
  if (!(await ensureNodeRunning(config))) throw new Error("Could not reach the Bivy node.");
3139
- const token = await localDeviceToken(config);
3202
+ const token = await sessionAuth(config, body.sessionId);
3140
3203
  const response = await fetch(`${url(config)}/api/apps/${action}`, {
3141
- method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: JSON.stringify(body),
3204
+ method: "POST", headers: { ...(token ? { authorization: `Bearer ${token}` } : {}), "content-type": "application/json" }, body: JSON.stringify(body),
3142
3205
  });
3143
3206
  const result = await response.json();
3144
3207
  if (!response.ok || result.error) throw new Error(result.error || `App command failed (${response.status}).`);
@@ -5821,6 +5884,22 @@ function readSelfVersion() {
5821
5884
  }
5822
5885
  }
5823
5886
 
5887
+ // A command an agent tried that doesn't exist is the best hint for what it
5888
+ // expected Bivy to have. Inside a session, keep the word (never its arguments)
5889
+ // in <data-dir>/agent-cli-misses.jsonl for `pnpm run eval:agent-ux` and
5890
+ // whoever names the next command. Bounded: one rotation at 256 KiB.
5891
+ function recordAgentMiss(word) {
5892
+ const session = resolveAttachSessionId({ env: process.env });
5893
+ if (!session || typeof word !== "string" || word.length > 64) return;
5894
+ try {
5895
+ const file = path.join(appDir, "agent-cli-misses.jsonl");
5896
+ if (fs.existsSync(file) && fs.statSync(file).size > 256 * 1024) fs.renameSync(file, `${file}.1`);
5897
+ fs.appendFileSync(file, `${JSON.stringify({ ts: Date.now(), session, word })}\n`, { mode: 0o600 });
5898
+ } catch {
5899
+ // Best-effort: a miss log must never change what the command does.
5900
+ }
5901
+ }
5902
+
5824
5903
  function printHelp() {
5825
5904
  const details = { run: `Agents: ${[...AGENT_INTEGRATIONS.keys()].join(" | ")}, or -- <command>` };
5826
5905
  console.log(`\n${renderHelp({ paint: c.cyan, bold: c.bold, details })}\n`);
@@ -5898,13 +5977,27 @@ function cmdGuide(args = []) {
5898
5977
  console.log(guide.markdown.trimEnd());
5899
5978
  }
5900
5979
 
5980
+ // Inside an agent session the daemon names its own CLI ($BIVY_NODE_CLI). When
5981
+ // `bivy` was found on PATH (every installer links it by that name) and is
5982
+ // another file, run the node's, so the agent gets the commands its node
5983
+ // documents. Running a file on purpose (`node …/bin/bivy.mjs`) is left alone.
5984
+ function nodeCliToRun() {
5985
+ const nodeCli = process.env.BIVY_NODE_CLI;
5986
+ if (path.basename(process.argv[1] ?? "") !== "bivy") return undefined;
5987
+ if (!nodeCli || !path.isAbsolute(nodeCli) || !fs.existsSync(nodeCli)) return undefined;
5988
+ try { return fs.realpathSync(nodeCli) === fs.realpathSync(selfScript) ? undefined : nodeCli; } catch { return undefined; }
5989
+ }
5990
+
5901
5991
  async function main() {
5992
+ const nodeCli = nodeCliToRun();
5993
+ if (nodeCli) process.exit(spawnSync(process.execPath, [nodeCli, ...process.argv.slice(2)], { stdio: "inherit", env: process.env }).status ?? 1);
5902
5994
  await hydrateCanonicalConfig();
5903
5995
  const argv = process.argv.slice(2);
5904
5996
  const [command, ...args] = argv;
5905
5997
  if (command === undefined) { printHelp(); return; }
5906
5998
  const entry = resolveCommand(command);
5907
5999
  if (!entry) {
6000
+ recordAgentMiss(command);
5908
6001
  const close = suggestCommands(command);
5909
6002
  cliError({
5910
6003
  code: "unknown_command",
@@ -6044,6 +6137,11 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
6044
6137
  break;
6045
6138
  }
6046
6139
  case "automation": {
6140
+ // An agent's apply is a proposal the user approves (see cmdAutomationProposal).
6141
+ if ((args[0] === "apply" && !args.includes("--dry-run") && !process.env.BIVY_AUTOMATION_DIRECT && resolveAttachSessionId({ env: process.env })) || args[0] === "proposal") {
6142
+ await cmdAutomationProposal(args);
6143
+ break;
6144
+ }
6047
6145
  if (!(await ensureDeps())) process.exit(1);
6048
6146
  process.exit(await run(nodeBin, [...nodeScriptArgs(automationEntry), ...args], { cwd: process.cwd(), env: process.env }));
6049
6147
  break;
@@ -78,7 +78,11 @@ export const COMMANDS = [
78
78
 
79
79
  // Runs and automations
80
80
  { name: "runs", group: "runs", scope: "account", json: true, usage: 'runs <start|list|status|wait>', summary: "Unattended Runs with checks, evidence and a Receipt", subcommands: ["start", "list", "status", "wait"] },
81
- { name: "automation", aliases: ["automations"], group: "runs", scope: "account", json: true, usage: "automation <list|trigger|init|validate|plan|test|apply>", summary: "Automations as code: validate, plan, apply, trigger", subcommands: ["list", "trigger", "init", "validate", "plan", "test", "test-filter", "apply"] },
81
+ { name: "automation", aliases: ["automations"], group: "runs", scope: "account", json: true, usage: "automation <list|trigger|init|validate|plan|test|apply>", summary: "Automations as code: validate, plan, apply, trigger", subcommands: ["list", "trigger", "init", "validate", "plan", "test", "test-filter", "apply", "proposal"],
82
+ tools: [
83
+ { name: "automation_plan", description: "Check an automations file (.bivy/automations.yaml) and show what each automation would do: trigger, routing, effective safety. Run it before automation_apply.", input: { path: { type: "string", description: "The automations file (default .bivy/automations.yaml)." } }, argv: ["automation", "plan", { arg: "path" }] },
84
+ { name: "automation_apply", description: "Propose applying an automations file to the user's account. They get an approval card listing each change; nothing is applied unless they approve. Returns the proposal: status applied, rejected, expired, failed, or pending (check later with bivy automation proposal <id> --wait).", input: { path: { type: "string", description: "The automations file (default .bivy/automations.yaml)." }, prune: { type: "boolean", description: "Also remove automations this file no longer has." } }, argv: ["automation", "apply", { arg: "path" }, { when: "prune", flag: "--prune" }] },
85
+ ] },
82
86
 
83
87
  // This node
84
88
  { name: "setup", aliases: ["init"], group: "node", scope: "node", usage: "setup", summary: "First-run wizard: agent, model login, remote sign-in, background service" },
@@ -9,11 +9,35 @@ infrastructure code:
9
9
  bivy automation validate # check it
10
10
  bivy automation plan --json # triggers, routing, effective safety
11
11
  bivy automation test --event event.json # which automation an event would start
12
- bivy automation apply # reconcile the account with the file
12
+ bivy automation apply # ask the user, then apply
13
13
  bivy automation list --json
14
14
  bivy automation trigger <id-or-key> # run one now
15
15
 
16
+ A scheduled one (triggers: schedule, github, linear, webhook, manual; the
17
+ format is in docs/automations-as-code.md):
18
+
19
+ version: 1
20
+ automations:
21
+ - id: nightly-tests
22
+ name: Nightly tests
23
+ trigger: schedule
24
+ schedule: { cron: "0 2 * * *", timezone: UTC } # or { at: "2026-10-01T09:00:00Z" }
25
+ repo: owner/name # required for schedules
26
+ instructions: |
27
+ Run the test suite. If it fails, open an issue with the failing tests.
28
+ safety:
29
+ approval: risky
30
+ sandbox: workspace-write
31
+
32
+ From a session, `apply` goes through the machine's approval mode. Usually the
33
+ user gets a card listing every change and decides. With "never ask" it applies
34
+ at once, and the chat records what changed either way. The command waits up to
35
+ 90 seconds for an answer and prints apply's output once it's applied (exit 0). Exit 1 means they declined
36
+ (don't retry the same thing), and exit 5 means they haven't answered yet: carry
37
+ on, and check later with `bivy automation proposal <id> --wait`.
38
+ `apply --dry-run` shows the changes without asking anyone.
39
+
16
40
  To pause one, set `enabled: false` on it and apply. Always `validate` and
17
- `plan` before `apply`, and show the user the plan when it
18
- changes something they didn't ask for. `apply --prune` also removes automations
19
- the file no longer has. `bivy automation --help` has every flag.
41
+ `plan` first, and say in the chat what the automation will do and when.
42
+ `apply --prune` also removes automations the file no longer has.
43
+ `bivy automation --help` has every flag.
@@ -49,6 +49,8 @@ export const BIVY_AGENT_NOTE = [
49
49
  '`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
50
50
  "its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
51
51
  "lists machines and their agents. Use your own sub-agents for everything else.",
52
+ "- Work that should run on its own later (on a schedule, or when CI fails or an issue arrives) is a Bivy automation: " +
53
+ "`bivy guide automate`. Applying one may ask the user first.",
52
54
  "- `bivy context` shows your session, workspace and published apps; `bivy guide` has short playbooks; " +
53
55
  "`bivy help` lists every command (`--json` for all).",
54
56
  ].join("\n");
package/dist/approval.js CHANGED
@@ -30,7 +30,7 @@ export class ApprovalManager {
30
30
  }
31
31
  async request(input) {
32
32
  const request = {
33
- id: randomUUID(),
33
+ id: input.id ?? randomUUID(),
34
34
  sessionId: input.sessionId,
35
35
  toolName: input.toolName,
36
36
  input: input.toolInput,