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

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/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 = []) {
@@ -3174,11 +3049,6 @@ ${c.bold("bivy prune")} — remove old sessions, --clone workspaces, and git wor
3174
3049
  is never pruned while the node is running, regardless of --keep/--older-than —
3175
3050
  only idle worktrees are removed. If the node is down, nothing is live to guard.
3176
3051
 
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
3052
  Paths (default to the installed node)
3183
3053
  --data-dir <dir> node data dir (default: this install's .bivy, or $BIVY_DATA_DIR)
3184
3054
  --workspace <dir> also scan this workspace for worktrees (default: configured workspace)
@@ -3246,41 +3116,12 @@ async function cmdPrune(args = []) {
3246
3116
  const now = Date.now();
3247
3117
  const plan = [];
3248
3118
  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) {
3119
+ if (doSessions) {
3120
+ // Sessions live in the metadata index (all agents) plus each agent's own
3121
+ // transcript store — not just .bivy/pi/sessions. Select from metadata so
3122
+ // shim/native (Claude Code, Codex, …) sessions are actually covered; the old
3123
+ // pi-sessions-only scan silently no-oped for every terminal-started session.
3124
+ const staleSessions = selectStaleSessions(loadMetadataSessions(dataDir).sessions, keepFor("sessions"), ageMs, now);
3284
3125
  plan.push({
3285
3126
  kind: "sessions",
3286
3127
  root: metadataFilePath(dataDir),
@@ -3303,8 +3144,20 @@ async function cmdPrune(args = []) {
3303
3144
  const dir = path.join(dataDir, "workspaces");
3304
3145
  plan.push({ kind: "workspaces", root: dir, remove: selectStale(pruneListEntries(dir, "dir"), keepFor("workspaces"), ageMs, now) });
3305
3146
  }
3306
- if (doWorktrees || worktreesToRemove.length) {
3307
- plan.push({ kind: "worktrees", root: worktreeRoots.join(", ") || "(none found)", remove: worktreesToRemove });
3147
+ if (doWorktrees) {
3148
+ // Keep the newest N worktrees overall (across all roots), matching the node
3149
+ // prune script. Scan roots: the data dir plus the configured workspace repo.
3150
+ const roots = findWorktreeRoots([dataDir, workspace].filter(Boolean));
3151
+ const all = roots.flatMap((r) => pruneListEntries(r, "dir"));
3152
+ const stale = selectStale(all, keepFor("worktrees"), ageMs, now);
3153
+ // Guard: never delete a worktree a live agent is using. Ask the daemon what's
3154
+ // live (it owns every runtime and PTY); protected worktrees drop out of the
3155
+ // removal set entirely, so an aggressive --older-than can't nuke a running
3156
+ // agent's checkout. Node down ⇒ nothing live ⇒ policy alone is safe.
3157
+ const live = await liveWorktreePaths(config);
3158
+ const remove = stale.filter((e) => !isLiveWorktree(e.path, live.paths));
3159
+ worktreeGuard = { reachable: live.reachable, protected: stale.length - remove.length };
3160
+ plan.push({ kind: "worktrees", root: roots.join(", ") || "(none found)", remove });
3308
3161
  }
3309
3162
  const total = plan.reduce((n, p) => n + p.remove.length, 0);
3310
3163
 
@@ -5615,9 +5468,6 @@ An agent's own --help passes through, e.g. 'bivy run claude --help'.`);
5615
5468
  case "suggest":
5616
5469
  await cmdSuggest(args);
5617
5470
  break;
5618
- case "delegate":
5619
- await cmdDelegate(args);
5620
- break;
5621
5471
  case "completions":
5622
5472
  case "completion":
5623
5473
  cmdCompletions(args);
@@ -9,8 +9,6 @@
9
9
  // not just Pi's `.bivy/pi/sessions`. Prune selects from that index; these helpers
10
10
  // decide which entries are stale. No I/O here on purpose.
11
11
 
12
- import path from "node:path";
13
-
14
12
  // Mirrors the server's isEmptyUntitledSummary: a session with no real title, no
15
13
  // first message and no messages is the hidden "Untitled" noise the sidebar filters
16
14
  // out. Keep-N protects only *real* sessions; empties are always eligible.
@@ -32,42 +30,6 @@ export function sessionActivityMs(s) {
32
30
  return 0;
33
31
  }
34
32
 
35
- // A Bivy-managed worktree lives at <repo>/.bivy/worktrees/<slug>. Only those are
36
- // ever removed on a session's behalf — never an arbitrary path a record names.
37
- export function isManagedWorktreePath(p) {
38
- const parts = path.resolve(String(p ?? "")).split(path.sep);
39
- return parts.length >= 3 && parts[parts.length - 3] === ".bivy" && parts[parts.length - 2] === "worktrees";
40
- }
41
-
42
- // Sessions and their worktrees are pruned as one unit, so neither outlives the
43
- // other. Selection runs per kind (sessions and worktrees have different keep
44
- // counts), then this links the two removal sets:
45
- // - a session goes when its worktree goes, or when its worktree is already
46
- // gone from disk (a session that can no longer open its checkout);
47
- // - a worktree goes when every session using it goes (and no live agent holds it).
48
- // Live ("working") sessions are never added, and their worktree is never removed. `isLive(path)` / `exists(path)` are
49
- // the caller's I/O; paths are compared resolved. Returns the widened sets.
50
- export function linkSessionWorktrees({ sessions, staleSessions, staleWorktrees, isLive = () => false, exists = () => true }) {
51
- const removeSessions = new Map(staleSessions.map((s) => [s.id, s]));
52
- const removeWorktrees = new Set(staleWorktrees.map((p) => path.resolve(p)));
53
- const byWorktree = new Map();
54
- for (const s of sessions) {
55
- if (!s.worktree || !isManagedWorktreePath(s.worktree)) continue;
56
- const wt = path.resolve(s.worktree);
57
- byWorktree.set(wt, [...(byWorktree.get(wt) ?? []), s]);
58
- }
59
- for (const [wt, owners] of byWorktree) {
60
- if (owners.some((s) => s.status === "working")) removeWorktrees.delete(wt);
61
- else if (exists(wt) && owners.every((s) => removeSessions.has(s.id)) && !isLive(wt)) removeWorktrees.add(wt);
62
- }
63
- for (const [wt, owners] of byWorktree) {
64
- if (!removeWorktrees.has(wt) && exists(wt)) continue;
65
- for (const s of owners) if (s.status !== "working") removeSessions.set(s.id, s);
66
- }
67
- const sorted = [...removeSessions.values()].sort((a, b) => sessionActivityMs(b) - sessionActivityMs(a));
68
- return { sessions: sorted, worktrees: [...removeWorktrees] };
69
- }
70
-
71
33
  // Removal set for sessions. Live ("working") sessions are never touched. The
72
34
  // newest `keep` NON-EMPTY sessions (across every agent) survive; empty/untitled
73
35
  // sessions are always beyond the keep window. An item is removed only when it is
@@ -42,10 +42,6 @@ export const BIVY_AGENT_NOTE = [
42
42
  '`bivy suggest "<complete instruction>" [--title "short label"]` instead of only listing them. The user can ' +
43
43
  "start each in one tap, in this session or in a parallel one that works in its own copy of the project, so " +
44
44
  "write it to stand on its own, with paths relative to the project root.",
45
- "- Only when the user asks for another agent or another of their machines to take part: " +
46
- '`bivy delegate "<self-contained task>" --agent <id> [--machine <name>] --wait` runs it there and prints ' +
47
- "its answer and any branch/PR; `--to codex,grok@<machine>` asks several to compare, `bivy delegate machines` " +
48
- "lists machines and their agents. Use your own sub-agents for everything else.",
49
45
  ].join("\n");
50
46
  const PREAMBLE = "The user's global Bivy instructions follow. They apply to every workspace; where they conflict with " +
51
47
  "instructions from the current repository (AGENTS.md, CLAUDE.md, …), the repository's instructions win.";
@@ -4,7 +4,7 @@ import { defineAgentIntegration } from "../definition.js";
4
4
  import { withExactCapabilitySurface } from "../../runtime/types.js";
5
5
  import { createCredentialStore } from "../../runtime/credentials.js";
6
6
  import { ClaudeCodeRuntime, claudeRuntimeFromEnv, claudeSdkInstalled, } from "./runtime.js";
7
- export const CLAUDE_TESTED_VERSION = "0.3.284";
7
+ export const CLAUDE_TESTED_VERSION = "0.3.281";
8
8
  const CLAUDE_CAPABILITIES = withExactCapabilitySurface({
9
9
  toolInterception: true,
10
10
  modelSelection: true,