@bivy/bivy 0.18.11-staging.16 → 0.18.11-staging.2

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.
Files changed (36) hide show
  1. package/bin/acp-shim.mjs +16 -142
  2. package/bin/agent-manifest.json +2 -2
  3. package/bin/bivy.mjs +27 -189
  4. package/bin/prune-sessions.mjs +0 -38
  5. package/dist/agent-instructions.js +0 -4
  6. package/dist/agents/claude-code/integration.js +1 -1
  7. package/dist/agents/claude-code/runtime.js +0 -15
  8. package/dist/agents/codex/integration.js +1 -1
  9. package/dist/agents/profiles.js +3 -3
  10. package/dist/apps/gateway.js +17 -53
  11. package/dist/apps/preview-shell.js +0 -2
  12. package/dist/apps/remote-preview.js +2 -4
  13. package/dist/apps/service.js +3 -18
  14. package/dist/apps/styles.css +1 -29
  15. package/dist/apps/types.js +0 -8
  16. package/dist/certification/generated.js +4 -4
  17. package/dist/control-plane-tasks.js +1 -6
  18. package/dist/controllers/app-commands.js +3 -7
  19. package/dist/controllers/fork-commands.js +20 -20
  20. package/dist/exec.js +4 -41
  21. package/dist/protocol/client-command-routes.js +1 -1
  22. package/dist/protocol/client-command-schemas.js +1 -4
  23. package/dist/run-tools.js +2 -15
  24. package/dist/runtime/protocol.js +4 -60
  25. package/dist/runtime/tool-call-map.js +0 -24
  26. package/dist/server.js +25 -289
  27. package/dist/session/event-log.js +3 -61
  28. package/dist/session/fork-standup.js +0 -5
  29. package/dist/session/fork.js +1 -5
  30. package/dist/session/sibling-client.js +3 -40
  31. package/dist/session/transcript-merge.js +1 -29
  32. package/dist/session/transcript-persistence.js +0 -4
  33. package/package.json +2 -2
  34. package/dist/runtime/model-ref.js +0 -33
  35. package/dist/session/delegation-cards.js +0 -28
  36. package/dist/session/delegations.js +0 -21
package/bin/acp-shim.mjs CHANGED
@@ -136,31 +136,9 @@ let initialized = false;
136
136
  let modelConfigId = "model";
137
137
  // A model chosen before the ACP session existed, applied once it does.
138
138
  let pendingModel = null;
139
- // True while session/load replays history (see session.resume).
140
- let replayingHistory = false;
141
- // The config-option id carrying reasoning effort (ACP category "thought_level"),
142
- // when the agent advertises one.
143
- let thoughtConfigId = null;
144
139
  // toolCallId -> { requestId, options } so a later bivy tool.decision answers the
145
140
  // right ACP permission request with a concrete optionId.
146
141
  const permissionRequests = new Map();
147
- // Calls the human (or policy) already approved. An agent that performs an
148
- // approved edit through fs/write_text_file (Grok does) must not be gated — and
149
- // carded — a second time for the same file.
150
- const approvedCalls = new Set();
151
-
152
- /** The approved, still-running call whose inputs name `file`, if any. */
153
- function approvedCallWriting(file) {
154
- for (const id of approvedCalls) {
155
- const state = toolCallState.get(id);
156
- if (!state) { approvedCalls.delete(id); continue; }
157
- const input = toolCallInput(state);
158
- const paths = [input.path, input.file_path, input.filePath, ...(state.locations || []).map((l) => l?.path)].filter(Boolean);
159
- const root = fs.realpathSync(cwd);
160
- if (paths.some((p) => path.resolve(root, String(p)) === file || path.resolve(cwd, String(p)) === file)) return id;
161
- }
162
- return undefined;
163
- }
164
142
  const sandboxTier = process.env.BIVY_ACP_SANDBOX || "workspace-write";
165
143
 
