@bivy/bivy 0.15.0-staging.4 → 0.15.0-staging.41

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
@@ -117,10 +117,16 @@ curl -fsSL https://bivy.sh/install.sh | bash
117
117
 
118
118
  macOS and Linux. Requires Node.js 20 or newer. The installer puts the
119
119
  [`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) npm package and the
120
- `bivy` command on your `PATH` with optional bridges skipped for speed, then runs the guided `bivy setup` wizard — agent
121
- choice, selected-agent install if needed, remote access, and an auto-start background service (launchd on macOS,
122
- systemd on Linux). Re-running it on a machine that already has Bivy just applies
123
- the latest build and restarts the service.
120
+ `bivy` command on your `PATH` with optional bridges skipped for speed, then runs
121
+ `bivy setup` — agent choice, selected-agent install if needed, remote access,
122
+ and an auto-start background service (launchd on macOS, systemd on Linux).
123
+
124
+ If Claude Code, Codex, OpenCode, Gemini, or another agent is already installed,
125
+ setup says so and uses that existing CLI, login, and configuration. Bivy does
126
+ not replace the agent or copy its native credentials; it adds remote continuity
127
+ and governance around the agent you already use. Re-running the installer on a
128
+ machine that already has Bivy just applies the latest build and restarts the
129
+ service.
124
130
 
125
131
  **What needs an account, and what doesn't.** The CLI alone — `bivy run`,
126
132
  `bivy resume`, `bivy sessions` — needs no account and no server; `bivy setup`
@@ -131,11 +137,19 @@ UI needs a control plane, because the node hosts none: use the hosted one at
131
137
  [self-host your own](docs/self-host-quickstart.md). Switch any time with
132
138
  `bivy relay:setup`.
133
139
 
140
+ Prefer to inspect the installer first?
141
+
142
+ ```bash
143
+ curl -fsSL https://bivy.sh/install.sh -o install.sh
144
+ less install.sh
145
+ bash install.sh
146
+ ```
147
+
134
148
  **What the installer does with sudo.** It escalates only when it must, and
135
149
  tells you when it does:
136
150
 
137
- - Debian/Ubuntu without a suitable Node.js: `sudo apt-get install build-essential
138
- python3 curl`, then NodeSource's Node 22 setup script via `sudo`.
151
+ - Debian/Ubuntu without a suitable Node.js: `sudo apt-get install curl
152
+ ca-certificates`, then NodeSource's Node 22 setup script via `sudo`.
139
153
  - Other Linux, or macOS, without a suitable Node.js: downloads the official
140
154
  Node 22 tarball from nodejs.org (sha256-checked) and installs it under
141
155
  `/usr/local` with `sudo`.
@@ -267,9 +281,9 @@ See [`docs/remote-access.md`](docs/remote-access.md) and
267
281
 
268
282
  ## Supported agents
269
283
 
270
- **Claude Code and Codex are the recommended, release-certified paths.** The
271
- broader catalog stays available under **More agents**; capabilities and fidelity
272
- vary by runtime.
284
+ **Bivy maintains wrappers for the popular coding agents below.** Claude Code,
285
+ Codex, Pi, and OpenCode have the richest release-tested capability sets today;
286
+ capabilities and fidelity still vary by runtime.
273
287
 
274
288
  | Agent | Command | Notes |
275
289
  |---|---|---|
@@ -6,7 +6,7 @@
6
6
  "command": "codex",
7
7
  "hidden": true,
8
8
  "supportTier": "supported",
9
- "certification": "unverified",
9
+ "certification": "adapter-tested",
10
10
  "headlessFlags": [
11
11
  "exec"
12
12
  ],
@@ -37,7 +37,7 @@
37
37
  "label": "Aider",
38
38
  "command": "aider",
39
39
  "hidden": false,
40
- "supportTier": "beta",
40
+ "supportTier": "supported",
41
41
  "certification": "adapter-tested",
42
42
  "headlessFlags": [
43
43
  "--yes-always",
@@ -53,8 +53,8 @@
53
53
  "label": "Hermes",
54
54
  "command": "hermes",
55
55
  "hidden": true,
56
- "supportTier": "beta",
57
- "certification": "adapter-tested",
56
+ "supportTier": "experimental",
57
+ "certification": "unverified",
58
58
  "headlessFlags": [],
59
59
  "install": {
60
60
  "kind": "npm",
@@ -66,7 +66,7 @@
66
66
  "label": "Goose",
67
67
  "command": "goose",
68
68
  "hidden": false,
69
- "supportTier": "beta",
69
+ "supportTier": "supported",
70
70
  "certification": "adapter-tested",
71
71
  "headlessFlags": [
72
72
  "run",
@@ -87,7 +87,7 @@
87
87
  "label": "Gemini CLI",
88
88
  "command": "gemini",
89
89
  "hidden": false,
90
- "supportTier": "beta",
90
+ "supportTier": "supported",
91
91
  "certification": "adapter-tested",
92
92
  "headlessFlags": [
93
93
  "-p",
@@ -105,7 +105,7 @@
105
105
  "label": "Qwen Code",
106
106
  "command": "qwen",
107
107
  "hidden": false,
108
- "supportTier": "beta",
108
+ "supportTier": "supported",
109
109
  "certification": "adapter-tested",
110
110
  "headlessFlags": [
111
111
  "-p",
@@ -123,7 +123,7 @@
123
123
  "label": "Cline",
124
124
  "command": "cline",
125
125
  "hidden": false,
126
- "supportTier": "beta",
126
+ "supportTier": "supported",
127
127
  "certification": "adapter-tested",
128
128
  "headlessFlags": [
129
129
  "-y",
@@ -139,7 +139,7 @@
139
139
  "label": "Crush",
140
140
  "command": "crush",
141
141
  "hidden": false,
142
- "supportTier": "beta",
142
+ "supportTier": "supported",
143
143
  "certification": "adapter-tested",
144
144
  "headlessFlags": [
145
145
  "run",
@@ -155,7 +155,7 @@
155
155
  "label": "Cursor",
156
156
  "command": "cursor-agent",
157
157
  "hidden": false,
158
- "supportTier": "beta",
158
+ "supportTier": "supported",
159
159
  "certification": "adapter-tested",
160
160
  "headlessFlags": [
161
161
  "--force",
@@ -172,7 +172,7 @@
172
172
  "label": "GitHub Copilot",
173
173
  "command": "copilot",
174
174
  "hidden": false,
175
- "supportTier": "beta",
175
+ "supportTier": "supported",
176
176
  "certification": "adapter-tested",
177
177
  "headlessFlags": [
178
178
  "--allow-all-tools",
@@ -188,7 +188,7 @@
188
188
  "label": "Grok",
189
189
  "command": "grok",
190
190
  "hidden": false,
191
- "supportTier": "beta",
191
+ "supportTier": "supported",
192
192
  "certification": "adapter-tested",
193
193
  "headlessFlags": [
194
194
  "-p",
@@ -205,7 +205,7 @@
205
205
  "label": "Amp",
206
206
  "command": "amp",
207
207
  "hidden": false,
208
- "supportTier": "beta",
208
+ "supportTier": "supported",
209
209
  "certification": "adapter-tested",
210
210
  "headlessFlags": [
211
211
  "-x",
@@ -222,7 +222,7 @@
222
222
  "label": "Auggie",
223
223
  "command": "auggie",
224
224
  "hidden": false,
225
- "supportTier": "beta",
225
+ "supportTier": "supported",
226
226
  "certification": "adapter-tested",
227
227
  "headlessFlags": [
228
228
  "--quiet",
@@ -238,7 +238,7 @@
238
238
  "label": "Droid",
239
239
  "command": "droid",
240
240
  "hidden": false,
241
- "supportTier": "beta",
241
+ "supportTier": "supported",
242
242
  "certification": "adapter-tested",
243
243
  "headlessFlags": [
244
244
  "exec",
@@ -256,7 +256,7 @@
256
256
  "label": "Continue",
257
257
  "command": "cn",
258
258
  "hidden": false,
259
- "supportTier": "beta",
259
+ "supportTier": "supported",
260
260
  "certification": "adapter-tested",
261
261
  "headlessFlags": [
262
262
  "--auto",
@@ -272,7 +272,7 @@
272
272
  "label": "Kilo Code",
273
273
  "command": "kilo",
274
274
  "hidden": false,
275
- "supportTier": "beta",
275
+ "supportTier": "supported",
276
276
  "certification": "adapter-tested",
277
277
  "headlessFlags": [
278
278
  "run",
@@ -289,7 +289,7 @@
289
289
  "label": "Rovo Dev",
290
290
  "command": "acli",
291
291
  "hidden": false,
292
- "supportTier": "beta",
292
+ "supportTier": "supported",
293
293
  "certification": "adapter-tested",
294
294
  "headlessFlags": [
295
295
  "rovodev",
package/bin/bivy.mjs CHANGED
@@ -543,6 +543,11 @@ function commandExists(cmd) {
543
543
  return runQuiet("sh", ["-lc", "command -v -- \"$1\" >/dev/null 2>&1", "sh", cmd]).code === 0;
544
544
  }
545
545
 
546
+ function commandOnPath(cmd) {
547
+ const result = runQuiet("sh", ["-lc", "command -v -- \"$1\"", "sh", cmd]);
548
+ return result.code === 0 ? result.stdout.trim().split(/\r?\n/)[0] || "" : "";
549
+ }
550
+
546
551
  function npmGlobalBinCommand(cmd) {
547
552
  if (!commandExists("npm")) return "";
548
553
  const prefix = runQuiet("npm", ["prefix", "-g"]);
@@ -552,9 +557,13 @@ function npmGlobalBinCommand(cmd) {
552
557
  return fs.existsSync(candidate) ? candidate : "";
553
558
  }
554
559
 
560
+ function nodeAtLeast(minMajor, minMinor = 0) {
561
+ const [major, minor] = process.versions.node.split(".").map(Number);
562
+ return major > minMajor || (major === minMajor && minor >= minMinor);
563
+ }
564
+
555
565
  function hasSupportedNode() {
556
- const [major] = process.versions.node.split(".").map(Number);
557
- return major >= 20;
566
+ return nodeAtLeast(20);
558
567
  }
559
568
 
560
569
  async function ensureDeps() {
@@ -612,8 +621,15 @@ function nodePackageInstalled(packageName) {
612
621
  return runQuiet(nodeBin, ["-e", "require.resolve(process.argv[1], { paths: [process.argv[2]] })", packageName, repoRoot]).code === 0;
613
622
  }
614
623
 
624
+ function nodePackageLoadable(packageName) {
625
+ return runQuiet(nodeBin, ["-e", "require(require.resolve(process.argv[1], { paths: [process.argv[2]] }))", packageName, repoRoot]).code === 0;
626
+ }
627
+
615
628
  async function ensureNodePackage(packageName) {
616
- if (nodePackageInstalled(packageName)) return true;
629
+ if (nodePackageInstalled(packageName)) {
630
+ console.log(c.green(` ✓ Found Bivy bridge package ${packageName}`));
631
+ return true;
632
+ }
617
633
  // Add with the package manager that owns this tree. Running `npm install` in a
618
634
  // pnpm workspace would write a competing package-lock.json and a hoisted
619
635
  // node_modules over pnpm's symlink layout, leaving the checkout in a state
@@ -626,7 +642,7 @@ async function ensureNodePackage(packageName) {
626
642
  console.error(c.red(`${cmd} is required to install ${packageName}.`));
627
643
  return false;
628
644
  }
629
- console.log(c.dim(`Installing ${packageName}…`));
645
+ console.log(c.dim(`Installing Bivy bridge package ${packageName}…`));
630
646
  const code = await run(cmd, [...baseArgs, packageName], { cwd: repoRoot });
631
647
  return code === 0 && nodePackageInstalled(packageName);
632
648
  }
@@ -634,13 +650,18 @@ async function ensureNodePackage(packageName) {
634
650
  const userLocalPrefix = process.env.BIVY_NPM_GLOBAL_PREFIX || path.join(os.homedir(), ".local");
635
651
 
636
652
  async function ensureNpmCommand(command, packageName, label) {
637
- if (commandExists(command)) return true;
653
+ const existing = commandOnPath(command);
654
+ if (existing) {
655
+ console.log(c.green(` ✓ Found existing ${label}: ${existing}`));
656
+ console.log(c.dim(` Bivy will use your installed ${label}; it will not replace its auth or configuration.`));
657
+ return true;
658
+ }
638
659
  if (!commandExists("npm")) {
639
660
  console.log(c.yellow(`Skipping ${label}: npm is not available.`));
640
661
  return false;
641
662
  }
642
663
  fs.mkdirSync(path.join(userLocalPrefix, "bin"), { recursive: true });
643
- console.log(c.dim(`Installing ${label} (${packageName})…`));
664
+ console.log(c.dim(`Installing ${label} (${packageName}) because it was not found on PATH…`));
644
665
  const code = await run("npm", ["install", "--global", "--prefix", userLocalPrefix, packageName, "--no-audit", "--no-fund"]);
645
666
  return code === 0 && commandExists(command);
646
667
  }
@@ -4121,6 +4142,14 @@ async function cmdDoctor(args = []) {
4121
4142
 
4122
4143
  console.log(c.bold("\n Bivy doctor\n"));
4123
4144
  console.log(` ${mark(hasSupportedNode())} Node ${process.version}${hasSupportedNode() ? "" : c.dim(" (needs >= 20.0.0)")}`);
4145
+ const ptyInstalled = nodePackageInstalled("node-pty");
4146
+ const ptyUsable = ptyInstalled && nodePackageLoadable("node-pty");
4147
+ console.log(` ${mark(ptyUsable, true)} terminal PTY ${ptyUsable ? c.green("available") : ptyInstalled ? c.yellow("installed but not loadable — reinstall after installing build tools") : c.dim("optional dependency missing — interactive terminals unavailable; chat/exec still work")}`);
4148
+ const claudeBridge = nodePackageInstalled("@anthropic-ai/claude-agent-sdk");
4149
+ console.log(` ${mark(claudeBridge, true)} Claude bridge ${claudeBridge ? c.green("installed") : c.dim("optional — installed on first Claude setup/use")}`);
4150
+ const piBridge = nodePackageInstalled("@earendil-works/pi-coding-agent");
4151
+ const piNodeOk = nodeAtLeast(22, 19);
4152
+ console.log(` ${mark(piBridge && piNodeOk, true)} Pi bridge ${piBridge ? (piNodeOk ? c.green("installed") : c.yellow("installed but needs Node >=22.19")) : c.dim("optional — installed only if you choose Pi")}`);
4124
4153
  console.log(` ${mark(commandExists("git"), true)} git${commandExists("git") ? "" : c.dim(" (recommended for repo-backed sessions)")}`);
4125
4154
  // GitHub is optional (a "No repo" session needs none), so this only ever warns.
4126
4155
  // `gh` is NOT required — it's a token fallback; the primary path is Bivy's own
@@ -5091,8 +5120,10 @@ Unlike 'bivy run', these commands operate on governed background Runs with check
5091
5120
  }
5092
5121
  const remote = await openRemoteApp();
5093
5122
  if (!remote) {
5094
- console.log("No remote access configured yet.");
5095
- console.log(`Run ${c.cyan("bivy relay:setup")} to enable the web/PWA app, then ${c.cyan("bivy open")}.`);
5123
+ console.log(c.yellow("Remote access is not configured on this machine yet."));
5124
+ console.log("The local node hosts only an API/WebSocket, not the Bivy PWA.");
5125
+ console.log(`Run ${c.cyan("bivy relay:setup")} to connect this node to the hosted or self-hosted remote web/PWA app, then run ${c.cyan("bivy open")} again.`);
5126
+ process.exitCode = 1;
5096
5127
  } else if (!canOpenBrowser()) {
5097
5128
  console.log(`Open the Bivy app here: ${c.cyan(remote.remoteBase)}`);
5098
5129
  }
@@ -0,0 +1,42 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright (c) 2026 Petter André Sjulstad
3
+ import { withExactCapabilitySurface } from "../../runtime/types.js";
4
+ /**
5
+ * The ONE capability table for Pi, shared by the real `PiRuntime` and the
6
+ * `LazyPiRuntime` facade the registry hands out before the SDK is loaded.
7
+ *
8
+ * This module deliberately imports nothing from the pi SDK so the lazy facade
9
+ * can use it without defeating its purpose. Keeping a single source of truth is
10
+ * a correctness requirement, not tidiness: the daemon reads flags such as
11
+ * `sessionRefIsPath` off whichever runtime object the registry returns — the
12
+ * facade — *before* any session is opened. When the facade's copy of this table
13
+ * drifted from the real one and lost `sessionRefIsPath`, the daemon treated every
14
+ * pi resume ref as an opaque id, stripped the transcript path to its basename,
15
+ * and pi created a brand-new empty session for every message sent to a closed
16
+ * chat (the "sending to a closed session creates a new one" regression).
17
+ */
18
+ export const PI_CAPABILITIES = withExactCapabilitySurface({
19
+ toolInterception: true,
20
+ modelSelection: true,
21
+ packages: true,
22
+ resume: true,
23
+ fork: false,
24
+ // pi resumes by session-file path (under the node's sessions dir), so the
25
+ // daemon applies its path-traversal guard to these refs.
26
+ sessionRefIsPath: true,
27
+ // pi's TUI ships with the node, so the chat<->TUI hand-off is always offered.
28
+ interactiveTui: true,
29
+ usageReporting: true,
30
+ // pi can find a prior on-disk session by cwd + start time, so a `bivy run`
31
+ // terminal with no pinned sessionId can still be continued as a governed chat.
32
+ sessionDiscovery: true,
33
+ // pi transcripts are structured messages that round-trip through the session
34
+ // store, so a pi->pi fork is full fidelity (see exportForFork/importForFork).
35
+ forkTransport: true,
36
+ // pi can also stand up a session from portable {role,text} history, so a fork
37
+ // FROM another agent INTO pi is a true replay, not a seeded summary.
38
+ forkHistoryImport: true,
39
+ // The pi-coding-agent SDK implements both explicitly: prompting mid-turn
40
+ // with no streamingBehavior hint throws, forcing every caller to choose.
41
+ streamingBehaviors: ["steer", "followUp"],
42
+ });
@@ -4,19 +4,8 @@ import { spawnSync } from "node:child_process";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { defineAgentIntegration } from "../definition.js";
7
- import { withExactCapabilitySurface } from "../../runtime/types.js";
7
+ import { PI_CAPABILITIES } from "./capabilities.js";
8
8
  export const PI_TESTED_VERSION = "0.84.3";
9
- const PI_CAPABILITIES = withExactCapabilitySurface({
10
- toolInterception: true,
11
- modelSelection: true,
12
- packages: true,
13
- resume: true,
14
- fork: false,
15
- interactiveTui: true,
16
- usageReporting: true,
17
- sessionDiscovery: true,
18
- streamingBehaviors: ["steer", "followUp"],
19
- });
20
9
  export function piAgentDir() {
21
10
  return process.env.PI_CODING_AGENT_DIR?.trim() || path.join(os.homedir(), ".pi", "agent");
22
11
  }
@@ -40,6 +29,15 @@ export function piCommandAvailable() {
40
29
  export function invalidatePiCommandProbe() {
41
30
  PI_COMMAND_CACHE.clear();
42
31
  }
32
+ export function piBridgeInstalled() {
33
+ try {
34
+ import.meta.resolve("@earendil-works/pi-coding-agent");
35
+ return true;
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
43
41
  function unsupportedNodeMessage() {
44
42
  return `Pi requires Node.js 22.19+ (found ${process.version}). Upgrade Node, or select another agent such as Claude Code/Codex/OpenCode.`;
45
43
  }
@@ -87,13 +85,15 @@ export function piIntegration(origin) {
87
85
  visible: true,
88
86
  origin,
89
87
  describe: () => {
90
- const installed = piCommandAvailable();
88
+ const commandInstalled = piCommandAvailable();
89
+ const bridgeInstalled = piBridgeInstalled();
90
+ const installed = commandInstalled && bridgeInstalled && nodeSupportsPi();
91
91
  return {
92
92
  id: "pi",
93
93
  executionMode: "protocol",
94
94
  displayName: "Pi",
95
95
  description: "The operator-installed Pi coding agent connected to Bivy for durable sessions, governance, packages, and model selection.",
96
- status: installed && nodeSupportsPi() ? "available" : "external",
96
+ status: installed ? "available" : "external",
97
97
  packageName: "@earendil-works/pi-coding-agent",
98
98
  language: "TypeScript",
99
99
  capabilities: PI_CAPABILITIES,
@@ -105,7 +105,9 @@ export function piIntegration(origin) {
105
105
  ? unsupportedNodeMessage()
106
106
  : installed
107
107
  ? "Uses the Pi command and agent-owned auth/configuration already on this node, and hands sessions back to that native TUI."
108
- : "Install and sign in to Pi on this node; Bivy will connect to that existing agent.",
108
+ : commandInstalled
109
+ ? "Pi is on PATH, but Bivy's optional Pi bridge is not installed. Select Pi in setup or run 'bivy agents:install'."
110
+ : "Install and sign in to Pi on this node; Bivy will connect to that existing agent.",
109
111
  install: installed ? undefined : {
110
112
  label: "Install Pi",
111
113
  description: "Installs the upstream Pi coding agent on this node.",
@@ -116,6 +118,8 @@ export function piIntegration(origin) {
116
118
  create: (options) => {
117
119
  if (!piCommandAvailable())
118
120
  throw new Error(`Pi command not found on PATH: ${piCommand()}`);
121
+ if (!piBridgeInstalled())
122
+ throw new Error("Bivy's optional Pi bridge is not installed. Run 'bivy setup' and choose Pi, or run 'bivy agents:install'.");
119
123
  // Vault-backed credentials (see catalogRuntimes): the daemon-hosted Pi
120
124
  // session reads the shared vault the user signed in to, not Pi's own
121
125
  // plaintext auth.json. The agent dir still supplies config/models/packages.
@@ -15,7 +15,7 @@ import { toModelInfo as sharedToModelInfo } from "../../runtime/normalize.js";
15
15
  import { provisionPiAuthJson } from "../../runtime/credential-provisioning.js";
16
16
  import { isNativeOAuthProvider } from "../../runtime/oauth/model-oauth-providers.js";
17
17
  import { bivySessionEnv } from "../../runtime/session-env.js";
18
- import { withExactCapabilitySurface } from "../../runtime/types.js";
18
+ import { PI_CAPABILITIES } from "./capabilities.js";
19
19
  import { mapToolCall, mapToolResult } from "../../runtime/tool-call-map.js";
20
20
  import { BackgroundShellTracker, createBackgroundAwareBashOperations } from "./background-shell.js";
21
21
  /**
@@ -406,28 +406,9 @@ export class PiRuntime {
406
406
  options;
407
407
  id = "pi";
408
408
  displayName = "Pi";
409
- capabilities = withExactCapabilitySurface({
410
- toolInterception: true,
411
- modelSelection: true,
412
- packages: true,
413
- resume: true,
414
- fork: false,
415
- // pi resumes by session-file path (under the node's sessions dir), so the
416
- // daemon applies its path-traversal guard to these refs.
417
- sessionRefIsPath: true,
418
- // pi's TUI ships with the node, so the chat<->TUI hand-off is always offered.
419
- interactiveTui: true,
420
- usageReporting: true,
421
- // pi transcripts are structured messages that round-trip through the session
422
- // store, so a pi->pi fork is full fidelity (see exportForFork/importForFork).
423
- forkTransport: true,
424
- // pi can also stand up a session from portable {role,text} history, so a fork
425
- // FROM another agent INTO pi is a true replay, not a seeded summary.
426
- forkHistoryImport: true,
427
- // The pi-coding-agent SDK implements both explicitly: prompting mid-turn
428
- // with no streamingBehavior hint throws, forcing every caller to choose.
429
- streamingBehaviors: ["steer", "followUp"],
430
- });
409
+ // Shared with LazyPiRuntime (the facade the registry actually hands out) —
410
+ // see capabilities.ts for why the two must never drift.
411
+ capabilities = PI_CAPABILITIES;
431
412
  constructor(options) {
432
413
  this.options = options;
433
414
  }
@@ -82,7 +82,7 @@ export const AGENT_PROFILES = {
82
82
  // git-aware turn and exits instead of dropping into the REPL.
83
83
  args: ["--yes-always", "--message"],
84
84
  promptMode: "argv",
85
- supportTier: "beta",
85
+ supportTier: "supported",
86
86
  authOwner: "mixed",
87
87
  blurb: "Popular git-native pair-programming agent (Aider).",
88
88
  // `aider --model <id> …` — a leading option (insertAt: 0). Aider resolves its
@@ -116,6 +116,7 @@ export const AGENT_PROFILES = {
116
116
  // Hidden from the picker: dumb-pipe adapter with no validated JSON parser or
117
117
  // documented session/resume flag (still runnable via BIVY_RUNTIME=hermes).
118
118
  hidden: true,
119
+ supportTier: "experimental",
119
120
  // No `resume`: no documented session/resume flag.
120
121
  install: { kind: "npm", pkg: "hermes-agent" },
121
122
  },
@@ -137,7 +138,7 @@ export const AGENT_PROFILES = {
137
138
  // BIVY_GOOSE_ACP=1 (or global BIVY_PREFER_ACP=1); off by default until validated.
138
139
  acp: { args: ["acp"] },
139
140
  promptMode: "argv",
140
- supportTier: "beta",
141
+ supportTier: "supported",
141
142
  blurb: "Block's open-source agent with a structured stream-json protocol (Goose).",
142
143
  // Homebrew isn't present on stock Linux nodes (brew → ENOENT); the official
143
144
  // download script installs the goose binary on both Linux and macOS.
@@ -154,7 +155,7 @@ export const AGENT_PROFILES = {
154
155
  displayName: "Gemini CLI",
155
156
  command: "gemini",
156
157
  packageName: "@google/gemini-cli",
157
- supportTier: "beta",
158
+ supportTier: "supported",
158
159
  blurb: "Google's terminal coding agent (Gemini CLI).",
159
160
  // `gemini -m <id> … -p "<prompt>"` — a leading option before the trailing `-p`
160
161
  // (insertAt: 0). The prompt flag stays last, so prepending is safe.
@@ -197,7 +198,7 @@ export const AGENT_PROFILES = {
197
198
  displayName: "Qwen Code",
198
199
  command: "qwen",
199
200
  packageName: "@qwen-code/qwen-code",
200
- supportTier: "beta",
201
+ supportTier: "supported",
201
202
  blurb: "Alibaba's Qwen Code CLI (a Gemini-CLI fork tuned for Qwen-Coder models).",
202
203
  // Gemini-CLI fork: same `-m <id> … -p` model flag (insertAt: 0).
203
204
  model: {
@@ -241,7 +242,7 @@ export const AGENT_PROFILES = {
241
242
  displayName: "Cline",
242
243
  command: "cline",
243
244
  packageName: "cline",
244
- supportTier: "beta",
245
+ supportTier: "supported",
245
246
  blurb: "Cline's standalone terminal agent (the CLI sibling of the Cline IDE extension).",
246
247
  // `cline -y "<prompt>"` runs one autonomous, non-interactive task (‑y/‑‑yolo
247
248
  // skips per-tool prompts so a piped run doesn't wedge on approval). Bivy's
@@ -264,7 +265,7 @@ export const AGENT_PROFILES = {
264
265
  displayName: "Crush",
265
266
  command: "crush",
266
267
  packageName: "@charmland/crush",
267
- supportTier: "beta",
268
+ supportTier: "supported",
268
269
  blurb: "Charm's glamourous open-source coding agent (Crush).",
269
270
  // `crush run "<prompt>"` runs a single non-interactive prompt and exits;
270
271
  // `-q/--quiet` suppresses the spinner UI so stdout is just the reply.
@@ -285,7 +286,7 @@ export const AGENT_PROFILES = {
285
286
  displayName: "Cursor",
286
287
  command: "cursor-agent",
287
288
  packageName: "cursor (curl https://cursor.com/install)",
288
- supportTier: "beta",
289
+ supportTier: "supported",
289
290
  blurb: "Cursor's standalone terminal coding agent (cursor-agent) — the editor's engine on the CLI.",
290
291
  // `cursor-agent --force -p "<prompt>"` runs one non-interactive print turn and
291
292
  // exits (`-p/--print`); `--force` auto-approves tool/command execution so a
@@ -320,7 +321,7 @@ export const AGENT_PROFILES = {
320
321
  displayName: "GitHub Copilot",
321
322
  command: "copilot",
322
323
  packageName: "@github/copilot",
323
- supportTier: "beta",
324
+ supportTier: "supported",
324
325
  blurb: "GitHub's official terminal coding agent (Copilot CLI).",
325
326
  // `copilot --allow-all-tools -p "<prompt>"` runs one programmatic turn and
326
327
  // exits; --allow-all-tools skips per-tool approval so a piped run doesn't
@@ -352,7 +353,7 @@ export const AGENT_PROFILES = {
352
353
  behaviors: { preflight: "grok", prepare: "grok-auth", nativeSessions: "grok" },
353
354
  command: "grok",
354
355
  packageName: "grok (curl -fsSL https://x.ai/cli/install.sh | bash)",
355
- supportTier: "beta",
356
+ supportTier: "supported",
356
357
  authOwner: "mixed",
357
358
  blurb: "xAI's official Grok coding agent (Grok CLI) — SuperGrok/X subscription or API key.",
358
359
  // Official CLI: `grok -p "<prompt>"` (alias `--single`) runs one headless
@@ -392,7 +393,7 @@ export const AGENT_PROFILES = {
392
393
  displayName: "Amp",
393
394
  command: "amp",
394
395
  packageName: "@sourcegraph/amp",
395
- supportTier: "beta",
396
+ supportTier: "supported",
396
397
  blurb: "Sourcegraph's autonomous coding agent with persistent threads (Amp).",
397
398
  // `amp -x "<prompt>"` (`--execute`) runs one thread turn and streams to stdout;
398
399
  // Amp doesn't gate tools per-run (governed by its own allowlist config), so no
@@ -414,7 +415,7 @@ export const AGENT_PROFILES = {
414
415
  displayName: "Auggie",
415
416
  command: "auggie",
416
417
  packageName: "@augmentcode/auggie",
417
- supportTier: "beta",
418
+ supportTier: "supported",
418
419
  blurb: "Augment Code's terminal agent backed by its codebase context engine (Auggie).",
419
420
  // `auggie --quiet --print "<prompt>"` runs one non-interactive turn and prints
420
421
  // the final reply (`--print`); `--quiet` drops the UI chatter. Prompt trails.
@@ -428,7 +429,7 @@ export const AGENT_PROFILES = {
428
429
  displayName: "Droid",
429
430
  command: "droid",
430
431
  packageName: "droid (curl https://app.factory.ai/cli)",
431
- supportTier: "beta",
432
+ supportTier: "supported",
432
433
  blurb: "Factory AI's autonomous terminal coding agent (Droid).",
433
434
  // `droid exec --auto high "<prompt>"` runs one headless task at high autonomy
434
435
  // (auto-approves) and streams to stdout. Prompt trails the `exec` subcommand.
@@ -456,7 +457,7 @@ export const AGENT_PROFILES = {
456
457
  displayName: "Continue",
457
458
  command: "cn",
458
459
  packageName: "@continuedev/cli",
459
- supportTier: "beta",
460
+ supportTier: "supported",
460
461
  blurb: "Continue's headless terminal agent (cn) driving configurable assistants.",
461
462
  // `cn --auto -p "<prompt>"` runs one headless turn (`-p` = no TUI) and prints
462
463
  // the final response; `--auto` allows all tools without prompting. Prompt
@@ -484,7 +485,7 @@ export const AGENT_PROFILES = {
484
485
  displayName: "Kilo Code",
485
486
  command: "kilo",
486
487
  packageName: "@kilocode/cli",
487
- supportTier: "beta",
488
+ supportTier: "supported",
488
489
  blurb: "Kilo Code's terminal CLI (an OpenCode fork) for pipeline-friendly agentic coding.",
489
490
  // `kilo run --auto "<prompt>"` runs one non-interactive turn (`run`) with
490
491
  // auto-approved permissions (`--auto`) and streams to stdout. Prompt trails.
@@ -518,7 +519,7 @@ export const AGENT_PROFILES = {
518
519
  displayName: "Rovo Dev",
519
520
  command: "acli",
520
521
  packageName: "atlassian acli (rovodev)",
521
- supportTier: "beta",
522
+ supportTier: "supported",
522
523
  blurb: "Atlassian's Rovo Dev terminal coding agent, run through the acli CLI.",
523
524
  // `acli rovodev run --yolo "<prompt>"` runs one instruction headlessly; --yolo
524
525
  // skips tool-approval prompts. Prompt trails the `rovodev run` subcommand.
@@ -3,9 +3,11 @@ export function certificationEntry(id) {
3
3
  return CERTIFICATION_MATRIX.agents.find((entry) => entry.id === id);
4
4
  }
5
5
  /**
6
- * The paid Supported promise is derived from certification, never merely copied
7
- * from a profile. A suspended/missing entry, wrong adapter mode, stale pin, or
8
- * missing required runtime capability is downgraded to Beta in the picker.
6
+ * Supported means Bivy maintains a wrapper for this integration. Certification is
7
+ * a separate fidelity signal: an active matrix entry verifies the richer release-
8
+ * tested capability set for a pinned adapter version, while drift or a different
9
+ * configured path keeps the wrapper Supported but marks the capability surface as
10
+ * adapter-tested instead of release-tested.
9
11
  */
10
12
  export function applyCertification(runtime) {
11
13
  if (runtime.supportTier !== "supported")
@@ -18,9 +20,8 @@ export function applyCertification(runtime) {
18
20
  if (!eligible) {
19
21
  return {
20
22
  ...runtime,
21
- supportTier: "beta",
22
- certification: "adapter-tested",
23
- notes: `${runtime.notes ? `${runtime.notes} ` : ""}This configured path is not the release-certified execution mode.`,
23
+ certification: runtime.certification ?? "adapter-tested",
24
+ notes: `${runtime.notes ? `${runtime.notes} ` : ""}This wrapper is maintained by Bivy; its current configured path is adapter-tested rather than release-tested.`,
24
25
  };
25
26
  }
26
27
  return { ...runtime, certification: "release-tested", testedVersion: entry.pinnedVersion };
@@ -34,7 +34,7 @@ export function linearBranchName(identifier) {
34
34
  const slug = identifier.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
35
35
  return `bivy/linear-${slug || "issue"}`;
36
36
  }
37
- export function buildLinearTaskPrompt(issue) {
37
+ export function buildLinearTaskPrompt(issue, instructions) {
38
38
  return [
39
39
  `You are working on Linear issue ${issue.identifier}: ${issue.title}`,
40
40
  "",
@@ -42,8 +42,8 @@ export function buildLinearTaskPrompt(issue) {
42
42
  "",
43
43
  issue.url ? `Issue: ${issue.url}` : "",
44
44
  "",
45
- "Understand the issue and surrounding codebase, implement it completely, and run the project's tests, linter, and type-checker.",
45
+ instructions?.trim() || "Understand the issue and surrounding codebase, implement it completely, and run the project's tests, linter, and type-checker.",
46
46
  "",
47
- `When finished, commit and push your changes and open a pull request yourself. Include a link to ${issue.identifier} in the pull request description.`,
47
+ instructions?.trim() ? "" : `When finished, commit and push your changes and open a pull request yourself. Include a link to ${issue.identifier} in the pull request description.`,
48
48
  ].filter((part, index, all) => part !== "" || all[index - 1] !== "").join("\n");
49
49
  }
package/dist/metadata.js CHANGED
@@ -103,7 +103,14 @@ export class MetadataStore {
103
103
  const prev = this.data.sessions[input.id];
104
104
  const createdAt = input.createdAt ?? prev?.createdAt ?? nowIso();
105
105
  const updatedAt = input.updatedAt ?? nowIso();
106
- this.data.sessions[input.id] = { ...prev, ...input, createdAt, updatedAt };
106
+ // Runtime adapters do not all expose the same resume fields. In particular,
107
+ // an adapter can temporarily have no sessionFile while it is being rebuilt.
108
+ // Do not let that partial snapshot erase the durable resume ref (or any
109
+ // other field) already recorded for the session. The old object spread did
110
+ // exactly that: `{ path: undefined }` replaced the Pi transcript path, so
111
+ // reopening the saved row fell through to a fresh session.
112
+ const defined = Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
113
+ this.data.sessions[input.id] = { ...prev, ...defined, createdAt, updatedAt };
107
114
  this.save();
108
115
  }
109
116
  touchSession(id, status) {
@@ -77,3 +77,12 @@ export function authProviderForSession(runtimeId, modelProvider) {
77
77
  return provider;
78
78
  return undefined;
79
79
  }
80
+ /** Convert a provider failure into the safe wire shape consumed by clients.
81
+ * Raw provider text can contain stack frames, paths, or tokens and must never
82
+ * be used as the user-facing auth error. */
83
+ export function classifyModelAuthError(raw, runtimeId, modelProvider) {
84
+ if (!isModelAuthError(raw))
85
+ return null;
86
+ const provider = authProviderForSession(runtimeId, modelProvider);
87
+ return provider ? { kind: "model_auth", provider } : null;
88
+ }
@@ -277,8 +277,8 @@ export function cliAgentManifest() {
277
277
  label: spec.displayName,
278
278
  command: spec.command,
279
279
  hidden: Boolean(spec.hidden),
280
- supportTier: spec.supportTier ?? "beta",
281
- certification: spec.testedVersion ? "release-tested" : (spec.supportTier ?? "beta") === "beta" ? "adapter-tested" : "unverified",
280
+ supportTier: spec.supportTier ?? "supported",
281
+ certification: spec.testedVersion ? "release-tested" : (spec.supportTier ?? "supported") === "supported" || (spec.supportTier ?? "supported") === "beta" ? "adapter-tested" : "unverified",
282
282
  ...(spec.testedVersion ? { testedVersion: spec.testedVersion } : {}),
283
283
  headlessFlags: [...headless].filter((a) => !a.includes("{")),
284
284
  install: spec.install ?? null,
@@ -1015,7 +1015,7 @@ function runtimeCertification(runtime) {
1015
1015
  };
1016
1016
  if (runtime.testedVersion)
1017
1017
  return { certification: "release-tested", testedVersion: runtime.testedVersion };
1018
- return { certification: runtime.supportTier === "beta" ? "adapter-tested" : "unverified" };
1018
+ return { certification: runtime.supportTier === "supported" || runtime.supportTier === "beta" ? "adapter-tested" : "unverified" };
1019
1019
  }
1020
1020
  function runtimeProtection(runtime) {
1021
1021
  // Native SDK/CLI sandboxes receive the requested read-only/workspace/full tier
package/dist/server.js CHANGED
@@ -34,7 +34,7 @@ import { InMemoryLocationRegistry } from "./runtime/location-registry.js";
34
34
  import { ControlPlaneSessionLocationRegistry, LayeredSessionLocationRegistry } from "./runtime/control-plane-location.js";
35
35
  import { attachAdoptedSessions, classifyAttachFailure } from "./runtime/adoption.js";
36
36
  import { createCredentialStore, testProviderCredential } from "./runtime/credentials.js";
37
- import { isModelAuthError, authProviderForSession } from "./runtime/auth-errors.js";
37
+ import { isModelAuthError, authProviderForSession, classifyModelAuthError } from "./runtime/auth-errors.js";
38
38
  import { createCredentialVault, migrateVaultDir } from "./runtime/credential-store.js";
39
39
  import { probeAnthropicAccess } from "./runtime/anthropic-preflight.js";
40
40
  import { createSessionNamer, fallbackSessionName } from "./session/session-namer.js";
@@ -930,6 +930,7 @@ function harnessDirFor(record) {
930
930
  function harnessBeginTurn(record) {
931
931
  const dir = harnessDirFor(record);
932
932
  record.harnessTurnReady = undefined;
933
+ record.harnessTurnFinished = false;
933
934
  if (!dir)
934
935
  return;
935
936
  const previous = record.workspaceState === "dirty" ? "dirty" : "clean";
@@ -954,6 +955,14 @@ function harnessBeginTurn(record) {
954
955
  })();
955
956
  }
956
957
  /** After a turn, snapshot again and broadcast the structured diff it produced. */
958
+ function finishHarnessTurn(record) {
959
+ if (record.harnessTurnFinished)
960
+ return;
961
+ record.harnessTurnFinished = true;
962
+ void harnessEndTurn(record).finally(() => {
963
+ void replication.onTurnComplete(record.id);
964
+ });
965
+ }
957
966
  async function harnessEndTurn(record) {
958
967
  const previous = record.workspaceState === "dirty" ? "dirty" : "clean";
959
968
  try {
@@ -1643,11 +1652,14 @@ function stampSessionEvent(payload) {
1643
1652
  return payload;
1644
1653
  }
1645
1654
  // Collapse the burst of assistant `message_update`s (one per agent stdout line,
1646
- // each carrying the FULL text so far) into ~1 fan-out per tick. See
1647
- // session-event-coalescer.ts for the rationale (kills the O(n^2) re-serialize).
1655
+ // each carrying the FULL text so far) to a human-smooth 4 fps. A 16 ms window
1656
+ // still allowed up to 62 cumulative snapshots/second, making a long answer
1657
+ // quadratic and surprisingly expensive over a phone's relay connection. The
1658
+ // latest snapshot supersedes every skipped one, and non-update events flush it
1659
+ // immediately, so 250 ms changes neither final content nor event ordering.
1648
1660
  // The coalescer's emit stamps the surviving (latest) update, so only ~one seq is
1649
- // spent per tick — the ring holds turn-scale events, not every stdout line.
1650
- const SESSION_UPDATE_COALESCE_MS = 16;
1661
+ // spent per window — the ring holds turn-scale events, not every stdout line.
1662
+ const SESSION_UPDATE_COALESCE_MS = 250;
1651
1663
  const sessionEvents = new SessionEventCoalescer({
1652
1664
  coalesceMs: SESSION_UPDATE_COALESCE_MS,
1653
1665
  emit: (payload) => broadcastCoalesced(stampSessionEvent(payload)),
@@ -2842,7 +2854,7 @@ const RELAY_COMMANDS = {
2842
2854
  // this the relay client (PWA) is stranded on "Working…" forever with
2843
2855
  // only a session.error toast. Clear working so a terminal state reaches it.
2844
2856
  clearSessionWorking(record);
2845
- broadcast({ type: "session.error", sessionId: record.id, error: actionableAgentError(record.runtimeId, error) });
2857
+ broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, error) });
2846
2858
  });
2847
2859
  },
2848
2860
  async "session.new"(msg) {
@@ -4212,7 +4224,7 @@ async function runIssueTaskInner(cfg, issue, source, overrides = {}) {
4212
4224
  try {
4213
4225
  recordRunAuditCorrelation(record, overrides.correlation);
4214
4226
  emit(record, "started", `Started work on ${cfg.owner}/${cfg.repo}#${issue.number}.`);
4215
- await runSessionTurn(record, buildTaskPrompt(issue, nodeGithubIssuePrompt()), overrides.signal);
4227
+ await runSessionTurn(record, buildTaskPrompt(issue, overrides.instructions ?? nodeGithubIssuePrompt()), overrides.signal);
4216
4228
  if (overrides.signal?.aborted)
4217
4229
  throw overrides.signal.reason ?? new Error("Run cancelled");
4218
4230
  emit(record, "agent_done", `Agent finished issue #${issue.number}; running deterministic checks.`);
@@ -4918,6 +4930,7 @@ async function runWorkItem(item, report, signal) {
4918
4930
  sandbox: normalizeSandboxTier(item.sandbox),
4919
4931
  approvalMode: approvalModeFrom(item.approvalMode),
4920
4932
  onEvidence: report,
4933
+ instructions: item.body,
4921
4934
  signal,
4922
4935
  correlation: { runId: item.id, attempt: item.attempt ?? 1, machineId: identity.nodeId },
4923
4936
  });
@@ -4938,7 +4951,7 @@ async function runWorkItem(item, report, signal) {
4938
4951
  throw new Error(`Linear work item has an invalid repo "${repoSlug}"`);
4939
4952
  // Case B: a re-dispatch the control plane correlated to an existing session
4940
4953
  // continues it as a normal chat instead of starting cold (mirrors GitHub).
4941
- if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue), report, { resumeOnMissing: item.targetKind === "existing_session", signal }))
4954
+ if (await continueCorrelatedSession(item, buildLinearTaskPrompt(issue, item.body), report, { resumeOnMissing: item.targetKind === "existing_session", signal }))
4942
4955
  return;
4943
4956
  const githubToken = await resolveGitHubToken();
4944
4957
  if (!githubToken)
@@ -4965,7 +4978,7 @@ async function runWorkItem(item, report, signal) {
4965
4978
  catch { }
4966
4979
  }
4967
4980
  await report({ output: { sessionId: record.id, branch }, events: [{ at: new Date().toISOString(), kind: "branch", summary: "Linear issue working branch and session created.", ref: branch, url: issue.url }] });
4968
- await runSessionTurn(record, buildLinearTaskPrompt(issue), signal);
4981
+ await runSessionTurn(record, buildLinearTaskPrompt(issue, item.body), signal);
4969
4982
  if (signal.aborted)