166
144
  function isWithin(root, candidate) {
@@ -212,9 +190,6 @@ function finishTurnDone() {
212
190
  turnDrainTimer = null;
213
191
  if (!turnDraining) return;
214
192
  turnDraining = false;
215
- ownCalls.clear();
216
- childParent.clear();
217
- approvedCalls.clear();
218
193
  bivy({ type: "session.status", status: "idle" });
219
194
  bivy({ type: "session.done" });
220
195
  }
@@ -247,11 +222,6 @@ function mergeToolCallState(toolCallId, u) {
247
222
  const next = {
248
223
  kind: u.kind ?? prev.kind,
249
224
  title: u.title ?? prev.title,
250
- // Agents open a call titled with the tool's own name ("task",
251
- // "spawn_subagent", "bash") and later retitle it with prose ("Count README
252
- // lines"). Keep the first identifier-like title as the stable name, so a
253
- // delegation still classifies as one after the retitle.
254
- name: prev.name ?? (typeof u.title === "string" && /^[A-Za-z_][\w.:-]*$/.test(u.title) ? u.title : undefined),
255
225
  rawInput: u.rawInput && typeof u.rawInput === "object" && Object.keys(u.rawInput).length ? u.rawInput : prev.rawInput,
256
226
  locations: Array.isArray(u.locations) && u.locations.length ? u.locations : prev.locations,
257
227
  content: content && content.length ? content : prev.content,
@@ -289,60 +259,7 @@ function toolCallInput(state) {
289
259
  * both the node's bucket classifier and the client's own name-based heuristic,
290
260
  * which both expect short tool-name-like tokens, not prose. */
291
261
  function toolCallName(state) {
292
- return (state.kind && KIND_TOOL_NAME[state.kind]) || state.name || state.title || state.kind || "tool";
293
- }
294
-
295
- // --- sub-agent sessions ------------------------------------------------------
296
- // Agents that run sub-agents as ACP sessions of their own (Grok's
297
- // spawn_subagent) stream the child's updates over this same connection, tagged
298
- // with the CHILD's sessionId. Folding those into the parent turn glued the
299
- // sub-agent's prose onto the parent's answer and listed its tools as the
300
- // parent's. Route them instead: child tool calls carry `parentToolCallId` (the
301
- // delegating call, so the transcript nests them) and child prose stays out of
302
- // the parent reply — the delegation's own tool result carries its answer.
303
- const childParent = new Map(); // child sessionId -> delegating toolCallId ("" if unknown)
304
- // This turn's own (non-child) calls, kept after they complete: a background
305
- // sub-agent's spawn call often finishes before the child's first update lands.
306
- const ownCalls = new Map(); // toolCallId -> accumulated state
307
- const DELEGATION_NAME = /agent|task|delegat/i;
308
-
309
- function isDelegation(state) {
310
- const input = state?.rawInput || {};
311
- return DELEGATION_NAME.test(String(state?.name || "")) || typeof input.subagent_type === "string" || typeof input.prompt === "string";
312
- }
313
-
314
- /** The delegating call for a child session: an explicit link when the agent
315
- * sent one (see linkChildSession), else the newest delegation-shaped call of
316
- * this turn that no other child has claimed, else the newest open call. */
317
- function parentCallFor(childSessionId) {
318
- if (childParent.has(childSessionId)) return childParent.get(childSessionId);
319
- const claimed = new Set(childParent.values());
320
- const calls = [...ownCalls].reverse().filter(([id]) => !claimed.has(id));
321
- const pick = calls.find(([, st]) => isDelegation(st)) ?? calls.find(([id]) => toolCallState.has(id));
322
- const parent = pick?.[0] ?? "";
323
- childParent.set(childSessionId, parent);
324
- return parent;
325
- }
326
-
327
- /** An agent-specific notification that names a child session and its parent
328
- * (`child_session_id` / `parent_session_id`, plus the tool call id or the
329
- * delegation's description) pins the link instead of the heuristic above. */
330
- function linkChildSession(update) {
331
- const child = update?.child_session_id ?? update?.childSessionId;
332
- const parentSession = update?.parent_session_id ?? update?.parentSessionId;
333
- if (!child || (parentSession && parentSession !== sessionId) || childParent.get(child)) return;
334
- const byId = update.tool_call_id ?? update.toolCallId;
335
- const match = byId && ownCalls.has(byId)
336
- ? byId
337
- : [...ownCalls].reverse().find(([, st]) => update.description && (st.title === update.description || st.rawInput?.description === update.description))?.[0];
338
- if (match) childParent.set(child, match);
339
- }
340
-
341
- /** Routing for an update from `sid`: null for this session, else the parent
342
- * call id ("" when it can't be placed). */
343
- function childRoute(sid) {
344
- if (!sid || !sessionId || sid === sessionId) return null;
345
- return parentCallFor(sid);
262
+ return (state.kind && KIND_TOOL_NAME[state.kind]) || state.title || state.kind || "tool";
346
263
  }
347
264
 
348
265
  async function ensureInitialized() {
@@ -374,33 +291,24 @@ function publishModels(result) {
374
291
  // ACP model ids are `provider/model`; split the provider so Bivy can group and
375
292
  // scope provider-specific settings the same way it does for other runtimes.
376
293
  .map((m) => ({ ...m, provider: m.id.includes("/") ? m.id.split("/")[0] : "agent" }));
377
- // Reasoning effort rides the same config-option surface under ACP's
378
- // `thought_level` category (Grok: reasoning_effort xhigh/high/medium/low).
379
- const thoughtOption = options.find((o) => String(o?.category ?? "") === "thought_level");
380
- const levels = (Array.isArray(thoughtOption?.options) ? thoughtOption.options : []).map((o) => String(o?.value ?? "")).filter(Boolean);
381
- if (!models.length && !levels.length) return;
382
- if (models.length) modelConfigId = String(modelOption?.id ?? "model");
383
- if (levels.length) thoughtConfigId = String(thoughtOption.id);
294
+ if (!models.length) return;
295
+ modelConfigId = String(modelOption?.id ?? "model");
384
296
  bivy({
385
297
  type: "runtime.models",
386
298
  models,
387
299
  ...(modelOption?.currentValue ? { currentModel: String(modelOption.currentValue) } : {}),
388
- ...(levels.length ? { thinking: { levels, ...(thoughtOption?.currentValue ? { current: String(thoughtOption.currentValue) } : {}) } } : {}),
389
300
  });
390
301
  }
391
302
 
392
303
  // --- ACP → bivy: streamed session/update notifications ----------------------
393
304
  function onSessionUpdate(params) {
394
305
  const u = params?.update;
395
- if (!u || typeof u !== "object" || replayingHistory) return;
306
+ if (!u || typeof u !== "object") return;
396
307
  // Any update arriving while the turn is draining means the agent is still
397
308
  // emitting the tail of this turn (see the drain note above) — reset the quiet
398
309
  // window so session.done waits for it instead of sealing history short.
399
310
  if (turnDraining) scheduleTurnDone();
400
311
  const kind = String(u.sessionUpdate || "");
401
- const parentCall = childRoute(params?.sessionId);
402
- const child = parentCall !== null;
403
- const nest = parentCall ? { parentToolCallId: parentCall } : {};
404
312
  const textOf = (content) => {
405
313
  if (!content) return "";
406
314
  if (typeof content === "string") return content;
@@ -419,12 +327,12 @@ function onSessionUpdate(params) {
419
327
  switch (kind) {
420
328
  case "agent_message_chunk": {
421
329
  const t = textOf(u.content);
422
- if (t && !child) bivy({ type: "message.delta", text: t });
330
+ if (t) bivy({ type: "message.delta", text: t });
423
331
  break;
424
332
  }
425
333
  case "agent_thought_chunk": {
426
334
  const t = textOf(u.content);
427
- if (t && !child) bivy({ type: "message.reasoning", text: t });
335
+ if (t) bivy({ type: "message.reasoning", text: t });
428
336
  break;
429
337
  }
430
338
  case "tool_call": {
@@ -432,14 +340,12 @@ function onSessionUpdate(params) {
432
340
  // shows the action; the result arrives via tool_call_update.
433
341
  const toolCallId = String(u.toolCallId ?? u.id ?? "");
434
342
  const state = mergeToolCallState(toolCallId, u);
435
- if (!child) ownCalls.set(toolCallId, state);
436
- bivy({ type: "tool.observe", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...nest });
343
+ bivy({ type: "tool.observe", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
437
344
  break;
438
345
  }
439
346
  case "tool_call_update": {
440
347
  const toolCallId = String(u.toolCallId ?? u.id ?? "");
441
348
  const state = mergeToolCallState(toolCallId, u);
442
- if (!child && ownCalls.has(toolCallId)) ownCalls.set(toolCallId, state);
443
349
  const status = String(u.status || "");
444
350
  if (status === "completed" || status === "failed") {
445
351
  toolCallState.delete(toolCallId);
@@ -456,18 +362,17 @@ function onSessionUpdate(params) {
456
362
  // the terminal frame's content when it has some, else the accumulated.
457
363
  result: textOf(u.content) || textOf(state.content) || status,
458
364
  isError: status === "failed",
459
- ...nest,
460
365
  });
461
366
  } else {
462
367
  // Still running: forward the fuller name/input as it fills in so a live
463
368
  // tool card isn't stuck with the sparse initial notification.
464
- bivy({ type: "tool.update", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...nest });
369
+ bivy({ type: "tool.update", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
465
370
  }
466
371
  break;
467
372
  }
468
373
  case "plan":
469
374
  // Optional planning stream — fold into reasoning so nothing is lost.
470
- if (!child && Array.isArray(u.entries)) bivy({ type: "message.reasoning", text: u.entries.map((e) => `• ${e.content ?? ""}`).join("\n") });
375
+ if (Array.isArray(u.entries)) bivy({ type: "message.reasoning", text: u.entries.map((e) => `• ${e.content ?? ""}`).join("\n") });
471
376
  break;
472
377
  default:
473
378
  break;
@@ -486,9 +391,7 @@ async function onAgentRequest(id, method, params) {
486
391
  const options = Array.isArray(params?.options) ? params.options : [];
487
392
  permissionRequests.set(toolCallId, { requestId: id, options });
488
393
  const state = mergeToolCallState(toolCallId, tc);
489
- const parentCall = childRoute(params?.sessionId);
490
- if (parentCall === null) ownCalls.set(toolCallId, state);
491
- bivy({ type: "tool.call", toolCallId, name: toolCallName(state), input: toolCallInput(state), ...(parentCall ? { parentToolCallId: parentCall } : {}) });
394
+ bivy({ type: "tool.call", toolCallId, name: toolCallName(state), input: toolCallInput(state) });
492
395
  return;
493
396
  }
494
397
  case "fs/read_text_file": {
@@ -510,15 +413,9 @@ async function onAgentRequest(id, method, params) {
510
413
  try {
511
414
  if (sandboxTier === "read-only") throw new Error("writes are disabled by the read-only sandbox");
512
415
  const file = workspacePath(params?.path, { write: true });
513
- if (approvedCallWriting(file)) {
514
- fs.writeFileSync(file, String(params?.content ?? ""));
515
- agentReply(id, {});
516
- return;
517
- }
518
416
  const toolCallId = `acp-fs-write-${id}`;
519
417
  permissionRequests.set(toolCallId, { kind: "fs-write", requestId: id, file, content: String(params?.content ?? "") });
520
- const parentCall = childRoute(params?.sessionId);
521
- bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file }, ...(parentCall ? { parentToolCallId: parentCall } : {}) });
418
+ bivy({ type: "tool.call", toolCallId, name: "write", input: { path: file } });
522
419
  } catch (e) {
523
420
  agentReplyError(id, -32000, `write failed: ${e.message}`);
524
421
  }
@@ -552,7 +449,6 @@ createInterface({ input: agent.stdout }).on("line", (line) => {
552
449
  if (msg.id !== undefined && msg.method) { void onAgentRequest(msg.id, msg.method, msg.params); return; }
553
450
  // Notification (method, no id).
554
451
  if (msg.method === "session/update") onSessionUpdate(msg.params);
555
- else if (msg.params?.update) linkChildSession(msg.params.update);
556
452
  });
557
453
 
558
454
  /**
@@ -611,14 +507,7 @@ async function onBivyCommand(msg) {
611
507
  // hanging the reopen with no watchdog to recover it (resume isn't a
612
508
  // "working" turn). On timeout/failure we fall back to a fresh session so
613
509
  // the chat still opens instead of spinning on "Fetching transcript…".
614
- // ACP agents replay the whole conversation as session/update during
615
- // session/load. Bivy already has that history; forwarding the replay
616
- // re-recorded past thinking (and could fold old text onto the last
617
- // reply) as if it were live.
618
- replayingHistory = true;
619
- let res;
620
- try { res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 }); }
621
- finally { replayingHistory = false; }
510
+ const res = await agentRequest("session/load", { sessionId: ref, cwd, mcpServers }, { timeoutMs: 30_000 });
622
511
  sessionId = res?.sessionId ?? ref;
623
512
  publishModels(res);
624
513
  } catch (error) {
@@ -665,22 +554,16 @@ async function onBivyCommand(msg) {
665
554
  permissionRequests.delete(msg.toolCallId);
666
555
  const allow = msg.decision !== "deny";
667
556
  if (entry.kind === "fs-write") {
668
- // The write card is the shim's own call, so the shim closes it too —
669
- // left open it pinned the turn "Working" after the agent finished.
670
- let error = allow ? "" : String(msg.reason || "write denied by Bivy policy");
671
- if (allow) {
672
- try { fs.writeFileSync(entry.file, entry.content); }
673
- catch (e) { error = `write failed: ${e.message}`; }
557
+ if (!allow) agentReplyError(entry.requestId, -32001, String(msg.reason || "write denied by Bivy policy"));
558
+ else {
559
+ try { fs.writeFileSync(entry.file, entry.content); agentReply(entry.requestId, {}); }
560
+ catch (e) { agentReplyError(entry.requestId, -32000, `write failed: ${e.message}`); }
674
561
  }
675
- if (error) agentReplyError(entry.requestId, allow ? -32000 : -32001, error);
676
- else agentReply(entry.requestId, {});
677
- bivy({ type: "tool.result", toolCallId: msg.toolCallId, name: "write", result: error || `Wrote ${entry.file}`, isError: Boolean(error) });
678
562
  return;
679
563
  }
680
564
  // Pick an ACP option matching the human's choice by its `kind`
681
565
  // (allow_once/allow_always vs reject_once/reject_always); fall back to the
682
566
  // first option, or a cancelled outcome when nothing fits.
683
- if (allow) approvedCalls.add(msg.toolCallId);
684
567
  const want = allow ? /^allow/ : /^reject/;
685
568
  const opt = entry.options.find((o) => want.test(String(o.kind || ""))) ?? entry.options[0];
686
569
  if (opt && opt.optionId !== undefined) agentReply(entry.requestId, { outcome: { outcome: "selected", optionId: opt.optionId } });
@@ -701,15 +584,6 @@ async function onBivyCommand(msg) {
701
584
  bivy({ replyTo: id, ok: true });
702
585
  return;
703
586
  }
704
- case "thinking.set": {
705
- const level = String(msg.level ?? "").trim();
706
- if (sessionId && thoughtConfigId && level) {
707
- try { await agentRequest("session/set_config_option", { sessionId, configId: thoughtConfigId, value: level }, { timeoutMs: 15_000 }); }
708
- catch (e) { bivy({ type: "runtime.debug", message: `acp set thought level failed: ${e instanceof Error ? e.message : String(e)}` }); }
709
- }
710
- if (id !== undefined) bivy({ replyTo: id, ok: true });
711
- return;
712
- }
713
587
  case "session.abort": {
714
588
  // A pending drain must not fire session.done after the user cancelled —
715
589
  // the turn is being torn down, not finishing on its own.
@@ -22,7 +22,7 @@
22
22
  "hidden": false,
23
23
  "supportTier": "supported",
24
24
  "certification": "release-tested",
25
- "testedVersion": "1.18.33",
25
+ "testedVersion": "1.18.32",
26
26
  "headlessFlags": [
27
27
  "run",
28
28
  "-s"
@@ -191,7 +191,7 @@
191
191
  "hidden": false,
192
192
  "supportTier": "supported",
193
193
  "certification": "release-tested",
194
- "testedVersion": "1.0.44",
194
+ "testedVersion": "1.0.41",
195
195
  "headlessFlags": [
196
196
  "--always-approve",
197
197
  "-p",
package/bin/bivy.mjs CHANGED
@@ -35,7 +35,7 @@ import { spawn, spawnSync } from "node:child_process";
35
35
  import { randomBytes, createCipheriv, createDecipheriv, randomUUID, createHash, verify as cryptoVerify } from "node:crypto";
36
36
  import { fileURLToPath, pathToFileURL } from "node:url";
37
37
  import vm from "node:vm";
38
- import { linkSessionWorktrees, selectStaleSessions, sessionActivityMs } from "./prune-sessions.mjs";
38
+ import { selectStaleSessions, sessionActivityMs } from "./prune-sessions.mjs";
39
39
  import { nativeResumeRef, resolveSessionsLimit, truncateSavedSessions } from "./sessions-list.mjs";
40
40
  import { renderManagedBlock, upsertManagedBlock, removeManagedBlock, rcFileForShell } from "./shim-path.mjs";
41
41
  import { removeInstallAndState } from "./uninstall-paths.mjs";
@@ -1827,7 +1827,7 @@ function cmdCompletions(args = []) {
1827
1827
  const shell = (args[0] || "").toLowerCase();
1828
1828
  const commands = [
1829
1829
  "run", "runs", "sessions", "ls", "resume", "promote", "rename", "nodes", "agent", "agents", "agents:install", "shim", "takeover", "token", "exec",
1830
- "send", "attach", "suggest", "delegate", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
1830
+ "send", "attach", "suggest", "kill", "setup", "start", "stop", "restart", "status", "doctor", "diagnostics", "capabilities", "logs", "login", "logout", "signout", "provider", "model",
1831
1831
  "update", "update:log", "audit", "automation", "config", "plugin", "open", "service", "secrets", "voice", "link", "relay:setup",
1832
1832
  "github:connect", "github:app-create", "github:app-connect", "github:app-sync", "prune", "uninstall", "help", "version",
1833
1833
  ];
@@ -2462,131 +2462,6 @@ async function cmdSuggest(args = []) {
2462
2462
  console.log(c.green("Suggested in the chat. The user can start it in one tap."));
2463
2463
  }
2464
2464
 
2465
- // Cross-agent / cross-machine delegation (docs/agent-delegation.md): hand a
2466
- // self-contained task from this session to another agent, optionally on another
2467
- // account machine, and get its answer back. Agent-agnostic — any agent with a
2468
- // shell can call it — and meant for when the user asks for another agent/machine.
2469
- const DELEGATE_USAGE = `Usage: bivy delegate "<task>" [--agent <id>] [--machine <name>] [--model <model>] [--repo owner/repo] [--approval never|risky|always|autonomous] [--sandbox <tier>] [--wait [seconds]] [--json]
2470
- bivy delegate "<task>" --to <agent>[@<machine>],<agent>[@<machine>],… [--wait [seconds]]
2471
- bivy delegate machines [--json]
2472
- bivy delegate status <run-id> [--json]
2473
- bivy delegate wait <run-id> [--timeout <seconds>] [--json]
2474
-
2475
- Hands a self-contained task to another agent (and/or another machine on your account) as a
2476
- delegated Run tied to this session ($BIVY_SESSION_ID). With --wait (or 'wait'), prints the
2477
- child's final answer and any branch/PR it produced once it finishes. --to sends the same task
2478
- to several agents/machines at once (up to 3) so the results can be compared.
2479
- 'machines' lists your machines and the agents installed on each.`;
2480
-
2481
- async function cmdDelegate(args = []) {
2482
- if (!args.length || args.some((a) => a === "-h" || a === "--help")) { console.log(DELEGATE_USAGE); return; }
2483
- const flagsWithValue = new Set(["--session", "--agent", "--machine", "--model", "--repo", "--timeout", "--to", "--approval", "--sandbox"]);
2484
- // Children inherit this session's approval/sandbox unless set here.
2485
- const safetyFlags = () => {
2486
- const safety = { approval: flag("--approval"), sandbox: flag("--sandbox") };
2487
- return safety.approval || safety.sandbox ? { safety } : {};
2488
- };
2489
- const flag = (name) => {
2490
- const i = args.indexOf(name);
2491
- return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
2492
- };
2493
- const json = args.includes("--json");
2494
- const waitIndex = args.indexOf("--wait");
2495
- const waitArg = waitIndex >= 0 && /^\d+$/.test(args[waitIndex + 1] ?? "") ? args[waitIndex + 1] : undefined;
2496
- const positional = args.filter((a, i) => !a.startsWith("--") && !(i > 0 && flagsWithValue.has(args[i - 1])) && !(waitArg && i === waitIndex + 1));
2497
- const sessionId = resolveAttachSessionId({ sessionFlag: flag("--session"), env: process.env });
2498
- if (!sessionId) { console.error(c.red("No session id. Run inside an agent session ($BIVY_SESSION_ID) or pass --session <id>.")); process.exit(1); return; }
2499
- const config = loadConfig();
2500
- if (!(await ensureNodeRunning(config))) { console.error(c.red(`Could not reach the Bivy node at ${url(config)}.`)); process.exit(1); return; }
2501
- let token;
2502
- try { token = await localDeviceToken(config); } catch { token = undefined; }
2503
- const base = `${url(config)}/api/session/${encodeURIComponent(sessionId)}/delegated-runs`;
2504
- const call = async (path, body) => {
2505
- const res = await fetch(`${base}${path}`, {
2506
- method: body ? "POST" : "GET",
2507
- headers: { "content-type": "application/json", ...(token ? { authorization: `Bearer ${token}` } : {}) },
2508
- ...(body ? { body: JSON.stringify(body) } : {}),
2509
- });
2510
- const data = await res.json().catch(() => ({}));
2511
- if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
2512
- return data;
2513
- };
2514
- // needs_attention = parked by the machine's policy; it won't move on its own.
2515
- const terminal = (run) => ["succeeded", "failed", "cancelled", "needs_attention"].includes(run?.status);
2516
- // The node caps one wait at 300s; loop until the child ends or our budget does.
2517
- const waitFor = async (runId, seconds) => {
2518
- const deadline = Date.now() + seconds * 1000;
2519
- let run = await call(`/${encodeURIComponent(runId)}`);
2520
- while (!terminal(run) && Date.now() < deadline) {
2521
- const slice = Math.max(1, Math.min(300, Math.ceil((deadline - Date.now()) / 1000)));
2522
- run = await call(`/${encodeURIComponent(runId)}/wait`, { timeoutSeconds: slice });
2523
- }
2524
- return run;
2525
- };
2526
- const print = (run) => {
2527
- if (json) { console.log(JSON.stringify(run, null, 2)); return; }
2528
- console.log(`Run ${run.runId}: ${run.status}${terminal(run) ? "" : " (still running — 'bivy delegate wait " + run.runId + "')"}`);
2529
- const refs = run.references || {};
2530
- if (refs.failure) console.log(`Failure: ${refs.failure}`);
2531
- if (refs.branch) console.log(`Branch: ${refs.branch}`);
2532
- if (refs.prUrl) console.log(`PR: ${refs.prUrl}`);
2533
- if (refs.sessionId) console.log(`Child session: ${refs.sessionId}`);
2534
- if (run.answer) console.log(`\nAnswer:\n${run.answer}`);
2535
- };
2536
- try {
2537
- const [action, runId] = positional;
2538
- if (action === "machines" && positional.length === 1) {
2539
- const res = await fetch(`${url(config)}/api/machines`, { headers: token ? { authorization: `Bearer ${token}` } : {} });
2540
- const data = await res.json().catch(() => ({}));
2541
- if (!res.ok) throw new Error(data?.error || `HTTP ${res.status}`);
2542
- if (json) { console.log(JSON.stringify(data.machines, null, 2)); return; }
2543
- for (const m of data.machines || []) {
2544
- const agents = m.agents ? m.agents.map((a) => a.id).join(", ") || "none" : m.error ? `unknown (${m.error})` : "unknown while offline";
2545
- console.log(`${m.name}${m.self ? " (this machine)" : ""} — ${m.online ? "online" : "offline"} — agents: ${agents}`);
2546
- }
2547
- return;
2548
- }
2549
- if (action === "status" && runId) return print(await call(`/${encodeURIComponent(runId)}`));
2550
- if (action === "wait" && runId) return print(await waitFor(runId, Number(flag("--timeout") ?? 600)));
2551
- const instructions = positional.join(" ").trim();
2552
- if (!instructions) { console.error(c.red(DELEGATE_USAGE)); process.exit(1); return; }
2553
- // Fan-out: the same task to several agent@machine targets, one group, so the
2554
- // parent's transcript shows them side by side to compare.
2555
- const to = flag("--to");
2556
- if (to) {
2557
- const targets = to.split(",").map((t) => t.trim()).filter(Boolean).map((t) => {
2558
- const [agent, machine] = t.split("@");
2559
- return { agent: agent ? governedChatAgentId(agent) : undefined, machine: machine || undefined, label: t };
2560
- });
2561
- const group = `fanout-${Date.now().toString(36)}`;
2562
- const runs = [];
2563
- for (const target of targets) {
2564
- runs.push({ target, run: await call("", { instructions, agent: target.agent, machine: target.machine, model: flag("--model"), repo: flag("--repo"), ...safetyFlags(), group }) });
2565
- }
2566
- if (waitIndex < 0) {
2567
- if (json) console.log(JSON.stringify(runs.map((r) => r.run), null, 2));
2568
- else for (const { target, run } of runs) console.log(`${target.label}: run ${run.runId} (${run.status})`);
2569
- return;
2570
- }
2571
- const finished = await Promise.all(runs.map(async ({ target, run }) => ({ target, run: await waitFor(run.runId, Number(waitArg ?? 600)) })));
2572
- if (json) { console.log(JSON.stringify(finished.map((f) => f.run), null, 2)); return; }
2573
- for (const { target, run } of finished) { console.log(`\n── ${target.label}`); print(run); }
2574
- return;
2575
- }
2576
- const agent = flag("--agent");
2577
- const started = await call("", { instructions, agent: agent ? governedChatAgentId(agent) : undefined, machine: flag("--machine"), model: flag("--model"), repo: flag("--repo"), ...safetyFlags() });
2578
- if (waitIndex < 0) {
2579
- if (!json) console.log(`Delegated as run ${started.runId} (${started.status}). Follow it with 'bivy delegate wait ${started.runId}'.`);
2580
- else print(started);
2581
- return;
2582
- }
2583
- print(await waitFor(started.runId, Number(waitArg ?? 600)));
2584
- } catch (error) {
2585
- console.error(c.red(`Delegate failed: ${error?.message || String(error)}`));
2586
- process.exit(1);
2587
- }
2588
- }
2589
-
2590
2465
  // Universal app publishing: every agent that can write JSON and run a command
2591
2466
  // uses the same contract. Relative static directories are manifest-relative.
2592
2467
  async function cmdApp(args = []) {
@@ -2597,7 +2472,7 @@ async function cmdApp(args = []) {
2597
2472
  bivy app remove <app-id> [--session <id>]
2598
2473
  bivy app shot [app-id] [--widths 390,1280] [--themes light,dark] [--path /page] [--session <id>]
2599
2474
  bivy app present [view] [--path /page] [--note "…"] [--session <id>]
2600
- bivy app share [app-id] [--view <view>] [--for 1h|1d|7d] [--view-only] [--session <id>]
2475
+ bivy app share [app-id] [--view <view>] [--session <id>]
2601
2476
  bivy app notes [app-id] [--view <view>] [--since <time>] [--session <id>]
2602
2477
  bivy app run [--name <name>] [--restart-on-change] [--session <id>] -- <command> [args…]
2603
2478
  bivy app click <x> <y> [--right|--middle] [--double] [--app <app>]
@@ -2643,10 +2518,9 @@ the chat with the app at phone width (and before/after), and it opens the live
2643
2518
  preview. Use it when you finish a visible change, or when the user asks to see
2644
2519
  it. [view] is an app or view name or ID (default: the one opened last).
2645
2520
  "share" mints a reusable link to one web view and prints its URL and expiry
2646
- (--for: 1h, 1d or 7d, default 1d; sooner if the user stops sharing in Apps, the
2647
- app is removed or the machine restarts). [app-id] is an app ID or name, --view
2648
- a view ID or name (default: the one opened last). --view-only leaves out the
2649
- reviewer tools (Point, Draw, notes): people see just the app. Anyone who has the link can use the app, including a live
2521
+ (24 hours, or until Revoke access in Apps, the app is removed or the machine
2522
+ restarts). [app-id] is an app ID or name, --view a view ID or name (default: the
2523
+ one opened last). Anyone who has the link can use the app, including a live
2650
2524
  server's backend, until it expires or is revoked: only post it where everyone
2651
2525
  who can read it may use the app.
2652
2526
  "notes" prints the notes people left through share links (Leave a note), as
@@ -2683,17 +2557,6 @@ Web previews require operator setup; see docs/apps.md.`);
2683
2557
  rest.splice(i, 2);
2684
2558
  }
2685
2559
  }
2686
- if (action === "share") {
2687
- const i = rest.indexOf("--for");
2688
- if (i >= 0) {
2689
- const value = rest[i + 1];
2690
- if (!["1h", "1d", "7d"].includes(value)) throw new Error("--for takes 1h, 1d or 7d.");
2691
- shot.duration = value;
2692
- rest.splice(i, 2);
2693
- }
2694
- const viewOnly = rest.indexOf("--view-only");
2695
- if (viewOnly >= 0) { shot.controls = false; rest.splice(viewOnly, 1); }
2696
- }
2697
2560
  if (action === "notes") {
2698
2561
  const i = rest.indexOf("--since");
2699
2562
  if (i >= 0) {
@@ -2793,7 +2656,7 @@ async function appRequest(action, body, { print = true } = {}) {
2793
2656
  if (action === "share") {
2794
2657
  const { type: _type, requestId: _requestId, ...link } = result;
2795
2658
  console.log(JSON.stringify({ ...link, expires: new Date(link.expiresAt).toISOString() }, null, 2));
2796
- console.error("Anyone with this link can use the app, including its live backend, until it expires or the user stops sharing (Apps → Share → Stop sharing).");
2659
+ console.error("Anyone with this link can use the app, including its live backend, until it expires or you revoke it (Apps → Revoke access).");
2797
2660
  return;
2798
2661
  }
2799
2662
  if (print) console.log(JSON.stringify(result, null, 2));
@@ -3174,11 +3037,6 @@ ${c.bold("bivy prune")} — remove old sessions, --clone workspaces, and git wor
3174
3037
  is never pruned while the node is running, regardless of --keep/--older-than —
3175
3038
  only idle worktrees are removed. If the node is down, nothing is live to guard.
3176
3039
 
3177
- A session and its worktree are removed together: pruning a worktree also
3178
- removes the sessions that ran in it, pruning a session also removes its
3179
- worktree (once no remaining session uses it), and sessions whose worktree is
3180
- already gone are removed — so no session is left pointing at a deleted checkout.
3181
-
3182
3040
  Paths (default to the installed node)
3183
3041
  --data-dir <dir> node data dir (default: this install's .bivy, or $BIVY_DATA_DIR)
3184
3042
  --workspace <dir> also scan this workspace for worktrees (default: configured workspace)
@@ -3246,41 +3104,12 @@ async function cmdPrune(args = []) {
3246
3104
  const now = Date.now();
3247
3105
  const plan = [];
3248
3106
  let worktreeGuard = { reachable: false, protected: 0 };
3249
- // Sessions live in the metadata index (all agents) plus each agent's own
3250
- // transcript store — not just .bivy/pi/sessions. Select from metadata so
3251
- // shim/native (Claude Code, Codex, …) sessions are actually covered; the old
3252
- // pi-sessions-only scan silently no-oped for every terminal-started session.
3253
- const allSessions = loadMetadataSessions(dataDir).sessions;
3254
- const selectedSessions = doSessions ? selectStaleSessions(allSessions, keepFor("sessions"), ageMs, now) : [];
3255
- // Worktrees: keep the newest N overall (across all roots), matching the node
3256
- // prune script. Scan roots: the data dir plus the configured workspace repo.
3257
- const worktreeRoots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
3258
- const staleWorktrees = doWorktrees ? selectStale(worktreeRoots.flatMap((r) => pruneListEntries(r, "dir")), keepFor("worktrees"), ageMs, now) : [];
3259
- // Guard: never delete a worktree a live agent is using. Ask the daemon what's
3260
- // live (it owns every runtime and PTY); protected worktrees drop out of the
3261
- // removal set entirely, so an aggressive --older-than can't nuke a running
3262
- // agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
3263
- const live = await liveWorktreePaths(config);
3264
- const isLive = (p) => isLiveWorktree(p, live.paths);
3265
- const idleWorktrees = staleWorktrees.filter((e) => !isLive(e.path));
3266
- worktreeGuard = { reachable: live.reachable, protected: staleWorktrees.length - idleWorktrees.length };
3267
- // A session and its worktree go together: removing one without the other left
3268
- // sessions pointing at a deleted checkout (or checkouts nobody can reach).
3269
- const linked = linkSessionWorktrees({
3270
- sessions: allSessions,
3271
- staleSessions: selectedSessions,
3272
- staleWorktrees: idleWorktrees.map((e) => e.path),
3273
- isLive,
3274
- exists: (p) => fs.existsSync(p),
3275
- });
3276
- const staleSessions = linked.sessions;
3277
- const worktreeMtime = new Map(idleWorktrees.map((e) => [path.resolve(e.path), e.mtimeMs]));
3278
- const worktreesToRemove = linked.worktrees.map((p) => {
3279
- let mtimeMs = worktreeMtime.get(p);
3280
- if (mtimeMs === undefined) { try { mtimeMs = fs.statSync(p).mtimeMs; } catch { mtimeMs = now; } }
3281
- return { path: p, mtimeMs };
3282
- }).sort((a, b) => b.mtimeMs - a.mtimeMs);
3283
- if (doSessions || staleSessions.length) {
3107
+ if (doSessions) {
3108
+ // Sessions live in the metadata index (all agents) plus each agent's own
3109
+ // transcript store — not just .bivy/pi/sessions. Select from metadata so
3110
+ // shim/native (Claude Code, Codex, …) sessions are actually covered; the old
3111
+ // pi-sessions-only scan silently no-oped for every terminal-started session.
3112
+ const staleSessions = selectStaleSessions(loadMetadataSessions(dataDir).sessions, keepFor("sessions"), ageMs, now);
3284
3113
  plan.push({
3285
3114
  kind: "sessions",
3286
3115
  root: metadataFilePath(dataDir),
@@ -3303,8 +3132,20 @@ async function cmdPrune(args = []) {
3303
3132
  const dir = path.join(dataDir, "workspaces");
3304
3133
  plan.push({ kind: "workspaces", root: dir, remove: selectStale(pruneListEntries(dir, "dir"), keepFor("workspaces"), ageMs, now) });
3305
3134
  }
3306
- if (doWorktrees || worktreesToRemove.length) {
3307
- plan.push({ kind: "worktrees", root: worktreeRoots.join(", ") || "(none found)", remove: worktreesToRemove });
3135
+ if (doWorktrees) {
3136
+ // Keep the newest N worktrees overall (across all roots), matching the node
3137
+ // prune script. Scan roots: the data dir plus the configured workspace repo.
3138
+ const roots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
3139
+ const all = roots.flatMap((r) => pruneListEntries(r, "dir"));
3140
+ const stale = selectStale(all, keepFor("worktrees"), ageMs, now);
3141
+ // Guard: never delete a worktree a live agent is using. Ask the daemon what's
3142
+ // live (it owns every runtime and PTY); protected worktrees drop out of the
3143
+ // removal set entirely, so an aggressive --older-than can't nuke a running
3144
+ // agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
3145
+ const live = await liveWorktreePaths(config);
3146
+ const remove = stale.filter((e) => !isLiveWorktree(e.path, live.paths));
3147
+ worktreeGuard = { reachable: live.reachable, protected: stale.length - remove.length };
3148
+ plan.push({ kind: "worktrees", root: roots.join(", ") || "(none found)", remove });
3308
3149
  }
3309
3150
  const total = plan.reduce((n, p) => n + p.remove.length, 0);
3310
3151
 
@@ -5615,9 +5456,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5615
5456
  case "suggest":
5616
5457
  await cmdSuggest(args);
5617
5458
  break;
5618
- case "delegate":
5619
- await cmdDelegate(args);
5620
- break;
5621
5459
  case "completions":
5622
5460
  case "completion":
5623
5461
  cmdCompletions(args);