4970
4983
  throw signal.reason ?? new Error("Run cancelled");
4971
4984
  await branchPublish.maybePushWorktreeBranch(record);
@@ -5958,6 +5971,7 @@ async function sessionListRows() {
5958
5971
  agent: meta?.runtimeId ?? s.agent,
5959
5972
  agentName: meta?.agentName ?? s.agentName,
5960
5973
  source: rec?.source ?? meta?.source,
5974
+ bivyCreated: Boolean(meta),
5961
5975
  forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
5962
5976
  branch: rec?.worktree?.branch ?? meta?.branch,
5963
5977
  sandbox: rec?.sandbox ?? normalizeSandboxTier(meta?.sandbox),
@@ -7174,6 +7188,11 @@ function actionableAgentError(runtimeId, error) {
7174
7188
  }
7175
7189
  return raw;
7176
7190
  }
7191
+ function sessionAgentError(record, error) {
7192
+ const raw = humanizeAgentError(error instanceof Error ? error.message : String(error));
7193
+ return classifyModelAuthError(raw, record.runtimeId, record.session.getCurrentModel()?.provider)
7194
+ ?? actionableAgentError(record.runtimeId, error);
7195
+ }
7177
7196
  /**
7178
7197
  * A turn that ended in a *terminal* model/provider failure the runtime would
7179
7198
  * otherwise swallow. `agent_end` carries the turn's messages and whether the
@@ -7230,7 +7249,7 @@ function attachSessionListeners(record) {
7230
7249
  policy: sessionRunPolicy,
7231
7250
  onNotice: (n) => broadcast({ type: "session.notice", sessionId: record.id, level: n.level, message: n.message }),
7232
7251
  onModelChanged: () => broadcast({ type: "model.updated", sessionId: record.id, model: publicModel(record.session.getCurrentModel(), record.session.getCurrentModel()) }),
7233
- onFailed: (message) => broadcast({ type: "session.error", sessionId: record.id, error: message }),
7252
+ onFailed: (message) => broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, message) }),
7234
7253
  });
7235
7254
  }
7236
7255
  record.unsubscribe = record.session.subscribe((event) => {
@@ -7327,9 +7346,14 @@ function attachSessionListeners(record) {
7327
7346
  transcripts.resolveInlineImages(record);
7328
7347
  }
7329
7348
  // Durably persist the throttled sidecars at the turn boundary so a crash
7330
- // loses at most the in-flight turn's UI detail, not the whole turn.
7331
- if (event.type === "turn_end")
7349
+ // loses at most the in-flight turn's UI detail, not the whole turn. A
7350
+ // runtime may keep its agent process alive and never emit agent_end (Pi),
7351
+ // so turn_end is also authoritative for the interactive working state.
7352
+ if (event.type === "turn_end") {
7332
7353
  eventLog.flush(record.id);
7354
+ clearSessionWorking(record);
7355
+ finishHarnessTurn(record);
7356
+ }
7333
7357
  // AskUserQuestion is intercepted and answered by the daemon's guardian /
7334
7358
  // QuestionManager (see guardianInterceptor), which broadcasts
7335
7359
  // session.question(.resolved) from its own listeners — runtimes no longer
@@ -7364,14 +7388,9 @@ function attachSessionListeners(record) {
7364
7388
  transcripts.clearLiveIntermediate(record.id);
7365
7389
  clearSessionWorking(record);
7366
7390
  void refreshSessionUsage(record);
7367
- // Snapshot the worktree and broadcast the structured diff this turn made —
7368
- // universal edit review + rewind target, for every runtime.
7369
- // Warm-replicate this turn to the standby AFTER the checkpoint is committed
7370
- // (so the shipped frame carries this turn's transcript AND its checkpoint).
7371
- // Gated on session sync — inert by default.
7372
- void harnessEndTurn(record).finally(() => {
7373
- void replication.onTurnComplete(record.id);
7374
- });
7391
+ // Snapshot/diff at whichever completion boundary arrived first. Pi keeps
7392
+ // its process alive after turn_end; other runtimes also emit agent_end.
7393
+ finishHarnessTurn(record);
7375
7394
  // A turn that ended in a terminal model/provider error (e.g. an expired
7376
7395
  // credential or a 4xx from the API) otherwise vanished: working cleared,
7377
7396
  // no reply, no signal. Surface it as a session-scoped error so the client
@@ -7433,7 +7452,7 @@ function attachSessionListeners(record) {
7433
7452
  scheduleAdvertise();
7434
7453
  broadcast({ type: "session.failed", sessionId: record.id, failedAt: record.lastFailureAt });
7435
7454
  if (messageError)
7436
- broadcast({ type: "session.error", sessionId: record.id, error: actionableAgentError(record.runtimeId, messageError) });
7455
+ broadcast({ type: "session.error", sessionId: record.id, error: sessionAgentError(record, messageError) });
7437
7456
  // If the terminal error is an auth failure (expired key/token → 4xx),
7438
7457
  // also raise the sign-in sheet for the failing provider.
7439
7458
  maybeSignalAuthRequired(record, turnError);
@@ -8057,7 +8076,7 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
8057
8076
  void refreshSessionUsage(record);
8058
8077
  if (makeActive)
8059
8078
  active = record;
8060
- broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
8079
+ broadcast({ type: "session.created", sessionId, name: record.session.getName(), workspace: sessionWorkspace, sessionFile: record.sessionFile, source: record.source, bivyCreated: true, branch: worktree?.branch, prUrl: record.prUrl, runtimeId: rt.id, agentName: rt.displayName, modelFallbackMessage, bivySession: bivySessionEnvelope(record), capabilities: capabilitiesWithCommands(rt.id, record.session) });
8061
8080
  void maybeNotifyBivyUpdate();
8062
8081
  scheduleAdvertise();
8063
8082
  return record;
@@ -8074,6 +8093,12 @@ async function createSession(workspace = defaultWorkspace, sessionFile, opts = {
8074
8093
  * of the same session onto one promise fixes both.
8075
8094
  */
8076
8095
  const resumingSessions = new Map();
8096
+ // Keep an id index as well as the resolved runtime ref. A prompt can arrive
8097
+ // while session.open is still resolving and may not carry the session's path;
8098
+ // without this index it cannot discover the in-flight open from metadata and
8099
+ // is incorrectly reported as "Session not found". Both commands must join the
8100
+ // same resume operation by the identity the client actually supplied.
8101
+ const resumingSessionsById = new Map();
8077
8102
  /**
8078
8103
  * Resolve the session a client command targets, resuming it from durable
8079
8104
  * metadata when it isn't currently held in memory. A session survives being
@@ -8096,6 +8121,9 @@ async function resolveOrResumeSession(sessionId, sessionPath) {
8096
8121
  const open = openSessions.get(id);
8097
8122
  if (open)
8098
8123
  return open;
8124
+ const byId = resumingSessionsById.get(id);
8125
+ if (byId)
8126
+ return byId;
8099
8127
  const pathRef = typeof sessionPath === "string" && sessionPath.trim() ? sessionPath.trim() : undefined;
8100
8128
  const meta = metadata.getSession(id) ?? (pathRef ? metadata.getSession(pathRef) : undefined);
8101
8129
  // A run that only kept its terminal log has nothing to resume — it opens as a
@@ -8151,15 +8179,23 @@ async function resolveOrResumeSession(sessionId, sessionPath) {
8151
8179
  }
8152
8180
  }
8153
8181
  const inflight = resumingSessions.get(key);
8154
- if (inflight)
8182
+ if (inflight) {
8183
+ resumingSessionsById.set(id, inflight);
8155
8184
  return inflight;
8185
+ }
8156
8186
  const resume = createSession(defaultWorkspace, ref, { runtimeId, makeActive: false });
8157
8187
  resumingSessions.set(key, resume);
8188
+ resumingSessionsById.set(id, resume);
8158
8189
  try {
8159
8190
  return await resume;
8160
8191
  }
8161
8192
  finally {
8162
8193
  resumingSessions.delete(key);
8194
+ // Only delete our promise. A future implementation may replace an entry
8195
+ // after a failed resume, and must not have its newer attempt removed by an
8196
+ // older finally block.
8197
+ if (resumingSessionsById.get(id) === resume)
8198
+ resumingSessionsById.delete(id);
8163
8199
  }
8164
8200
  }
8165
8201
  // Enumerating a runtime's models needs a live session (the model registry lives
@@ -9835,6 +9871,7 @@ app.get("/api/sessions", async (_req, res, next) => {
9835
9871
  agent: meta?.runtimeId ?? s.agent,
9836
9872
  agentName: meta?.agentName ?? s.agentName,
9837
9873
  source: rec?.source ?? meta?.source,
9874
+ bivyCreated: Boolean(meta),
9838
9875
  forkedFrom: rec?.forkedFrom ?? meta?.forkedFrom,
9839
9876
  branch: rec?.worktree?.branch ?? meta?.branch,
9840
9877
  prUrl: rec?.prUrl ?? meta?.prUrl,
@@ -9856,7 +9893,18 @@ app.get("/api/sessions", async (_req, res, next) => {
9856
9893
  });
9857
9894
  app.post("/api/sessions/open", async (req, res, next) => {
9858
9895
  try {
9859
- const session = await createSession(defaultWorkspace, String(req.body?.path ?? ""), { runtimeId: agentFrom(req.body ?? {}) });
9896
+ // Direct transports use this endpoint for both opening a saved session and
9897
+ // the legacy path-only open flow. The old path-only implementation ignored
9898
+ // sessionId, so a direct client sending into a closed chat opened a brand-new
9899
+ // session instead of reopening the selected conversation.
9900
+ const requestedId = typeof req.body?.sessionId === "string" && req.body.sessionId.trim()
9901
+ ? req.body.sessionId.trim()
9902
+ : undefined;
9903
+ const session = requestedId
9904
+ ? await resolveOrResumeSession(requestedId, req.body?.path)
9905
+ : await createSession(defaultWorkspace, String(req.body?.path ?? ""), { runtimeId: agentFrom(req.body ?? {}) });
9906
+ if (!session)
9907
+ return res.status(404).json({ error: "Session not found" });
9860
9908
  res.json({
9861
9909
  id: session.id,
9862
9910
  workspace: session.workspace,
@@ -104,7 +104,7 @@ export function resolveSessionContract(input) {
104
104
  };
105
105
  const supportTier = input.supportTier ?? "experimental";
106
106
  const hasProtectionDegradation = degradedReasons.some((r) => PROTECTION_AREAS.has(r.area));
107
- const requiresAcknowledgement = supportTier === "supported" && hasProtectionDegradation && !input.acknowledgedAt;
107
+ const requiresAcknowledgement = input.certification === "release-tested" && hasProtectionDegradation && !input.acknowledgedAt;
108
108
  return {
109
109
  schemaVersion: SESSION_CONTRACT_SCHEMA_VERSION,
110
110
  resolvedAt: input.now,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bivy/bivy",
3
- "version": "0.15.0-staging.4",
3
+ "version": "0.15.0-staging.41",
4
4
  "type": "module",
5
5
  "license": "AGPL-3.0-only",
6
6
  "description": "Run coding agents on machines you own. Open-source, self-hostable agent workspace.",
@@ -66,6 +66,6 @@
66
66
  "nanoid": "3.3.18",
67
67
  "undici": "8.10.0"
68
68
  },
69
- "readme": "# Bivy\n\n[![npm](https://img.shields.io/npm/v/@bivy/bivy?color=2b6cb0&label=%40bivy%2Fbivy)](https://www.npmjs.com/package/@bivy/bivy)\n[![license: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-2b6cb0)](LICENSE)\n[![node](https://img.shields.io/badge/node-%E2%89%A520-2b6cb0)](https://nodejs.org)\n\n**Run coding agents on the machines you already own — then reach them from your\nphone, browser, or another terminal.**\n\nStart Claude Code on your workstation, right where the repo, the running dev\nserver, and the staging database already live. Walk away. On the train, open\nyour phone: read what the agent did, answer its question, approve the migration\n— over a link only your devices can decrypt. The work never left your machine.\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # start an agent where your work lives\nbivy open # pick it up from your phone or browser\n```\n\nFirst thing to try: ask the agent to explain the repository, make one small safe\nchange, then open the same Session in the web app or on your phone while it runs.\n\n**[Quickstart](docs/quickstart.md)** ·\n**[Docs](docs/README.md)** ·\n**[Why Bivy](docs/why-bivy.md)** ·\n**[Security model](docs/security-model.md)** ·\n**[bivy.sh](https://bivy.sh)**\n\n> **0.x software.** The core loop is solid and used daily. Interfaces and\n> cross-runtime fidelity still change between releases — check the\n> [runtime support matrix](docs/runtime-support-matrix.md) before you depend on\n> a specific agent capability.\n\n## Why not just a cloud sandbox?\n\nA hosted sandbox starts from an *approximation* of your environment. A Bivy\nMachine **is** your environment — the actual working tree, the services already\nrunning, the caches already warm.\n\n| | Cloud sandbox | Bivy Machine |\n|---|---|---|\n| Your repository | a cloned copy | the real working tree, uncommitted changes and all |\n| Dev server & database | mocked, or absent | already running, right beside the agent |\n| Private networks & internal APIs | out of reach | reachable |\n| Toolchains, package caches | cold, reinstalled each time | warm, already installed |\n| GPUs / local inference | rented separately | the ones on your box |\n| Where your code sits | someone else's infrastructure | the machine you already trust |\n\nYou keep the environment. Bivy adds the part that was missing: **reaching that\nenvironment from anywhere, and leaving it working while you're gone.**\n\n## What you can do\n\nBivy gives you two ways to put an agent to work.\n\n### Sessions — interactive, and portable\n\nStart an agent, watch it work, jump in to steer, stop, or approve. Then leave\nyour desk and keep going:\n\n```bash\nbivy run claude # or codex, pi, gemini, and a dozen more\nbivy open # continue the same session in the browser or PWA\nbivy resume # pick it back up in the terminal\nbivy run claude --no-follow # start it in the background instead of attaching\nbivy run claude --chat # start the governed app session and open it in the browser\n```\n\n- **Reconnect from anywhere** — phone, browser, or another terminal — to the\n same live Session. The PWA adds voice input, read-aloud, phone-to-agent\n file/image uploads, and agent-to-phone attachments.\n- **Move work without starting over.** Import existing Claude Code and Codex\n Sessions, or fork, copy, and move a Bivy Session to another agent, model, or\n Machine.\n- **Run more than one Machine** — a workstation, a private-network server, a GPU\n box — on one account, and pick the environment each Session needs.\n\n### Runs — unattended, and accountable\n\nQueue one on demand, or let an event kick it off — either way it returns\nimmediately and reports back:\n\n```bash\nbivy runs start \"...\" # queue a one-off unattended Run, then `bivy runs wait <id>`\nbivy automation init # or define governed jobs in .bivy/automations.yaml\n```\n\n- **Trigger from real events** — a failed CI job, a GitHub or Linear issue,\n Slack, a schedule, or a signed webhook.\n- **Pin the guardrails** — Machine, agent, model, sandbox, approval mode, and a\n hard attempt ceiling — right next to the job.\n- **Get a Receipt** — every Run reports the checks it ran and how it turned out,\n not just a wall of output.\n\nTry the [capability recipes](docs/capability-recipes.md) to see each of these\nend to end, or the [runtime support matrix](docs/runtime-support-matrix.md) for\nexactly what each agent supports.\n\n## Bring your own stack\n\nUse provider subscriptions through native agent logins, API keys stored in\nBivy's vault, or local / OpenAI-compatible inference. Claude Code, Codex, and Pi\nhave first-class SDK integrations; any other ACP or headless agent needs no\nadapter at all — it's a data row you add with one command:\n\n```bash\nbivy agent add # register an existing ACP or process agent\n```\n\n## Install\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nmacOS and Linux. Requires Node.js 20 or newer. The installer puts the\n[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) npm package and the\n`bivy` command on your `PATH` with optional bridges skipped for speed, then runs the guided `bivy setup` wizard — agent\nchoice, selected-agent install if needed, remote access, and an auto-start background service (launchd on macOS,\nsystemd on Linux). Re-running it on a machine that already has Bivy just applies\nthe latest build and restarts the service.\n\n**What needs an account, and what doesn't.** The CLI alone — `bivy run`,\n`bivy resume`, `bivy sessions` — needs no account and no server; `bivy setup`\nlets you pick **local only for now** and skip remote access. A browser or phone\nUI needs a control plane, because the node hosts none: use the hosted one at\n`app.bivy.sh` (sign in with GitHub or email; free tier plus a paid plan — see\n[bivy.sh#pricing](https://bivy.sh#pricing)) or\n[self-host your own](docs/self-host-quickstart.md). Switch any time with\n`bivy relay:setup`.\n\n**What the installer does with sudo.** It escalates only when it must, and\ntells you when it does:\n\n- Debian/Ubuntu without a suitable Node.js: `sudo apt-get install build-essential\n python3 curl`, then NodeSource's Node 22 setup script via `sudo`.\n- Other Linux, or macOS, without a suitable Node.js: downloads the official\n Node 22 tarball from nodejs.org (sha256-checked) and installs it under\n `/usr/local` with `sudo`.\n- If npm's global prefix isn't writable it falls back to `~/.local` — it never\n runs `npm install` under `sudo`.\n- It appends a marked PATH block to `~/.bashrc` or `~/.zshrc`\n (`BIVY_NO_RC_UPDATE=1` to opt out).\n\nWant no sudo at all? Bring your own Node.js 20+ and skip the script:\n\n```bash\nnpm install -g @bivy/bivy && bivy setup # install globally\nnpx @bivy/bivy setup # or try it once, no install\n```\n\nReleases are published from CI with provenance attestations; verify a build's\norigin with `npm audit signatures`. See [`docs/releasing.md`](docs/releasing.md).\n\n### Your first session\n\nOnce `bivy setup` finishes, use Bivy from inside an existing repo. The first win\nis simple: start the local agent, give it a real task in that environment, then\nreopen the same Session from another surface.\n\n```bash\ncd your-repo\nbivy run claude # start an agent as a durable session in the current repo\n# Try: \"Explain this repo and suggest one small, safe improvement.\"\nbivy open # open that same session in the web app (needs relay setup)\nbivy resume # or pick it back up here in the terminal\n```\n\nFrom here the [quickstart](docs/quickstart.md) walks through Runs, multiple\nMachines, and automations.\n\n### Install options\n\nEnvironment variables passed to the one-line installer change what it does:\n\n| Goal | Variable |\n|---|---|\n| Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |\n| Pin an exact version | `BIVY_VERSION=0.1.0` |\n| Install the npm package into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |\n| Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |\n| Install optional Bivy bridges/native terminal dependency up front | `BIVY_INSTALL_OPTIONAL_DEPS=1` |\n| Don't touch `~/.bashrc` / `~/.zshrc`; print the PATH line instead | `BIVY_NO_RC_UPDATE=1` |\n\nFor example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.\n\nWorking from a checkout of this repository instead:\n\n```bash\npnpm install\npnpm run setup\n```\n\nSee [`docs/install.md`](docs/install.md) for where data lives, service\nmanagement, and uninstall.\n\n## Updating\n\n```bash\nbivy update\n```\n\n`bivy update` detects how Bivy was installed and does the right thing, then\nwaits for any active session to finish its current turn and restarts the\nbackground service so the node reconnects on the new build:\n\n| Install kind | What `bivy update` does |\n|---|---|\n| npm global (`npm i -g`) | `npm install -g @bivy/bivy@<channel>`, then restart the service |\n| installer / packaged | re-runs `install.sh` (migrating to npm if needed), then restart |\n| git checkout | `git pull --ff-only` + `pnpm install --frozen-lockfile`, then restart |\n| `npx` run | nothing to update — each run already fetches the latest |\n\nUpdates follow the release **channel** recorded at install time — `latest`\n(production) by default, or `staging` if you installed with\n`BIVY_CHANNEL=staging`. Switch channels (the choice is remembered for next\ntime), or skip the wait for a busy session:\n\n```bash\nbivy update --staging # move to the dev channel\nbivy update --stable # move back to production (latest)\nbivy update --force # don't wait for an in-flight turn to finish\n```\n\nThe daemon also checks the registry periodically and posts an in-session notice\nwhen a newer build is available.\n\n## Architecture\n\nBivy has three parts. **Only the first one holds your data.**\n\n```text\n your machine hosted or self-hosted\n\n ┌──────────────┐ ┌─────────┐ ┌───────────────┐\n │ node daemon │ ──dials──▶ │ relay │ ◀────▶ │ control plane │\n │ agents, keys │ outbound │ opaque │ │ accounts, web │\n │ repo, tools │ │ frames │ │ app, metadata │\n └──────────────┘ └─────────┘ └───────────────┘\n ▲ ▲\n └────────── end-to-end encrypted session ───────────┘\n phone · browser · another terminal\n```\n\n- **Node** — a daemon on your machine. Owns the workspace, credentials, and agent\n processes. Serves an API and WebSocket on `http://localhost:4317` plus a\n `/healthz` probe. **It hosts no web UI.**\n- **Relay** — forwards encrypted frames between your node and your devices. Your\n node dials out, so no inbound port is opened. The relay cannot read the frames.\n- **Control plane** — holds your account, node registry, and session index, and\n serves the web/PWA client. Use the hosted one or run your own.\n\nBecause the node serves no UI, a browser or phone needs a control plane — hosted\nat `app.bivy.sh`, or one you deploy yourself. The terminal CLI needs neither.\nInteractive Session traffic is end-to-end encrypted between a Machine and its\npaired devices: the relay never sees plaintext and cannot decrypt it. Who can\n*authorize* a device depends on how you pair — with a QR / `bivy link` pairing,\nor on a self-hosted deployment, the control plane can't read your Sessions\neither; with hosted account sign-in you trust the control plane to authorize\ndevices and to serve the web app that holds the keys. See\n[known limitations](docs/security-model.md#known-limitations-for-0x).\n\nSee [`docs/remote-access.md`](docs/remote-access.md) and\n[`docs/security-model.md`](docs/security-model.md).\n\n## Supported agents\n\n**Claude Code and Codex are the recommended, release-certified paths.** The\nbroader catalog stays available under **More agents**; capabilities and fidelity\nvary by runtime.\n\n| Agent | Command | Notes |\n|---|---|---|\n| Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |\n| Codex | `bivy run codex` | Installs `@openai/codex` |\n| Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |\n| OpenCode | `bivy run opencode` | Installs `opencode-ai` |\n| Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |\n| Qwen Code | `bivy run qwen` | Installs `@qwen-code/qwen-code` |\n| Goose | `bivy run goose` | Requires `goose` on PATH |\n| Aider | `bivy run aider` | No session resume (upstream gap) |\n| Cline | `bivy run cline` | Installs `cline` |\n| Crush | `bivy run crush` | No session resume (upstream gap) |\n| Cursor | `bivy run cursor` | ACP-capable |\n| GitHub Copilot | `bivy run copilot` | ACP-capable |\n| Grok | `bivy run grok` | Model selection |\n| Amp | `bivy run amp` | Native thread resume |\n| Auggie | `bivy run auggie` | Headless CLI |\n| Droid | `bivy run droid` | Model selection |\n| Continue | `bivy run continue` | Headless CLI |\n| Kilo Code | `bivy run kilocode` | ACP-capable |\n| Rovo Dev | `bivy run rovodev` | Installed out of band |\n\nAlso defined but hidden from the picker as *Experimental* — runnable via\n`BIVY_RUNTIME=<id>`: Codebuff (`codebuff`, no verified headless mode upstream\nyet), Hermes (`hermes`, generic process adapter), and OpenClaw (`openclaw`,\nCLI adapter only, no resume yet).\n\nAny other command works via `bivy run -- ./your-agent --flags`. ACP-capable\nagents can be promoted to Bivy's governed protocol path for per-tool approvals\nand native resume. To add a reusable process or ACP agent to both the CLI and web\npicker without changing Bivy, run `bivy agent add`, or scaffold and install a\ndeclarative [plugin manifest](docs/plugins.md) with `bivy plugin init`\n(declarative plugins are Experimental, `v1alpha1`, and run out of process).\n\n[`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly\nwhat each agent supports — resume, model selection, approvals, sandboxing.\n\n## Common commands\n\n```bash\nbivy # show the command overview\nbivy run claude # launch Claude Code as a durable session\nbivy run codex # run a different agent\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires relay setup)\nbivy automation init # create .bivy/automations.yaml\nbivy agent add # connect an existing ACP or process agent\nbivy plugin list # installed declarative integration packages\nbivy status # config summary and node reachability\nbivy doctor # health check\nbivy logs -f # tail node logs\nbivy update # update Bivy and restart the service\n```\n\nFull command list, flags, and examples: [`docs/cli-reference.md`](docs/cli-reference.md).\n\n## Configuration\n\nThe common knobs:\n\n```bash\nBIVY_WORKSPACE=/path/to/repo # default workspace\nBIVY_SANDBOX=read-only # read-only | workspace-write (default) | danger-full-access\nBIVY_APPROVAL_MODE=risky # never | risky | always | autonomous (default)\n```\n\nCreate and inspect the typed node configuration, or add repository-owned\nsafety/check/retry policy:\n\n```bash\nbivy config init\nbivy config set defaults.agent codex\nbivy config explain defaults.sandbox\nbivy config init --project # .bivy/policy.yaml\n```\n\nSee [`docs/config-as-code.md`](docs/config-as-code.md). Every environment\nvariable and precedence rule lives in\n[`docs/configuration.md`](docs/configuration.md).\n\n## Approvals and sandboxing\n\nThe default approval mode is **`autonomous`**: agents act without per-action\nprompts. How much that actually protects you depends on the runtime. Native-sandbox\nagents enforce the chosen access tier; structured runtimes also pass tool calls\nthrough Bivy's policy and approval layer. Process agents that Bivy cannot\nintercept run with your OS user permissions — the picker flags this and requires\nconfirmation before you pick that path.\n\nWhere Bivy receives structured shell/file calls, a heuristic floor blocks known\ncatastrophic commands and structured writes outside the workspace, and a\nbackstop set (force-push, publish, deploy, sudo) pauses for a human. This catches\naccidents; **it is not an adversarial isolation boundary.**\n\nWant to be asked about more? Set the mode explicitly:\n\n```bash\nBIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits\nBIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits\nBIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available\n```\n\nApprove from the terminal, browser, or phone.\n\nSandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced\nnatively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.\nAgents without a native sandbox may expose structured tool or MCP controls, but\nthose don't cover activity the agent performs outside those channels; some\nprocess adapters run entirely with your user permissions. Check the picker's\nProtection label. **Bivy does not currently ship its own OS-level jail.**\n\n## Credentials\n\nInteractive prompts, transcripts, and workspace files stay encrypted across the\nrelay. Credentials can remain on a Machine or in a vault you control:\n\n```bash\nbivy secrets list\nbivy secrets set github.repo-token\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\nbivy secrets doctor\n```\n\n`secret://`, `env://`, and `op://` (1Password) references resolve on demand when\nthe daemon provisions an agent run, so raw values never sit in your config.\n\n**One deliberate exception to relay blindness:** if you explicitly enable hosted\nunattended provisioning, Bivy Cloud may store encrypted cloud, repository,\nmodel, or key-escrow material that the service can technically access. Treat this\nas an explicit hosted-custody mode. See the\n[security model](docs/security-model.md#what-the-control-plane-sees) and\n[`docs/key-management.md`](docs/key-management.md).\n\n## Automations as code\n\nDefine governed jobs in `.bivy/automations.yaml`, validate them, and simulate\ntrigger events locally before applying anything:\n\n```bash\nbivy automation init\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml\nbivy automation apply\n```\n\nInstructions are encrypted on the applying node before upload. Safety policy\nlives beside the job — sandbox, approval mode, and a hard attempt ceiling that\nretry/fallback rules cannot exceed. See\n[`docs/automations-as-code.md`](docs/automations-as-code.md).\n\n## GitHub Runs\n\nLabel an issue `bivy` (or `bivy/<machine>` to target a Machine), or mention the\nBivy GitHub App in a comment. Bivy creates a Run on the selected Machine, uses an\nisolated worktree, executes configured checks, and reports an explicit outcome.\n\nCore applies no commercial usage limits. Bivy Cloud billing and commercial\npolicy live in the separate Cloud repository.\n\nA private GitHub App only installs on the account that owns it, so connect one\napp per GitHub account — one for your personal repos, one per organization\n(`bivy github:app-create --org <org>`). A node can serve several at once, each\nwith its own key and `@`-mention handle.\n\nSee [`docs/github-work-queue.md`](docs/github-work-queue.md).\n\n## Linear Runs\n\nApply `bivy` or `bivy/<machine>` to a Linear issue to create a Run on the selected\nMachine. The Machine fetches issue content directly from Linear, works in an\nisolated GitHub worktree, and asks the agent to open a pull request. See\n[`docs/linear-work-queue.md`](docs/linear-work-queue.md).\n\n## Development\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server (proxies /api and /ws to the node)\n```\n\nChecks — all of these run in CI:\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nRepository layout:\n\n- `src/` — node daemon, runtime adapters, approvals, secrets, sessions\n- `bin/` — the `bivy` CLI\n- `packages/core` — shared protocol, pairing, wire format\n- `packages/web` — the React/Vite PWA client (`@bivy/web`)\n- `services/relay` — self-hostable relay\n- `services/control-plane` — self-hostable control plane\n- `deploy/` — self-host deployment examples\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## Self-hosting\n\nNode, relay, and control plane are all in this repository. Point a node at your\nown deployment by passing URLs to `bivy relay:setup` — re-running it switches an\nexisting node over to the new endpoints:\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nEach URL has a flag and an environment-variable equivalent (the flag wins):\n\n| Flag | Environment variable | Points at | Default |\n|---|---|---|---|\n| `--control-plane <url>` | `BIVY_CONTROL_PLANE_URL` | accounts, node registry, and the web-app API | hosted (`app.bivy.sh`) |\n| `--relay <wss-url>` | `BIVY_RELAY_URL` | the encrypted-frame relay your node dials out to | hosted |\n| `--client <url>` | `BIVY_CLIENT_BASE_URL` | base URL used when building app/PWA links | the `--control-plane` URL |\n\nSign-in defaults to GitHub device login (`--github`); pass\n`--email you@example.com` for an email magic-link, or `--session-token <token>`\nto skip interactive sign-in. `relay:setup` checks the control plane is reachable,\nenrolls this node, and writes the endpoints to `.bivy/relay.json`, so `bivy open`,\n`bivy link`, and `bivy update` all keep using your deployment afterwards.\n\n**Self-hosting is community-supported** — no SLA, best-effort help via GitHub\nissues. You own TLS, backups, upgrades, and hardening. Start with the\none-command VPS path in\n[`docs/self-host-quickstart.md`](docs/self-host-quickstart.md); the ops\nreference (backups, rotation, security boundary) is\n[`docs/self-host.md`](docs/self-host.md).\n\n## Security\n\nReport vulnerabilities through [GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new).\nPlease don't open a public issue. See [`SECURITY.md`](SECURITY.md) for scope,\nresponse times, and safe harbour, and [`docs/security-model.md`](docs/security-model.md)\nfor the trust model and known limitations.\n\n## License\n\nBivy Core is free and open-source software under the GNU Affero General Public\nLicense, version 3.0 only (AGPL-3.0-only). You may use, study, modify, and\nself-host it under that license. If you modify Bivy and let users interact with\nit over a network, section 13 requires you to offer them the corresponding\nsource code. See [`LICENSE`](LICENSE).\n\n**Where the open-core line is.** Everything in this repository — node, CLI,\nrelay, control plane, and the web/PWA client — is AGPL Core, with no usage\nlimits. **Bivy Cloud** is the hosted operation of that stack plus billing and\nplans, and lives in a separate private repository. Contributions are accepted\nunder the [DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
69
+ "readme": "# Bivy\n\n[![npm](https://img.shields.io/npm/v/@bivy/bivy?color=2b6cb0&label=%40bivy%2Fbivy)](https://www.npmjs.com/package/@bivy/bivy)\n[![license: AGPL-3.0-only](https://img.shields.io/badge/license-AGPL--3.0--only-2b6cb0)](LICENSE)\n[![node](https://img.shields.io/badge/node-%E2%89%A520-2b6cb0)](https://nodejs.org)\n\n**Run coding agents on the machines you already own — then reach them from your\nphone, browser, or another terminal.**\n\nStart Claude Code on your workstation, right where the repo, the running dev\nserver, and the staging database already live. Walk away. On the train, open\nyour phone: read what the agent did, answer its question, approve the migration\n— over a link only your devices can decrypt. The work never left your machine.\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash # install + guided setup\ncd your-repo\nbivy run claude # start an agent where your work lives\nbivy open # pick it up from your phone or browser\n```\n\nFirst thing to try: ask the agent to explain the repository, make one small safe\nchange, then open the same Session in the web app or on your phone while it runs.\n\n**[Quickstart](docs/quickstart.md)** ·\n**[Docs](docs/README.md)** ·\n**[Why Bivy](docs/why-bivy.md)** ·\n**[Security model](docs/security-model.md)** ·\n**[bivy.sh](https://bivy.sh)**\n\n> **0.x software.** The core loop is solid and used daily. Interfaces and\n> cross-runtime fidelity still change between releases — check the\n> [runtime support matrix](docs/runtime-support-matrix.md) before you depend on\n> a specific agent capability.\n\n## Why not just a cloud sandbox?\n\nA hosted sandbox starts from an *approximation* of your environment. A Bivy\nMachine **is** your environment — the actual working tree, the services already\nrunning, the caches already warm.\n\n| | Cloud sandbox | Bivy Machine |\n|---|---|---|\n| Your repository | a cloned copy | the real working tree, uncommitted changes and all |\n| Dev server & database | mocked, or absent | already running, right beside the agent |\n| Private networks & internal APIs | out of reach | reachable |\n| Toolchains, package caches | cold, reinstalled each time | warm, already installed |\n| GPUs / local inference | rented separately | the ones on your box |\n| Where your code sits | someone else's infrastructure | the machine you already trust |\n\nYou keep the environment. Bivy adds the part that was missing: **reaching that\nenvironment from anywhere, and leaving it working while you're gone.**\n\n## What you can do\n\nBivy gives you two ways to put an agent to work.\n\n### Sessions — interactive, and portable\n\nStart an agent, watch it work, jump in to steer, stop, or approve. Then leave\nyour desk and keep going:\n\n```bash\nbivy run claude # or codex, pi, gemini, and a dozen more\nbivy open # continue the same session in the browser or PWA\nbivy resume # pick it back up in the terminal\nbivy run claude --no-follow # start it in the background instead of attaching\nbivy run claude --chat # start the governed app session and open it in the browser\n```\n\n- **Reconnect from anywhere** — phone, browser, or another terminal — to the\n same live Session. The PWA adds voice input, read-aloud, phone-to-agent\n file/image uploads, and agent-to-phone attachments.\n- **Move work without starting over.** Import existing Claude Code and Codex\n Sessions, or fork, copy, and move a Bivy Session to another agent, model, or\n Machine.\n- **Run more than one Machine** — a workstation, a private-network server, a GPU\n box — on one account, and pick the environment each Session needs.\n\n### Runs — unattended, and accountable\n\nQueue one on demand, or let an event kick it off — either way it returns\nimmediately and reports back:\n\n```bash\nbivy runs start \"...\" # queue a one-off unattended Run, then `bivy runs wait <id>`\nbivy automation init # or define governed jobs in .bivy/automations.yaml\n```\n\n- **Trigger from real events** — a failed CI job, a GitHub or Linear issue,\n Slack, a schedule, or a signed webhook.\n- **Pin the guardrails** — Machine, agent, model, sandbox, approval mode, and a\n hard attempt ceiling — right next to the job.\n- **Get a Receipt** — every Run reports the checks it ran and how it turned out,\n not just a wall of output.\n\nTry the [capability recipes](docs/capability-recipes.md) to see each of these\nend to end, or the [runtime support matrix](docs/runtime-support-matrix.md) for\nexactly what each agent supports.\n\n## Bring your own stack\n\nUse provider subscriptions through native agent logins, API keys stored in\nBivy's vault, or local / OpenAI-compatible inference. Claude Code, Codex, and Pi\nhave first-class SDK integrations; any other ACP or headless agent needs no\nadapter at all — it's a data row you add with one command:\n\n```bash\nbivy agent add # register an existing ACP or process agent\n```\n\n## Install\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh | bash\n```\n\nmacOS and Linux. Requires Node.js 20 or newer. The installer puts the\n[`@bivy/bivy`](https://www.npmjs.com/package/@bivy/bivy) npm package and the\n`bivy` command on your `PATH` with optional bridges skipped for speed, then runs\n`bivy setup` — agent choice, selected-agent install if needed, remote access,\nand an auto-start background service (launchd on macOS, systemd on Linux).\n\nIf Claude Code, Codex, OpenCode, Gemini, or another agent is already installed,\nsetup says so and uses that existing CLI, login, and configuration. Bivy does\nnot replace the agent or copy its native credentials; it adds remote continuity\nand governance around the agent you already use. Re-running the installer on a\nmachine that already has Bivy just applies the latest build and restarts the\nservice.\n\n**What needs an account, and what doesn't.** The CLI alone — `bivy run`,\n`bivy resume`, `bivy sessions` — needs no account and no server; `bivy setup`\nlets you pick **local only for now** and skip remote access. A browser or phone\nUI needs a control plane, because the node hosts none: use the hosted one at\n`app.bivy.sh` (sign in with GitHub or email; free tier plus a paid plan — see\n[bivy.sh#pricing](https://bivy.sh#pricing)) or\n[self-host your own](docs/self-host-quickstart.md). Switch any time with\n`bivy relay:setup`.\n\nPrefer to inspect the installer first?\n\n```bash\ncurl -fsSL https://bivy.sh/install.sh -o install.sh\nless install.sh\nbash install.sh\n```\n\n**What the installer does with sudo.** It escalates only when it must, and\ntells you when it does:\n\n- Debian/Ubuntu without a suitable Node.js: `sudo apt-get install curl\n ca-certificates`, then NodeSource's Node 22 setup script via `sudo`.\n- Other Linux, or macOS, without a suitable Node.js: downloads the official\n Node 22 tarball from nodejs.org (sha256-checked) and installs it under\n `/usr/local` with `sudo`.\n- If npm's global prefix isn't writable it falls back to `~/.local` — it never\n runs `npm install` under `sudo`.\n- It appends a marked PATH block to `~/.bashrc` or `~/.zshrc`\n (`BIVY_NO_RC_UPDATE=1` to opt out).\n\nWant no sudo at all? Bring your own Node.js 20+ and skip the script:\n\n```bash\nnpm install -g @bivy/bivy && bivy setup # install globally\nnpx @bivy/bivy setup # or try it once, no install\n```\n\nReleases are published from CI with provenance attestations; verify a build's\norigin with `npm audit signatures`. See [`docs/releasing.md`](docs/releasing.md).\n\n### Your first session\n\nOnce `bivy setup` finishes, use Bivy from inside an existing repo. The first win\nis simple: start the local agent, give it a real task in that environment, then\nreopen the same Session from another surface.\n\n```bash\ncd your-repo\nbivy run claude # start an agent as a durable session in the current repo\n# Try: \"Explain this repo and suggest one small, safe improvement.\"\nbivy open # open that same session in the web app (needs relay setup)\nbivy resume # or pick it back up here in the terminal\n```\n\nFrom here the [quickstart](docs/quickstart.md) walks through Runs, multiple\nMachines, and automations.\n\n### Install options\n\nEnvironment variables passed to the one-line installer change what it does:\n\n| Goal | Variable |\n|---|---|\n| Track the dev channel (new build on every merge to `main`) | `BIVY_CHANNEL=staging` |\n| Pin an exact version | `BIVY_VERSION=0.1.0` |\n| Install the npm package into a user-owned prefix | `BIVY_NPM_PREFIX=~/.local` |\n| Preinstall every known upstream agent | `BIVY_INSTALL_ALL_AGENTS=1` |\n| Install optional Bivy bridges/native terminal dependency up front | `BIVY_INSTALL_OPTIONAL_DEPS=1` |\n| Don't touch `~/.bashrc` / `~/.zshrc`; print the PATH line instead | `BIVY_NO_RC_UPDATE=1` |\n\nFor example: `BIVY_CHANNEL=staging curl -fsSL https://bivy.sh/install.sh | bash`.\n\nWorking from a checkout of this repository instead:\n\n```bash\npnpm install\npnpm run setup\n```\n\nSee [`docs/install.md`](docs/install.md) for where data lives, service\nmanagement, and uninstall.\n\n## Updating\n\n```bash\nbivy update\n```\n\n`bivy update` detects how Bivy was installed and does the right thing, then\nwaits for any active session to finish its current turn and restarts the\nbackground service so the node reconnects on the new build:\n\n| Install kind | What `bivy update` does |\n|---|---|\n| npm global (`npm i -g`) | `npm install -g @bivy/bivy@<channel>`, then restart the service |\n| installer / packaged | re-runs `install.sh` (migrating to npm if needed), then restart |\n| git checkout | `git pull --ff-only` + `pnpm install --frozen-lockfile`, then restart |\n| `npx` run | nothing to update — each run already fetches the latest |\n\nUpdates follow the release **channel** recorded at install time — `latest`\n(production) by default, or `staging` if you installed with\n`BIVY_CHANNEL=staging`. Switch channels (the choice is remembered for next\ntime), or skip the wait for a busy session:\n\n```bash\nbivy update --staging # move to the dev channel\nbivy update --stable # move back to production (latest)\nbivy update --force # don't wait for an in-flight turn to finish\n```\n\nThe daemon also checks the registry periodically and posts an in-session notice\nwhen a newer build is available.\n\n## Architecture\n\nBivy has three parts. **Only the first one holds your data.**\n\n```text\n your machine hosted or self-hosted\n\n ┌──────────────┐ ┌─────────┐ ┌───────────────┐\n │ node daemon │ ──dials──▶ │ relay │ ◀────▶ │ control plane │\n │ agents, keys │ outbound │ opaque │ │ accounts, web │\n │ repo, tools │ │ frames │ │ app, metadata │\n └──────────────┘ └─────────┘ └───────────────┘\n ▲ ▲\n └────────── end-to-end encrypted session ───────────┘\n phone · browser · another terminal\n```\n\n- **Node** — a daemon on your machine. Owns the workspace, credentials, and agent\n processes. Serves an API and WebSocket on `http://localhost:4317` plus a\n `/healthz` probe. **It hosts no web UI.**\n- **Relay** — forwards encrypted frames between your node and your devices. Your\n node dials out, so no inbound port is opened. The relay cannot read the frames.\n- **Control plane** — holds your account, node registry, and session index, and\n serves the web/PWA client. Use the hosted one or run your own.\n\nBecause the node serves no UI, a browser or phone needs a control plane — hosted\nat `app.bivy.sh`, or one you deploy yourself. The terminal CLI needs neither.\nInteractive Session traffic is end-to-end encrypted between a Machine and its\npaired devices: the relay never sees plaintext and cannot decrypt it. Who can\n*authorize* a device depends on how you pair — with a QR / `bivy link` pairing,\nor on a self-hosted deployment, the control plane can't read your Sessions\neither; with hosted account sign-in you trust the control plane to authorize\ndevices and to serve the web app that holds the keys. See\n[known limitations](docs/security-model.md#known-limitations-for-0x).\n\nSee [`docs/remote-access.md`](docs/remote-access.md) and\n[`docs/security-model.md`](docs/security-model.md).\n\n## Supported agents\n\n**Bivy maintains wrappers for the popular coding agents below.** Claude Code,\nCodex, Pi, and OpenCode have the richest release-tested capability sets today;\ncapabilities and fidelity still vary by runtime.\n\n| Agent | Command | Notes |\n|---|---|---|\n| Claude Code | `bivy run claude` | Uses the operator-installed `claude` command through an SDK bridge |\n| Codex | `bivy run codex` | Installs `@openai/codex` |\n| Pi | `bivy run pi` | Uses the operator-installed `pi` command and Pi auth/config |\n| OpenCode | `bivy run opencode` | Installs `opencode-ai` |\n| Gemini CLI | `bivy run gemini` | Installs `@google/gemini-cli` |\n| Qwen Code | `bivy run qwen` | Installs `@qwen-code/qwen-code` |\n| Goose | `bivy run goose` | Requires `goose` on PATH |\n| Aider | `bivy run aider` | No session resume (upstream gap) |\n| Cline | `bivy run cline` | Installs `cline` |\n| Crush | `bivy run crush` | No session resume (upstream gap) |\n| Cursor | `bivy run cursor` | ACP-capable |\n| GitHub Copilot | `bivy run copilot` | ACP-capable |\n| Grok | `bivy run grok` | Model selection |\n| Amp | `bivy run amp` | Native thread resume |\n| Auggie | `bivy run auggie` | Headless CLI |\n| Droid | `bivy run droid` | Model selection |\n| Continue | `bivy run continue` | Headless CLI |\n| Kilo Code | `bivy run kilocode` | ACP-capable |\n| Rovo Dev | `bivy run rovodev` | Installed out of band |\n\nAlso defined but hidden from the picker as *Experimental* — runnable via\n`BIVY_RUNTIME=<id>`: Codebuff (`codebuff`, no verified headless mode upstream\nyet), Hermes (`hermes`, generic process adapter), and OpenClaw (`openclaw`,\nCLI adapter only, no resume yet).\n\nAny other command works via `bivy run -- ./your-agent --flags`. ACP-capable\nagents can be promoted to Bivy's governed protocol path for per-tool approvals\nand native resume. To add a reusable process or ACP agent to both the CLI and web\npicker without changing Bivy, run `bivy agent add`, or scaffold and install a\ndeclarative [plugin manifest](docs/plugins.md) with `bivy plugin init`\n(declarative plugins are Experimental, `v1alpha1`, and run out of process).\n\n[`docs/runtime-support-matrix.md`](docs/runtime-support-matrix.md) lists exactly\nwhat each agent supports — resume, model selection, approvals, sandboxing.\n\n## Common commands\n\n```bash\nbivy # show the command overview\nbivy run claude # launch Claude Code as a durable session\nbivy run codex # run a different agent\nbivy sessions # list live and saved sessions\nbivy resume # resume the most recent session\nbivy open # open the web app (requires relay setup)\nbivy automation init # create .bivy/automations.yaml\nbivy agent add # connect an existing ACP or process agent\nbivy plugin list # installed declarative integration packages\nbivy status # config summary and node reachability\nbivy doctor # health check\nbivy logs -f # tail node logs\nbivy update # update Bivy and restart the service\n```\n\nFull command list, flags, and examples: [`docs/cli-reference.md`](docs/cli-reference.md).\n\n## Configuration\n\nThe common knobs:\n\n```bash\nBIVY_WORKSPACE=/path/to/repo # default workspace\nBIVY_SANDBOX=read-only # read-only | workspace-write (default) | danger-full-access\nBIVY_APPROVAL_MODE=risky # never | risky | always | autonomous (default)\n```\n\nCreate and inspect the typed node configuration, or add repository-owned\nsafety/check/retry policy:\n\n```bash\nbivy config init\nbivy config set defaults.agent codex\nbivy config explain defaults.sandbox\nbivy config init --project # .bivy/policy.yaml\n```\n\nSee [`docs/config-as-code.md`](docs/config-as-code.md). Every environment\nvariable and precedence rule lives in\n[`docs/configuration.md`](docs/configuration.md).\n\n## Approvals and sandboxing\n\nThe default approval mode is **`autonomous`**: agents act without per-action\nprompts. How much that actually protects you depends on the runtime. Native-sandbox\nagents enforce the chosen access tier; structured runtimes also pass tool calls\nthrough Bivy's policy and approval layer. Process agents that Bivy cannot\nintercept run with your OS user permissions — the picker flags this and requires\nconfirmation before you pick that path.\n\nWhere Bivy receives structured shell/file calls, a heuristic floor blocks known\ncatastrophic commands and structured writes outside the workspace, and a\nbackstop set (force-push, publish, deploy, sudo) pauses for a human. This catches\naccidents; **it is not an adversarial isolation boundary.**\n\nWant to be asked about more? Set the mode explicitly:\n\n```bash\nBIVY_APPROVAL_MODE=risky # prompt on risky shell commands and file edits\nBIVY_APPROVAL_MODE=always # prompt on all shell commands and file edits\nBIVY_APPROVAL_MODE=never # no prompts; structured-tool heuristic blocks still apply where available\n```\n\nApprove from the terminal, browser, or phone.\n\nSandbox tiers (`read-only`, `workspace-write`, `danger-full-access`) are enforced\nnatively by agents that support them — Codex, Claude Code, Gemini CLI, Qwen Code.\nAgents without a native sandbox may expose structured tool or MCP controls, but\nthose don't cover activity the agent performs outside those channels; some\nprocess adapters run entirely with your user permissions. Check the picker's\nProtection label. **Bivy does not currently ship its own OS-level jail.**\n\n## Credentials\n\nInteractive prompts, transcripts, and workspace files stay encrypted across the\nrelay. Credentials can remain on a Machine or in a vault you control:\n\n```bash\nbivy secrets list\nbivy secrets set github.repo-token\nbivy secrets ref github.repo-token op://Bivy/GitHub/repo-token\nbivy secrets doctor\n```\n\n`secret://`, `env://`, and `op://` (1Password) references resolve on demand when\nthe daemon provisions an agent run, so raw values never sit in your config.\n\n**One deliberate exception to relay blindness:** if you explicitly enable hosted\nunattended provisioning, Bivy Cloud may store encrypted cloud, repository,\nmodel, or key-escrow material that the service can technically access. Treat this\nas an explicit hosted-custody mode. See the\n[security model](docs/security-model.md#what-the-control-plane-sees) and\n[`docs/key-management.md`](docs/key-management.md).\n\n## Automations as code\n\nDefine governed jobs in `.bivy/automations.yaml`, validate them, and simulate\ntrigger events locally before applying anything:\n\n```bash\nbivy automation init\nbivy automation validate\nbivy automation test --event .bivy/events/failed-ci.yaml\nbivy automation apply\n```\n\nInstructions are encrypted on the applying node before upload. Safety policy\nlives beside the job — sandbox, approval mode, and a hard attempt ceiling that\nretry/fallback rules cannot exceed. See\n[`docs/automations-as-code.md`](docs/automations-as-code.md).\n\n## GitHub Runs\n\nLabel an issue `bivy` (or `bivy/<machine>` to target a Machine), or mention the\nBivy GitHub App in a comment. Bivy creates a Run on the selected Machine, uses an\nisolated worktree, executes configured checks, and reports an explicit outcome.\n\nCore applies no commercial usage limits. Bivy Cloud billing and commercial\npolicy live in the separate Cloud repository.\n\nA private GitHub App only installs on the account that owns it, so connect one\napp per GitHub account — one for your personal repos, one per organization\n(`bivy github:app-create --org <org>`). A node can serve several at once, each\nwith its own key and `@`-mention handle.\n\nSee [`docs/github-work-queue.md`](docs/github-work-queue.md).\n\n## Linear Runs\n\nApply `bivy` or `bivy/<machine>` to a Linear issue to create a Run on the selected\nMachine. The Machine fetches issue content directly from Linear, works in an\nisolated GitHub worktree, and asks the agent to open a pull request. See\n[`docs/linear-work-queue.md`](docs/linear-work-queue.md).\n\n## Development\n\n```bash\npnpm install\npnpm run dev # node daemon on http://localhost:4317\npnpm run dev:web # web client dev server (proxies /api and /ws to the node)\n```\n\nChecks — all of these run in CI:\n\n```bash\npnpm run typecheck\npnpm run typecheck:web\npnpm run lint\npnpm run test:unit\npnpm run test:core\npnpm run check:licenses\npnpm run check:secrets\n```\n\nRepository layout:\n\n- `src/` — node daemon, runtime adapters, approvals, secrets, sessions\n- `bin/` — the `bivy` CLI\n- `packages/core` — shared protocol, pairing, wire format\n- `packages/web` — the React/Vite PWA client (`@bivy/web`)\n- `services/relay` — self-hostable relay\n- `services/control-plane` — self-hostable control plane\n- `deploy/` — self-host deployment examples\n\nSee [`CONTRIBUTING.md`](CONTRIBUTING.md).\n\n## Self-hosting\n\nNode, relay, and control plane are all in this repository. Point a node at your\nown deployment by passing URLs to `bivy relay:setup` — re-running it switches an\nexisting node over to the new endpoints:\n\n```bash\nbivy relay:setup \\\n --control-plane https://bivy.example.com \\\n --relay wss://relay.example.com\n```\n\nEach URL has a flag and an environment-variable equivalent (the flag wins):\n\n| Flag | Environment variable | Points at | Default |\n|---|---|---|---|\n| `--control-plane <url>` | `BIVY_CONTROL_PLANE_URL` | accounts, node registry, and the web-app API | hosted (`app.bivy.sh`) |\n| `--relay <wss-url>` | `BIVY_RELAY_URL` | the encrypted-frame relay your node dials out to | hosted |\n| `--client <url>` | `BIVY_CLIENT_BASE_URL` | base URL used when building app/PWA links | the `--control-plane` URL |\n\nSign-in defaults to GitHub device login (`--github`); pass\n`--email you@example.com` for an email magic-link, or `--session-token <token>`\nto skip interactive sign-in. `relay:setup` checks the control plane is reachable,\nenrolls this node, and writes the endpoints to `.bivy/relay.json`, so `bivy open`,\n`bivy link`, and `bivy update` all keep using your deployment afterwards.\n\n**Self-hosting is community-supported** — no SLA, best-effort help via GitHub\nissues. You own TLS, backups, upgrades, and hardening. Start with the\none-command VPS path in\n[`docs/self-host-quickstart.md`](docs/self-host-quickstart.md); the ops\nreference (backups, rotation, security boundary) is\n[`docs/self-host.md`](docs/self-host.md).\n\n## Security\n\nReport vulnerabilities through [GitHub private vulnerability reporting](https://github.com/bivysh/bivy/security/advisories/new).\nPlease don't open a public issue. See [`SECURITY.md`](SECURITY.md) for scope,\nresponse times, and safe harbour, and [`docs/security-model.md`](docs/security-model.md)\nfor the trust model and known limitations.\n\n## License\n\nBivy Core is free and open-source software under the GNU Affero General Public\nLicense, version 3.0 only (AGPL-3.0-only). You may use, study, modify, and\nself-host it under that license. If you modify Bivy and let users interact with\nit over a network, section 13 requires you to offer them the corresponding\nsource code. See [`LICENSE`](LICENSE).\n\n**Where the open-core line is.** Everything in this repository — node, CLI,\nrelay, control plane, and the web/PWA client — is AGPL Core, with no usage\nlimits. **Bivy Cloud** is the hosted operation of that stack plus billing and\nplans, and lives in a separate private repository. Contributions are accepted\nunder the [DCO](CONTRIBUTING.md#certificate-of-origin); there is no CLA.\n",
70
70
  "readmeFilename": "README.md"
71
71
  }