@livx.cc/agentx 0.95.1 → 0.95.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/dist/index.js CHANGED
@@ -1588,7 +1588,7 @@ function makeRealShellTool(options) {
1588
1588
  proc.stderr?.on("data", collect);
1589
1589
  proc.on("error", (err) => {
1590
1590
  if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
1591
- log4.debug("shell spawn error", err);
1591
+ log5.debug("shell spawn error", err);
1592
1592
  finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
1593
1593
  });
1594
1594
  proc.on("close", (code) => {
@@ -1650,7 +1650,7 @@ ${clean(out) || "(no output yet)"}`;
1650
1650
  }
1651
1651
  ];
1652
1652
  }
1653
- var log4, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1653
+ var log5, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1654
1654
  var init_tools_shell = __esm({
1655
1655
  "src/tools.shell.ts"() {
1656
1656
  "use strict";
@@ -1658,7 +1658,7 @@ var init_tools_shell = __esm({
1658
1658
  init_redact();
1659
1659
  init_logging();
1660
1660
  init_shell_sandbox();
1661
- log4 = forComponent("shell");
1661
+ log5 = forComponent("shell");
1662
1662
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
1663
1663
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
1664
1664
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
@@ -2435,7 +2435,163 @@ ${sections.join("\n\n---\n\n")}`;
2435
2435
 
2436
2436
  // src/subagent.ts
2437
2437
  init_tools();
2438
+ import { mkdirSync as mkdirSync2, writeFileSync } from "fs";
2439
+ import { join as join3 } from "path";
2438
2440
  init_OverlayFilesystem();
2441
+
2442
+ // src/worktree.ts
2443
+ import { spawnSync } from "child_process";
2444
+ import { symlinkSync, mkdirSync, statSync, readFileSync, rmSync } from "fs";
2445
+ import { join } from "path";
2446
+ var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
2447
+ var MAX_SLUG_LENGTH = 64;
2448
+ var GIT_NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
2449
+ function validateWorktreeSlug(slug2) {
2450
+ if (!slug2) throw new Error("worktree slug must not be empty");
2451
+ if (slug2.length > MAX_SLUG_LENGTH) throw new Error(`worktree slug must be \u2264${MAX_SLUG_LENGTH} chars (got ${slug2.length})`);
2452
+ for (const seg of slug2.split("/")) {
2453
+ if (seg === "." || seg === "..") throw new Error(`worktree slug must not contain "." or ".." segments`);
2454
+ if (!VALID_SEGMENT.test(seg)) throw new Error(`worktree slug segment "${seg}" contains invalid characters (allowed: a-z A-Z 0-9 . _ -)`);
2455
+ }
2456
+ }
2457
+ function flattenSlug(slug2) {
2458
+ return slug2.replaceAll("/", "+");
2459
+ }
2460
+ function worktreeBranchName(slug2) {
2461
+ return `worktree-${flattenSlug(slug2)}`;
2462
+ }
2463
+ function worktreesDir(repoRoot) {
2464
+ return join(repoRoot, ".claude", "worktrees");
2465
+ }
2466
+ function worktreePathFor(repoRoot, slug2) {
2467
+ return join(worktreesDir(repoRoot), flattenSlug(slug2));
2468
+ }
2469
+ function git(args, cwd) {
2470
+ const r = spawnSync("git", args, {
2471
+ cwd,
2472
+ stdio: ["ignore", "pipe", "pipe"],
2473
+ env: { ...process.env, ...GIT_NO_PROMPT_ENV }
2474
+ });
2475
+ return { ok: r.status === 0, stdout: (r.stdout ?? "").toString().trim(), stderr: (r.stderr ?? "").toString().trim() };
2476
+ }
2477
+ function findGitRoot(from) {
2478
+ const r = git(["rev-parse", "--show-toplevel"], from);
2479
+ return r.ok ? r.stdout : null;
2480
+ }
2481
+ function readWorktreeHead(worktreePath) {
2482
+ try {
2483
+ const gitFile = readFileSync(join(worktreePath, ".git"), "utf-8").trim();
2484
+ const m = gitFile.match(/^gitdir:\s*(.+)$/);
2485
+ if (!m) return null;
2486
+ const headPath = join(m[1], "HEAD");
2487
+ const head = readFileSync(headPath, "utf-8").trim();
2488
+ const refMatch = head.match(/^ref:\s*(.+)$/);
2489
+ if (!refMatch) return head.length === 40 ? head : null;
2490
+ const refPath = join(m[1], "..", refMatch[1]);
2491
+ try {
2492
+ return readFileSync(refPath, "utf-8").trim();
2493
+ } catch {
2494
+ }
2495
+ const r = git(["rev-parse", "HEAD"], worktreePath);
2496
+ return r.ok ? r.stdout : null;
2497
+ } catch {
2498
+ return null;
2499
+ }
2500
+ }
2501
+ function getOrCreateWorktree(repoRoot, slug2) {
2502
+ validateWorktreeSlug(slug2);
2503
+ const worktreePath = worktreePathFor(repoRoot, slug2);
2504
+ const branch = worktreeBranchName(slug2);
2505
+ const existingHead = readWorktreeHead(worktreePath);
2506
+ if (existingHead) return { worktreePath, branch, headCommit: existingHead, existed: true };
2507
+ mkdirSync(worktreesDir(repoRoot), { recursive: true });
2508
+ const defaultBranch = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoRoot);
2509
+ let baseBranch = defaultBranch.ok ? defaultBranch.stdout.replace(/^origin\//, "") : "HEAD";
2510
+ const originRef = `origin/${baseBranch}`;
2511
+ const hasOrigin = git(["rev-parse", "--verify", originRef], repoRoot);
2512
+ if (!hasOrigin.ok) {
2513
+ const fetched = git(["fetch", "origin", baseBranch], repoRoot);
2514
+ if (!fetched.ok) baseBranch = "HEAD";
2515
+ }
2516
+ const base = baseBranch === "HEAD" ? "HEAD" : originRef;
2517
+ const r = git(["worktree", "add", "-B", branch, worktreePath, base], repoRoot);
2518
+ if (!r.ok) throw new Error(`failed to create worktree: ${r.stderr}`);
2519
+ const headSha = git(["rev-parse", "HEAD"], worktreePath);
2520
+ symlinkDirs(repoRoot, worktreePath, ["node_modules", ".bun"]);
2521
+ return { worktreePath, branch, headCommit: headSha.stdout, existed: false, baseBranch };
2522
+ }
2523
+ function symlinkDirs(repoRoot, worktreePath, dirs) {
2524
+ for (const dir of dirs) {
2525
+ const src = join(repoRoot, dir);
2526
+ const dst = join(worktreePath, dir);
2527
+ try {
2528
+ statSync(src);
2529
+ } catch {
2530
+ continue;
2531
+ }
2532
+ try {
2533
+ symlinkSync(src, dst, "dir");
2534
+ } catch (e) {
2535
+ if (e?.code !== "EEXIST") console.error(`[worktree] symlink ${dir}: ${e?.message ?? e}`);
2536
+ }
2537
+ }
2538
+ }
2539
+ function cleanupWorktree(repoRoot, slug2, opts) {
2540
+ validateWorktreeSlug(slug2);
2541
+ const worktreePath = worktreePathFor(repoRoot, slug2);
2542
+ const branch = worktreeBranchName(slug2);
2543
+ try {
2544
+ statSync(worktreePath);
2545
+ } catch {
2546
+ return { removed: false, reason: "not found" };
2547
+ }
2548
+ if (!opts?.force) {
2549
+ const status = git(["status", "--porcelain", "-uno"], worktreePath);
2550
+ if (status.ok && status.stdout) return { removed: false, reason: "uncommitted changes" };
2551
+ }
2552
+ for (const dir of ["node_modules", ".bun"]) {
2553
+ try {
2554
+ rmSync(join(worktreePath, dir));
2555
+ } catch {
2556
+ }
2557
+ }
2558
+ const rm = git(["worktree", "remove", "--force", worktreePath], repoRoot);
2559
+ if (!rm.ok) return { removed: false, reason: rm.stderr };
2560
+ git(["branch", "-D", branch], repoRoot);
2561
+ return { removed: true };
2562
+ }
2563
+ var _session = null;
2564
+ function getWorktreeSession() {
2565
+ return _session;
2566
+ }
2567
+ function enterWorktree(slug2, cwd) {
2568
+ const repoRoot = findGitRoot(cwd);
2569
+ if (!repoRoot) throw new Error("--worktree requires a git repository");
2570
+ const info = getOrCreateWorktree(repoRoot, slug2);
2571
+ process.chdir(info.worktreePath);
2572
+ _session = {
2573
+ originalCwd: cwd,
2574
+ worktreePath: info.worktreePath,
2575
+ slug: slug2,
2576
+ branch: info.branch,
2577
+ headCommit: info.headCommit
2578
+ };
2579
+ return _session;
2580
+ }
2581
+ function exitWorktree() {
2582
+ if (!_session) return null;
2583
+ const repoRoot = findGitRoot(_session.originalCwd);
2584
+ if (!repoRoot) return null;
2585
+ process.chdir(_session.originalCwd);
2586
+ const result = cleanupWorktree(repoRoot, _session.slug);
2587
+ _session = null;
2588
+ return result;
2589
+ }
2590
+
2591
+ // src/subagent.ts
2592
+ init_NodeDiskFilesystem();
2593
+ init_logging();
2594
+ var log3 = forComponent("subagent");
2439
2595
  async function boundedPool(items, limit, fn) {
2440
2596
  const out = new Array(items.length);
2441
2597
  let next = 0;
@@ -2448,13 +2604,86 @@ async function boundedPool(items, limit, fn) {
2448
2604
  await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
2449
2605
  return out;
2450
2606
  }
2607
+ function sanitizeSlug(raw) {
2608
+ return raw.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/-+/g, "-").slice(0, 40);
2609
+ }
2610
+ function createWorktreeFs(cwd, label, index) {
2611
+ const repoRoot = findGitRoot(cwd);
2612
+ if (!repoRoot) return 'isolation: "worktree" requires a git repository';
2613
+ const slug2 = `sub-${index}-${sanitizeSlug(label)}`;
2614
+ try {
2615
+ const info = getOrCreateWorktree(repoRoot, slug2);
2616
+ return { repoRoot, slug: slug2, worktreePath: info.worktreePath, branch: info.branch };
2617
+ } catch (e) {
2618
+ return `worktree creation failed: ${e instanceof Error ? e.message : String(e)}`;
2619
+ }
2620
+ }
2621
+ function releaseWorktree(handle) {
2622
+ return cleanupWorktree(handle.repoRoot, handle.slug, { force: true });
2623
+ }
2624
+ function worktreePromptPrefix(branch) {
2625
+ return `You are running in an isolated git worktree on branch "${branch}". When you finish making changes, commit them with a clear message before replying with your summary. Your worktree will be removed after you finish \u2014 only committed work survives (on the branch).
2626
+
2627
+ `;
2628
+ }
2629
+ var traceSeq = 0;
2630
+ function traceFileFor(dir, label, depth) {
2631
+ try {
2632
+ mkdirSync2(dir, { recursive: true });
2633
+ const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
2634
+ return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
2635
+ } catch (e) {
2636
+ log3.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
2637
+ return void 0;
2638
+ }
2639
+ }
2640
+ function flushTrace(file, meta, messages, status) {
2641
+ try {
2642
+ writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
2643
+ } catch (e) {
2644
+ log3.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
2645
+ }
2646
+ }
2647
+ function installTrace(child, file, meta) {
2648
+ const prev = child.options.host;
2649
+ child.options.host = {
2650
+ ...prev,
2651
+ notify: (e) => {
2652
+ if (e.kind === "turn_start") flushTrace(file, meta, child.transcript, "running");
2653
+ prev?.notify?.(e);
2654
+ }
2655
+ };
2656
+ }
2657
+ function abortHint(label, res, file) {
2658
+ const last = (res.text || "").trim().slice(0, 500);
2659
+ const lines = [
2660
+ `[sub-task '${label}' did NOT finish \u2014 ${res.finishReason} after ${res.steps} step(s). Treat its work as INCOMPLETE.]`,
2661
+ last ? `Progress so far: ${last}` : "It produced no summary before stopping."
2662
+ ];
2663
+ if (file) lines.push(`Full transcript (every step it took): ${file} \u2014 Read it to see what was actually done.`);
2664
+ lines.push("Decide: ignore it, re-delegate a fresh Task that continues from the trace, or start over. Do NOT assume the sub-task completed.");
2665
+ return lines.join("\n");
2666
+ }
2667
+ async function runChildTraced(args) {
2668
+ const { opts, label, agentType, prompt } = args;
2669
+ if (args.signal) args.childOpts.signal = args.signal;
2670
+ const file = opts.traceDir ? traceFileFor(opts.traceDir, label, args.childDepth) : void 0;
2671
+ const meta = { label, agentType, prompt };
2672
+ const child = new Agent(args.childOpts);
2673
+ if (file) installTrace(child, file, meta);
2674
+ const res = await child.run(prompt);
2675
+ if (file) flushTrace(file, meta, child.transcript, res.finishReason);
2676
+ const result = res.finishReason === "stop" ? res.text || `(child '${label}' finished with no summary; finishReason=${res.finishReason})` : abortHint(label, res, file);
2677
+ await opts.hooks?.onSubagentStop?.(result, { label, agentType });
2678
+ return { res, result };
2679
+ }
2451
2680
  function childOptionsFor(opts, fs, depth, maxDepth, agentType) {
2452
2681
  let def;
2453
2682
  if (agentType) {
2454
2683
  def = (opts.agents ?? []).find((a) => a.name === agentType);
2455
2684
  if (!def) return `no subagent type '${agentType}'. Available: ${(opts.agents ?? []).map((a) => a.name).join(", ") || "(none defined)"}`;
2456
2685
  }
2457
- const childOpts = { ai: opts.ai, fs, model: def?.model ?? opts.model, subagents: true, depth: depth + 1, maxDepth };
2686
+ const childOpts = { ai: opts.ai, fs, model: def?.model ?? opts.model, subagents: true, depth: depth + 1, maxDepth, traceDir: opts.traceDir };
2458
2687
  if (opts.maxSteps != null) childOpts.maxSteps = opts.maxSteps;
2459
2688
  if (def?.systemPrompt) childOpts.systemPrompt = def.systemPrompt;
2460
2689
  if (def?.tools?.length) {
@@ -2471,7 +2700,7 @@ function makeTaskTool(opts) {
2471
2700
  const maxDepth = opts.maxDepth ?? 2;
2472
2701
  return {
2473
2702
  name: "Task",
2474
- description: "Delegate a self-contained sub-task to a child agent over the same filesystem. It runs autonomously with its own step budget and returns a concise summary \u2014 use to isolate context-heavy work (broad search, a scoped refactor). Provide a short `description` and a full `prompt`. Set `background: true` to run it detached (returns a job id to poll with JobOutput while you keep working); its file edits are overlay-isolated and commit when it finishes.",
2703
+ description: 'Delegate a self-contained sub-task to a child agent over the same filesystem. It runs autonomously with its own step budget and returns a concise summary \u2014 use to isolate context-heavy work (broad search, a scoped refactor). Provide a short `description` and a full `prompt`. If it is interrupted or fails, the result is a STATUS HINT with a pointer to its full transcript (Read it to recover what it did, then decide whether to continue or restart) \u2014 do not assume an incomplete sub-task succeeded. Set `background: true` to run it detached (returns a job id to poll with JobOutput while you keep working); its file edits are overlay-isolated and commit when it finishes. Set `isolation: "worktree"` to run in a real git worktree (needed when the child uses real shell commands that must see/modify actual files) \u2014 slower (~200ms setup), auto-cleaned if no changes.',
2475
2704
  parameters: {
2476
2705
  type: "object",
2477
2706
  required: ["description", "prompt"],
@@ -2479,36 +2708,83 @@ function makeTaskTool(opts) {
2479
2708
  description: { type: "string", description: "a short (3-5 word) label for the sub-task" },
2480
2709
  prompt: { type: "string", description: "the full instructions the child agent should carry out" },
2481
2710
  agentType: { type: "string", description: "optional named subagent type (its persona, model, and scoped tools) \u2014 see the catalog" },
2482
- background: { type: "boolean", description: "run detached (overlay-isolated, commits on finish); returns a job id to poll with JobOutput instead of blocking" }
2711
+ background: { type: "boolean", description: "run detached (overlay-isolated, commits on finish); returns a job id to poll with JobOutput instead of blocking" },
2712
+ isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode: "overlay" (default, VFS layer) or "worktree" (real git worktree for shell-heavy work)' }
2483
2713
  }
2484
2714
  },
2485
- async run({ description, prompt, agentType, background }, ctx) {
2715
+ async run({ description, prompt, agentType, background, isolation }, ctx) {
2486
2716
  if (depth >= maxDepth) {
2487
2717
  return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn another child agent \u2014 do this work directly instead.`;
2488
2718
  }
2489
2719
  const label = String(description ?? agentType ?? "sub-task");
2720
+ const useWorktree = isolation === "worktree";
2490
2721
  if (background && ctx.jobs) {
2491
2722
  const id = ctx.jobs.start(async ({ signal }) => {
2492
- const overlay = new OverlayFilesystem(opts.fs);
2493
- const childOpts2 = childOptionsFor(opts, overlay, depth, maxDepth, agentType);
2723
+ let fs2;
2724
+ let commit;
2725
+ let wtHandle2;
2726
+ if (useWorktree) {
2727
+ const h = createWorktreeFs(process.cwd(), label, 0);
2728
+ if (typeof h === "string") throw new Error(h);
2729
+ wtHandle2 = h;
2730
+ fs2 = new NodeDiskFilesystem(h.worktreePath);
2731
+ commit = async () => {
2732
+ };
2733
+ } else {
2734
+ const overlay = new OverlayFilesystem(opts.fs);
2735
+ fs2 = overlay;
2736
+ commit = () => overlay.commit();
2737
+ }
2738
+ const childOpts2 = childOptionsFor(opts, fs2, depth, maxDepth, agentType);
2494
2739
  if (typeof childOpts2 === "string") throw new Error(childOpts2);
2495
- childOpts2.signal = signal;
2496
- const res2 = await new Agent(childOpts2).run(String(prompt ?? ""));
2497
- if (signal.aborted) return "[killed before commit]";
2498
- await overlay.commit();
2499
- const summary2 = res2.text || `(child '${label}' finished with no summary; finishReason=${res2.finishReason})`;
2500
- await opts.hooks?.onSubagentStop?.(summary2, { label, agentType });
2501
- return summary2;
2740
+ const childPrompt = wtHandle2 ? worktreePromptPrefix(wtHandle2.branch) + String(prompt ?? "") : String(prompt ?? "");
2741
+ const { res, result } = await runChildTraced({ opts, childOpts: childOpts2, prompt: childPrompt, label, agentType, childDepth: depth + 1, signal });
2742
+ if (res.finishReason === "aborted" || signal.aborted) {
2743
+ wtHandle2 && releaseWorktree(wtHandle2);
2744
+ return result;
2745
+ }
2746
+ await commit();
2747
+ let summary = result;
2748
+ if (wtHandle2) {
2749
+ const r = releaseWorktree(wtHandle2);
2750
+ if (!r.removed) summary += `
2751
+
2752
+ [worktree removal failed: ${r.reason}]`;
2753
+ }
2754
+ return summary;
2502
2755
  }, { kind: "agent", label });
2503
2756
  return `Started background sub-task ${id} ('${label}') \u2014 poll with JobOutput({id:"${id}"}).`;
2504
2757
  }
2505
- const childOpts = childOptionsFor(opts, opts.fs, depth, maxDepth, agentType);
2506
- if (typeof childOpts === "string") return `Error: ${childOpts}`;
2507
- const child = new Agent(childOpts);
2508
- const res = await child.run(String(prompt ?? ""));
2509
- const summary = res.text || `(child agent finished '${label}' with no summary; finishReason=${res.finishReason})`;
2510
- await opts.hooks?.onSubagentStop?.(summary, { label, agentType });
2511
- return summary;
2758
+ let fs;
2759
+ let wtHandle;
2760
+ if (useWorktree) {
2761
+ const h = createWorktreeFs(process.cwd(), label, 0);
2762
+ if (typeof h === "string") return `Error: ${h}`;
2763
+ wtHandle = h;
2764
+ fs = new NodeDiskFilesystem(h.worktreePath);
2765
+ } else {
2766
+ fs = opts.fs;
2767
+ }
2768
+ const childOpts = childOptionsFor(opts, fs, depth, maxDepth, agentType);
2769
+ if (typeof childOpts === "string") {
2770
+ wtHandle && releaseWorktree(wtHandle);
2771
+ return `Error: ${childOpts}`;
2772
+ }
2773
+ try {
2774
+ const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(prompt ?? "") : String(prompt ?? "");
2775
+ const { result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType, childDepth: depth + 1, signal: ctx.signal });
2776
+ let summary = result;
2777
+ if (wtHandle) {
2778
+ const r = releaseWorktree(wtHandle);
2779
+ if (!r.removed) summary += `
2780
+
2781
+ [worktree removal failed: ${r.reason}]`;
2782
+ }
2783
+ return summary;
2784
+ } catch (e) {
2785
+ if (wtHandle) releaseWorktree(wtHandle);
2786
+ throw e;
2787
+ }
2512
2788
  }
2513
2789
  };
2514
2790
  }
@@ -2518,7 +2794,7 @@ function makeTaskBatchTool(opts) {
2518
2794
  const maxParallel = opts.maxParallel ?? 4;
2519
2795
  return {
2520
2796
  name: "TaskBatch",
2521
- description: "Delegate SEVERAL independent sub-tasks to child agents that run concurrently; returns all their summaries. Each child is write-isolated (its file edits are merged back in array order). Use for parallel fan-out (review/search/scoped refactors across files). Provide `tasks: [{ description, prompt, agentType? }]`.",
2797
+ description: 'Delegate SEVERAL independent sub-tasks to child agents that run concurrently; returns all their summaries. Each child is write-isolated (its file edits are merged back in array order). Use for parallel fan-out (review/search/scoped refactors across files). Provide `tasks: [{ description, prompt, agentType? }]`. Set `isolation: "worktree"` to run each child in its own git worktree (needed for real shell commands) \u2014 slower, auto-cleaned if no changes.',
2522
2798
  parameters: {
2523
2799
  type: "object",
2524
2800
  required: ["tasks"],
@@ -2535,30 +2811,53 @@ function makeTaskBatchTool(opts) {
2535
2811
  agentType: { type: "string", description: "optional named subagent type" }
2536
2812
  }
2537
2813
  }
2538
- }
2814
+ },
2815
+ isolation: { type: "string", enum: ["overlay", "worktree"], description: 'isolation mode for ALL children: "overlay" (default) or "worktree" (real git worktree each)' }
2539
2816
  }
2540
2817
  },
2541
- async run({ tasks }, _ctx) {
2818
+ async run({ tasks, isolation }, ctx) {
2542
2819
  if (depth >= maxDepth) return `Error: Task depth limit reached (maxDepth ${maxDepth}). Cannot spawn child agents \u2014 do this work directly instead.`;
2543
2820
  const list = Array.isArray(tasks) ? tasks : [];
2544
2821
  if (!list.length) return "Error: TaskBatch needs a non-empty `tasks` array.";
2822
+ const useWorktree = isolation === "worktree";
2545
2823
  const results = await boundedPool(list, maxParallel, async (t, i) => {
2546
2824
  const label = String(t?.description ?? t?.agentType ?? `task ${i + 1}`);
2547
- const overlay = new OverlayFilesystem(opts.fs);
2548
- const childOpts = childOptionsFor(opts, overlay, depth, maxDepth, t?.agentType);
2825
+ let fs;
2826
+ let overlay;
2827
+ let wtHandle;
2828
+ if (useWorktree) {
2829
+ const h = createWorktreeFs(process.cwd(), label, i);
2830
+ if (typeof h === "string") return { label, error: h, ok: false };
2831
+ wtHandle = h;
2832
+ fs = new NodeDiskFilesystem(h.worktreePath);
2833
+ } else {
2834
+ overlay = new OverlayFilesystem(opts.fs);
2835
+ fs = overlay;
2836
+ }
2837
+ const childOpts = childOptionsFor(opts, fs, depth, maxDepth, t?.agentType);
2549
2838
  if (typeof childOpts === "string") return { label, error: childOpts, ok: false };
2550
2839
  try {
2551
- const res = await new Agent(childOpts).run(String(t?.prompt ?? ""));
2552
- await opts.hooks?.onSubagentStop?.(res.text, { label, agentType: t?.agentType });
2553
- return { label, text: res.text, overlay, ok: res.finishReason !== "error" };
2840
+ const childPrompt = wtHandle ? worktreePromptPrefix(wtHandle.branch) + String(t?.prompt ?? "") : String(t?.prompt ?? "");
2841
+ const { res, result } = await runChildTraced({ opts, childOpts, prompt: childPrompt, label, agentType: t?.agentType, childDepth: depth + 1, signal: ctx.signal });
2842
+ return { label, text: result, overlay, wtHandle, ok: res.finishReason !== "error" && res.finishReason !== "aborted" };
2554
2843
  } catch (e) {
2555
- return { label, error: e instanceof Error ? e.message : String(e), ok: false };
2844
+ return { label, error: e instanceof Error ? e.message : String(e), wtHandle, ok: false };
2556
2845
  }
2557
2846
  });
2558
- for (const r of results) if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
2559
- });
2560
- return results.map((r, i) => `### ${i + 1}. ${r.label}
2561
- ${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}`).join("\n\n");
2847
+ const lines = [];
2848
+ for (const r of results) {
2849
+ if (r.ok && r.overlay) await r.overlay.commit().catch(() => {
2850
+ });
2851
+ let extra = "";
2852
+ if (r.wtHandle) {
2853
+ const wr = releaseWorktree(r.wtHandle);
2854
+ if (!wr.removed) extra = `
2855
+ [worktree removal failed: ${wr.reason}]`;
2856
+ }
2857
+ lines.push(`### ${lines.length + 1}. ${r.label}
2858
+ ${r.error ? `ERROR: ${r.error}` : r.text || "(no summary)"}${extra}`);
2859
+ }
2860
+ return lines.join("\n\n");
2562
2861
  }
2563
2862
  };
2564
2863
  }
@@ -2803,7 +3102,7 @@ function reasoningToChatFragment(model, effort) {
2803
3102
  }
2804
3103
 
2805
3104
  // src/Agent.ts
2806
- var log3 = forComponent("Agent");
3105
+ var log4 = forComponent("Agent");
2807
3106
  function isAbortError(err) {
2808
3107
  const e = err;
2809
3108
  const blob = `${e?.message ?? ""} ${e?.name ?? ""} ${e?.code ?? ""} ${e?.cause?.name ?? ""}`;
@@ -2879,6 +3178,11 @@ var AgentOptions = class {
2879
3178
  depth = 0;
2880
3179
  /** Hard ceiling on subagent nesting (beyond it the `Task` tool refuses to spawn). */
2881
3180
  maxDepth = 2;
3181
+ /** Real-disk dir where child agents stream their transcript as they run (origin-anchored — survives
3182
+ * worktree teardown / a child's own cwd). When set, an interrupted/failed child commits a STATUS HINT
3183
+ * + a pointer to its trace instead of a dangling tool_call, so the parent can read it and decide
3184
+ * whether to ignore, pick it up, or restart. Unset (library/sandbox default) = no tracing. */
3185
+ traceDir;
2882
3186
  /** Stream tokens from the model. Takes effect only with a `host.notify`; off => current (non-stream) behavior. */
2883
3187
  stream = false;
2884
3188
  /** Fold the dropped middle of an over-long transcript into a synthetic summary (edge-safe, no LLM). Off => drop-oldest. */
@@ -2999,7 +3303,7 @@ var Agent = class _Agent {
2999
3303
  const disk = new NodeDiskFilesystem2(process.cwd());
3000
3304
  await disk.init();
3001
3305
  this.options.fs = new JailedFilesystem2(disk);
3002
- log3.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3306
+ log4.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3003
3307
  }
3004
3308
  this.buildCtx();
3005
3309
  }
@@ -3053,7 +3357,7 @@ var Agent = class _Agent {
3053
3357
  agents = loaded.agents;
3054
3358
  if (loaded.catalog) systemPrompt += "\n\n" + loaded.catalog;
3055
3359
  }
3056
- const taskOpts = { ai: o.ai, model: o.model, fs, depth: o.depth, maxDepth: o.maxDepth, agents, hooks: o.hooks };
3360
+ const taskOpts = { ai: o.ai, model: o.model, fs, depth: o.depth, maxDepth: o.maxDepth, agents, hooks: o.hooks, traceDir: o.traceDir };
3057
3361
  tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
3058
3362
  }
3059
3363
  if (o.checkpoints) tools = [...tools, ...checkpointTools()];
@@ -3142,7 +3446,7 @@ var Agent = class _Agent {
3142
3446
  let lastFp = "";
3143
3447
  let repeats = 0;
3144
3448
  const kill = (finishReason) => {
3145
- log3.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
3449
+ log4.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
3146
3450
  this.ctx.jobs?.killAll();
3147
3451
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated };
3148
3452
  };
@@ -3188,7 +3492,7 @@ var Agent = class _Agent {
3188
3492
  const transient = !o.signal?.aborted && !isAbortError(err) && attempt < 2 && (network || serverSide);
3189
3493
  if (!transient) throw err;
3190
3494
  const waitMs = 1e3 * (attempt + 1);
3191
- log3.warn(`network drop mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
3495
+ log4.warn(`network drop mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
3192
3496
  o.host?.notify?.({ kind: "retry", message: `connection dropped \u2014 retrying step (#${attempt + 1})` });
3193
3497
  await new Promise((r) => setTimeout(r, waitMs));
3194
3498
  }
@@ -3204,7 +3508,7 @@ var Agent = class _Agent {
3204
3508
  bodyStr = void 0;
3205
3509
  }
3206
3510
  if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
3207
- log3.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err);
3511
+ log4.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, err);
3208
3512
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
3209
3513
  }
3210
3514
  if (o.signal?.aborted) return kill("aborted");
@@ -3231,7 +3535,7 @@ var Agent = class _Agent {
3231
3535
  });
3232
3536
  }
3233
3537
  if (toolCalls.length === 0) {
3234
- log3.verbose(`completed in ${steps} step(s)`);
3538
+ log4.verbose(`completed in ${steps} step(s)`);
3235
3539
  await this.ctx.jobs?.drain();
3236
3540
  await this.activeHooks?.onStop?.(res.content ?? "");
3237
3541
  return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
@@ -3297,13 +3601,13 @@ var Agent = class _Agent {
3297
3601
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
3298
3602
  if (decision?.block) {
3299
3603
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
3300
- log3.debug(`${tc.function.name} -> ${blocked}`);
3604
+ log4.debug(`${tc.function.name} -> ${blocked}`);
3301
3605
  await hooks?.postToolUse?.(call, blocked, meta);
3302
3606
  return blocked;
3303
3607
  }
3304
3608
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
3305
3609
  if (earlyError) {
3306
- log3.debug(`${tc.function.name} -> ${earlyError}`);
3610
+ log4.debug(`${tc.function.name} -> ${earlyError}`);
3307
3611
  await hooks?.postToolUse?.(call, earlyError, meta);
3308
3612
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
3309
3613
  return earlyError;
@@ -3312,12 +3616,12 @@ var Agent = class _Agent {
3312
3616
  let images;
3313
3617
  let threw = false;
3314
3618
  try {
3315
- log3.debug(`${tc.function.name}(${tc.function.arguments})`);
3619
+ log4.debug(`${tc.function.name}(${tc.function.arguments})`);
3316
3620
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
3317
3621
  try {
3318
3622
  hooks.onToolOutput(call, chunk, meta);
3319
3623
  } catch (e) {
3320
- log3.debug(`onToolOutput hook error: ${e}`);
3624
+ log4.debug(`onToolOutput hook error: ${e}`);
3321
3625
  }
3322
3626
  } : void 0;
3323
3627
  const raw = await tool.run(args, this.ctx);
@@ -3329,7 +3633,7 @@ var Agent = class _Agent {
3329
3633
  }
3330
3634
  } catch (e) {
3331
3635
  const msg = e instanceof Error ? e.message : String(e);
3332
- log3.debug(`${tc.function.name} -> error: ${msg}`);
3636
+ log4.debug(`${tc.function.name} -> error: ${msg}`);
3333
3637
  result = `Error: ${msg}`;
3334
3638
  threw = true;
3335
3639
  } finally {
@@ -3455,7 +3759,7 @@ function fitTokenBudget(messages, maxTokens) {
3455
3759
  const ids = callIdSet(messages.slice(from));
3456
3760
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
3457
3761
  if (total > maxTokens)
3458
- log3.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3762
+ log4.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
3459
3763
  return [...head, ...messages.slice(from)];
3460
3764
  }
3461
3765
  function compact(m, max, focus) {
@@ -4116,7 +4420,7 @@ init_tools_web();
4116
4420
  init_tools();
4117
4421
  init_tools_structured();
4118
4422
  init_logging();
4119
- var log5 = forComponent("scratch");
4423
+ var log6 = forComponent("scratch");
4120
4424
  var SCRATCH_DIR = "/scratch";
4121
4425
  function shortArgs(args) {
4122
4426
  try {
@@ -4156,7 +4460,7 @@ var Scratch = class {
4156
4460
  await (this.dirReady ??= mkdirp(this.fs, dir));
4157
4461
  await this.fs.writeFile(path, header + raw);
4158
4462
  } catch (e) {
4159
- log5.debug("scratch write failed; returning raw", e);
4463
+ log6.debug("scratch write failed; returning raw", e);
4160
4464
  return raw;
4161
4465
  }
4162
4466
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -4186,7 +4490,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
4186
4490
  await (this.dirReady ??= mkdirp(this.fs, dir));
4187
4491
  await this.fs.writeFile(path, header + full);
4188
4492
  } catch (e) {
4189
- log5.debug("scratch spill failed; cropping lossy", e);
4493
+ log6.debug("scratch spill failed; cropping lossy", e);
4190
4494
  return full.slice(0, pageBytes) + `
4191
4495
 
4192
4496
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -4232,7 +4536,7 @@ Question: ${q}`);
4232
4536
  const answer = (res.text ?? "").trim();
4233
4537
  return answer || "(no answer found in scratch)";
4234
4538
  } catch (e) {
4235
- log5.debug("Ask peek failed", e);
4539
+ log6.debug("Ask peek failed", e);
4236
4540
  return `Error querying scratch: ${e?.message ?? e}`;
4237
4541
  }
4238
4542
  }
@@ -4241,7 +4545,7 @@ Question: ${q}`);
4241
4545
 
4242
4546
  // src/lessons.ts
4243
4547
  init_logging();
4244
- var log6 = forComponent("Lessons");
4548
+ var log7 = forComponent("Lessons");
4245
4549
  var LessonOptionsDefaults = class {
4246
4550
  minRepeats = 2;
4247
4551
  };
@@ -4266,15 +4570,15 @@ function lessonCapture(options) {
4266
4570
  counts.set(lesson.slug, n);
4267
4571
  if (n < o.minRepeats) return;
4268
4572
  written.add(lesson.slug);
4269
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log6.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
4270
- log6.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
4573
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log7.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
4574
+ log7.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
4271
4575
  }
4272
4576
  };
4273
4577
  }
4274
4578
 
4275
4579
  // src/reflect.ts
4276
4580
  init_logging();
4277
- var log7 = forComponent("Reflect");
4581
+ var log8 = forComponent("Reflect");
4278
4582
  async function reflectOnRun(o) {
4279
4583
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
4280
4584
  if (!digest.trim()) return null;
@@ -4290,7 +4594,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4290
4594
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
4291
4595
  text = r?.content ?? "";
4292
4596
  } catch (e) {
4293
- log7.warn(`reflection call failed: ${e?.message ?? e}`);
4597
+ log8.warn(`reflection call failed: ${e?.message ?? e}`);
4294
4598
  return null;
4295
4599
  }
4296
4600
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -4300,10 +4604,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4300
4604
  try {
4301
4605
  await writeFact(o.fs, o.dir, slug2, lesson);
4302
4606
  } catch (e) {
4303
- log7.warn(`could not persist lesson: ${e?.message ?? e}`);
4607
+ log8.warn(`could not persist lesson: ${e?.message ?? e}`);
4304
4608
  return null;
4305
4609
  }
4306
- log7.debug(`reflection persisted ${slug2}`);
4610
+ log8.debug(`reflection persisted ${slug2}`);
4307
4611
  return slug2;
4308
4612
  }
4309
4613
  function digestRun(messages, maxChars) {
@@ -4320,7 +4624,7 @@ function digestRun(messages, maxChars) {
4320
4624
  // src/duplex.ts
4321
4625
  import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
4322
4626
  init_logging();
4323
- var log8 = forComponent("DuplexAgent");
4627
+ var log9 = forComponent("DuplexAgent");
4324
4628
  function describeCall(call) {
4325
4629
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
4326
4630
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -4460,7 +4764,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
4460
4764
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER);
4461
4765
  if (m) {
4462
4766
  this.fabricationCut = true;
4463
- log8.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
4767
+ log9.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
4464
4768
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
4465
4769
  if (!safe) return;
4466
4770
  if (safe.trim()) this.spokeThisTurn = true;
@@ -4527,23 +4831,28 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
4527
4831
  }
4528
4832
  };
4529
4833
  }
4530
- /** True when the just-finished turn dispatched a task but voiced nothing — dead air to repair.
4834
+ /** True when the just-finished turn voiced NOTHING — dead air to repair. Two ways this happens:
4835
+ * (a) a dispatch with no spoken ack, and (b) an INLINE turn whose `final` channel came back empty —
4836
+ * gpt-oss harmony sometimes puts the whole reply in `analysis` (→ thinking_delta, suppressed in
4837
+ * voice) and emits an empty `final`, so no text_delta ever streams. Both ship silence; both repair.
4531
4838
  * Requires a host: without one there's no stream to detect speech on (and no one to speak to). */
4532
- get silentDispatch() {
4533
- return !!this.options.host && this.turnDispatched && !this.spokeThisTurn;
4839
+ get silentTurn() {
4840
+ return !!this.options.host && !this.spokeThisTurn;
4534
4841
  }
4535
- /** A dispatch with no spoken text is dead air. Re-prompt the reflex ONCE so the LLM itself voices a
4536
- * short ack (no template). If it STILL says nothing, fall back to a minimal line so silence never ships. */
4842
+ /** A turn that voiced nothing is dead air. Re-prompt the reflex ONCE so the LLM itself voices a short
4843
+ * line (no template). If it STILL says nothing, fall back to a minimal line so silence never ships.
4844
+ * Wording adapts to whether work was dispatched (an ack) or the inline reply was simply lost. */
4537
4845
  async ackIfSilent() {
4846
+ const dispatched = this.turnDispatched;
4538
4847
  this.nudging = true;
4539
4848
  try {
4540
- await this.voice.send("[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools.");
4849
+ await this.voice.send(dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
4541
4850
  } catch (e) {
4542
- log8.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
4851
+ log9.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
4543
4852
  } finally {
4544
4853
  this.nudging = false;
4545
4854
  }
4546
- if (!this.spokeThisTurn) this.options.host?.notify?.({ kind: "text_delta", message: "Okay, on it." });
4855
+ if (!this.spokeThisTurn) this.options.host?.notify?.({ kind: "text_delta", message: dispatched ? "Okay, on it." : "Sorry, could you say that again?" });
4547
4856
  }
4548
4857
  /** One user turn: the voice agent streams the reply (and may Act/Think). Serialized with re-voice turns. */
4549
4858
  send(content) {
@@ -4551,7 +4860,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
4551
4860
  await this.initMemory();
4552
4861
  this.resetTurn();
4553
4862
  const res = await this.voice.send(content);
4554
- if (this.silentDispatch) await this.ackIfSilent();
4863
+ if (this.silentTurn) await this.ackIfSilent();
4555
4864
  return res;
4556
4865
  });
4557
4866
  }
@@ -4596,7 +4905,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
4596
4905
  if (!events.length) return;
4597
4906
  this.resetTurn();
4598
4907
  await this.voice.send(events.join("\n"));
4599
- if (this.silentDispatch) await this.ackIfSilent();
4908
+ if (this.silentTurn) await this.ackIfSilent();
4600
4909
  this.notify("revoice_done", "");
4601
4910
  });
4602
4911
  }
@@ -4687,7 +4996,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4687
4996
  this.notify("task_verify", `task ${id}: verifying`, { id });
4688
4997
  const cres = await new Agent(agentOpts).run(checkBrief);
4689
4998
  if (cres.finishReason !== "stop") {
4690
- log8.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
4999
+ log9.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
4691
5000
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
4692
5001
  }
4693
5002
  const sum = (a = 0, b = 0) => a + b;
@@ -4790,7 +5099,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4790
5099
  }
4791
5100
  rec.status = "done";
4792
5101
  rec.result = res.text;
4793
- log8.verbose(`task ${id} done (${res.steps} steps)`);
5102
+ log9.verbose(`task ${id} done (${res.steps} steps)`);
4794
5103
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
4795
5104
  id,
4796
5105
  text: res.text,
@@ -4808,7 +5117,7 @@ Another agent just implemented the above. Independently check the CURRENT state
4808
5117
  this.dropAsk(rec.id);
4809
5118
  rec.status = "error";
4810
5119
  rec.result = msg;
4811
- log8.warn(`task ${rec.id} failed: ${msg}`);
5120
+ log9.warn(`task ${rec.id} failed: ${msg}`);
4812
5121
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
4813
5122
  this.queueRevoice(`[task ${rec.id} failed] ${msg}`);
4814
5123
  }
@@ -5168,7 +5477,7 @@ init_logging();
5168
5477
 
5169
5478
  // src/voice/engine.ts
5170
5479
  init_logging();
5171
- var log9 = forComponent("VoiceEngine");
5480
+ var log10 = forComponent("VoiceEngine");
5172
5481
  var now = () => performance.now();
5173
5482
  var forSpeech = (t) => t.replace(/[*_`#]+/g, "").replace(/^[ \t]*[-•]\s+/gm, "").replace(/\s*[\u2013\u2014]\s*/g, ", ").replace(/[\u2010\u2011]/g, "-").replace(/\s*\|\s*/g, ", ").replace(/(\d)\s+%/g, "$1%").replace(/\.{3,}/g, ".");
5174
5483
  var VoiceEngineOptions = class {
@@ -5276,7 +5585,7 @@ var VoiceEngine = class _VoiceEngine {
5276
5585
  this.stt.onLevel = (rms) => this.handleLevel(rms);
5277
5586
  await Promise.all([this.tts.connect(), this.stt.start()]);
5278
5587
  this.setState("listening");
5279
- log9.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5588
+ log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5280
5589
  }
5281
5590
  get usingAec() {
5282
5591
  return this.stt.usingAec;
@@ -5328,7 +5637,7 @@ var VoiceEngine = class _VoiceEngine {
5328
5637
  this.reply += text;
5329
5638
  for (const w of this.words(this.reply)) this.echoWords.add(w);
5330
5639
  this.tts.speak(forSpeech(text), true);
5331
- if (!this.spokeDeltas && this.turnStartAt) log9.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
5640
+ if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
5332
5641
  this.spokeDeltas = true;
5333
5642
  this.setState("speaking");
5334
5643
  }
@@ -5349,7 +5658,7 @@ var VoiceEngine = class _VoiceEngine {
5349
5658
  }
5350
5659
  this.drainTimer = null;
5351
5660
  this.speaking = false;
5352
- if (this.turnStartAt) log9.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
5661
+ if (this.turnStartAt) log10.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
5353
5662
  this.echoUntil = now() + 2500;
5354
5663
  if (!this.usingAec) this.stt.reset();
5355
5664
  this.setState("listening");
@@ -5490,7 +5799,7 @@ var VoiceEngine = class _VoiceEngine {
5490
5799
  this.pendingUtt = this.pendingUtt ? `${this.pendingUtt} ${text}` : text;
5491
5800
  if (this.pendingTimer) clearTimeout(this.pendingTimer);
5492
5801
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
5493
- log9.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
5802
+ log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
5494
5803
  this.options.onHold();
5495
5804
  if (this.options.holdFiller && !this.speaking) {
5496
5805
  this.beginSpeech();
@@ -5588,7 +5897,7 @@ async function resolveAuth(auth) {
5588
5897
  }
5589
5898
 
5590
5899
  // src/voice/soniox.ts
5591
- var log10 = forComponent("SonioxSTT");
5900
+ var log11 = forComponent("SonioxSTT");
5592
5901
  var now2 = () => performance.now();
5593
5902
  var SonioxSTTOptions = class {
5594
5903
  auth = "";
@@ -5645,9 +5954,9 @@ var SonioxSTT = class {
5645
5954
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
5646
5955
  this.ws.onclose = (ev) => {
5647
5956
  if (this.stopped) return;
5648
- log10.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
5957
+ log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
5649
5958
  this.reset();
5650
- this.connectWs().catch((e) => log10.error(`soniox reconnect failed: ${e.message}`));
5959
+ this.connectWs().catch((e) => log11.error(`soniox reconnect failed: ${e.message}`));
5651
5960
  };
5652
5961
  }
5653
5962
  async start() {
@@ -5657,7 +5966,7 @@ var SonioxSTT = class {
5657
5966
  this.endpointTimer = setInterval(() => {
5658
5967
  const combined = (this.finalText + this.partialText).trim();
5659
5968
  if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
5660
- if (this.firstTokenAt) log10.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
5969
+ if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
5661
5970
  this.reset();
5662
5971
  this.onUtterance(combined, now2());
5663
5972
  }, 120);
@@ -5674,7 +5983,7 @@ var SonioxSTT = class {
5674
5983
  });
5675
5984
  }
5676
5985
  handle(m) {
5677
- if (m.error_message) return log10.error(`soniox: ${m.error_message}`);
5986
+ if (m.error_message) return log11.error(`soniox: ${m.error_message}`);
5678
5987
  let endpoint = false;
5679
5988
  for (const t of m.tokens ?? []) {
5680
5989
  if (t.text === "<end>") endpoint = true;
@@ -5690,7 +5999,7 @@ var SonioxSTT = class {
5690
5999
  this.onPartial(combined);
5691
6000
  if (endpoint && this.finalText.trim()) {
5692
6001
  const utterance = this.finalText.trim();
5693
- if (this.firstTokenAt) log10.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6002
+ if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
5694
6003
  this.reset();
5695
6004
  this.onUtterance(utterance, now2());
5696
6005
  }
@@ -5712,7 +6021,7 @@ var SonioxSTT = class {
5712
6021
 
5713
6022
  // src/voice/cartesia.ts
5714
6023
  init_logging();
5715
- var log11 = forComponent("CartesiaTTS");
6024
+ var log12 = forComponent("CartesiaTTS");
5716
6025
  var now3 = () => performance.now();
5717
6026
  var CartesiaTTSOptions = class {
5718
6027
  auth = "";
@@ -5762,9 +6071,9 @@ var CartesiaTTS = class _CartesiaTTS {
5762
6071
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
5763
6072
  });
5764
6073
  this.ws.onclose = (ev) => {
5765
- log11.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6074
+ log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
5766
6075
  if (!this.closed) {
5767
- this.connecting = this.doConnect().catch((e) => log11.error(`cartesia reconnect failed: ${e.message}`));
6076
+ this.connecting = this.doConnect().catch((e) => log12.error(`cartesia reconnect failed: ${e.message}`));
5768
6077
  }
5769
6078
  };
5770
6079
  this.ws.onmessage = (ev) => {
@@ -5786,11 +6095,11 @@ var CartesiaTTS = class _CartesiaTTS {
5786
6095
  this.down = true;
5787
6096
  this.downAt = now3();
5788
6097
  this.consecutiveOk = 0;
5789
- log11.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6098
+ log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
5790
6099
  this.onDone();
5791
6100
  this.startProbe();
5792
6101
  } else if (!this.down) {
5793
- log11.warn(`cartesia: ${JSON.stringify(m)}`);
6102
+ log12.warn(`cartesia: ${JSON.stringify(m)}`);
5794
6103
  }
5795
6104
  }
5796
6105
  };
@@ -5804,7 +6113,7 @@ var CartesiaTTS = class _CartesiaTTS {
5804
6113
  this.consecutiveOk = 0;
5805
6114
  this.stopProbe();
5806
6115
  const downMs = this.downAt ? now3() - this.downAt : 0;
5807
- (downMs < 2e3 ? log11.debug : log11.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
6116
+ (downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
5808
6117
  }
5809
6118
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
5810
6119
  async ensureConnected() {
@@ -5876,155 +6185,6 @@ function base64ToBytes(b64) {
5876
6185
  return out;
5877
6186
  }
5878
6187
 
5879
- // src/worktree.ts
5880
- import { spawnSync } from "child_process";
5881
- import { symlinkSync, mkdirSync, statSync, readFileSync, rmSync } from "fs";
5882
- import { join as join2 } from "path";
5883
- var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
5884
- var MAX_SLUG_LENGTH = 64;
5885
- var GIT_NO_PROMPT_ENV = { GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
5886
- function validateWorktreeSlug(slug2) {
5887
- if (!slug2) throw new Error("worktree slug must not be empty");
5888
- if (slug2.length > MAX_SLUG_LENGTH) throw new Error(`worktree slug must be \u2264${MAX_SLUG_LENGTH} chars (got ${slug2.length})`);
5889
- for (const seg of slug2.split("/")) {
5890
- if (seg === "." || seg === "..") throw new Error(`worktree slug must not contain "." or ".." segments`);
5891
- if (!VALID_SEGMENT.test(seg)) throw new Error(`worktree slug segment "${seg}" contains invalid characters (allowed: a-z A-Z 0-9 . _ -)`);
5892
- }
5893
- }
5894
- function flattenSlug(slug2) {
5895
- return slug2.replaceAll("/", "+");
5896
- }
5897
- function worktreeBranchName(slug2) {
5898
- return `worktree-${flattenSlug(slug2)}`;
5899
- }
5900
- function worktreesDir(repoRoot) {
5901
- return join2(repoRoot, ".claude", "worktrees");
5902
- }
5903
- function worktreePathFor(repoRoot, slug2) {
5904
- return join2(worktreesDir(repoRoot), flattenSlug(slug2));
5905
- }
5906
- function git(args, cwd) {
5907
- const r = spawnSync("git", args, {
5908
- cwd,
5909
- stdio: ["ignore", "pipe", "pipe"],
5910
- env: { ...process.env, ...GIT_NO_PROMPT_ENV }
5911
- });
5912
- return { ok: r.status === 0, stdout: (r.stdout ?? "").toString().trim(), stderr: (r.stderr ?? "").toString().trim() };
5913
- }
5914
- function findGitRoot(from) {
5915
- const r = git(["rev-parse", "--show-toplevel"], from);
5916
- return r.ok ? r.stdout : null;
5917
- }
5918
- function readWorktreeHead(worktreePath) {
5919
- try {
5920
- const gitFile = readFileSync(join2(worktreePath, ".git"), "utf-8").trim();
5921
- const m = gitFile.match(/^gitdir:\s*(.+)$/);
5922
- if (!m) return null;
5923
- const headPath = join2(m[1], "HEAD");
5924
- const head = readFileSync(headPath, "utf-8").trim();
5925
- const refMatch = head.match(/^ref:\s*(.+)$/);
5926
- if (!refMatch) return head.length === 40 ? head : null;
5927
- const refPath = join2(m[1], "..", refMatch[1]);
5928
- try {
5929
- return readFileSync(refPath, "utf-8").trim();
5930
- } catch {
5931
- }
5932
- const r = git(["rev-parse", "HEAD"], worktreePath);
5933
- return r.ok ? r.stdout : null;
5934
- } catch {
5935
- return null;
5936
- }
5937
- }
5938
- function getOrCreateWorktree(repoRoot, slug2) {
5939
- validateWorktreeSlug(slug2);
5940
- const worktreePath = worktreePathFor(repoRoot, slug2);
5941
- const branch = worktreeBranchName(slug2);
5942
- const existingHead = readWorktreeHead(worktreePath);
5943
- if (existingHead) return { worktreePath, branch, headCommit: existingHead, existed: true };
5944
- mkdirSync(worktreesDir(repoRoot), { recursive: true });
5945
- const defaultBranch = git(["symbolic-ref", "--short", "refs/remotes/origin/HEAD"], repoRoot);
5946
- let baseBranch = defaultBranch.ok ? defaultBranch.stdout.replace(/^origin\//, "") : "HEAD";
5947
- const originRef = `origin/${baseBranch}`;
5948
- const hasOrigin = git(["rev-parse", "--verify", originRef], repoRoot);
5949
- if (!hasOrigin.ok) {
5950
- const fetched = git(["fetch", "origin", baseBranch], repoRoot);
5951
- if (!fetched.ok) baseBranch = "HEAD";
5952
- }
5953
- const base = baseBranch === "HEAD" ? "HEAD" : originRef;
5954
- const r = git(["worktree", "add", "-B", branch, worktreePath, base], repoRoot);
5955
- if (!r.ok) throw new Error(`failed to create worktree: ${r.stderr}`);
5956
- const headSha = git(["rev-parse", "HEAD"], worktreePath);
5957
- symlinkDirs(repoRoot, worktreePath, ["node_modules", ".bun"]);
5958
- return { worktreePath, branch, headCommit: headSha.stdout, existed: false, baseBranch };
5959
- }
5960
- function symlinkDirs(repoRoot, worktreePath, dirs) {
5961
- for (const dir of dirs) {
5962
- const src = join2(repoRoot, dir);
5963
- const dst = join2(worktreePath, dir);
5964
- try {
5965
- statSync(src);
5966
- } catch {
5967
- continue;
5968
- }
5969
- try {
5970
- symlinkSync(src, dst, "dir");
5971
- } catch (e) {
5972
- if (e?.code !== "EEXIST") console.error(`[worktree] symlink ${dir}: ${e?.message ?? e}`);
5973
- }
5974
- }
5975
- }
5976
- function cleanupWorktree(repoRoot, slug2, opts) {
5977
- validateWorktreeSlug(slug2);
5978
- const worktreePath = worktreePathFor(repoRoot, slug2);
5979
- const branch = worktreeBranchName(slug2);
5980
- try {
5981
- statSync(worktreePath);
5982
- } catch {
5983
- return { removed: false, reason: "not found" };
5984
- }
5985
- if (!opts?.force) {
5986
- const status = git(["status", "--porcelain", "-uno"], worktreePath);
5987
- if (status.ok && status.stdout) return { removed: false, reason: "uncommitted changes" };
5988
- }
5989
- for (const dir of ["node_modules", ".bun"]) {
5990
- try {
5991
- rmSync(join2(worktreePath, dir));
5992
- } catch {
5993
- }
5994
- }
5995
- const rm = git(["worktree", "remove", "--force", worktreePath], repoRoot);
5996
- if (!rm.ok) return { removed: false, reason: rm.stderr };
5997
- git(["branch", "-D", branch], repoRoot);
5998
- return { removed: true };
5999
- }
6000
- var _session = null;
6001
- function getWorktreeSession() {
6002
- return _session;
6003
- }
6004
- function enterWorktree(slug2, cwd) {
6005
- const repoRoot = findGitRoot(cwd);
6006
- if (!repoRoot) throw new Error("--worktree requires a git repository");
6007
- const info = getOrCreateWorktree(repoRoot, slug2);
6008
- process.chdir(info.worktreePath);
6009
- _session = {
6010
- originalCwd: cwd,
6011
- worktreePath: info.worktreePath,
6012
- slug: slug2,
6013
- branch: info.branch,
6014
- headCommit: info.headCommit
6015
- };
6016
- return _session;
6017
- }
6018
- function exitWorktree() {
6019
- if (!_session) return null;
6020
- const repoRoot = findGitRoot(_session.originalCwd);
6021
- if (!repoRoot) return null;
6022
- process.chdir(_session.originalCwd);
6023
- const result = cleanupWorktree(repoRoot, _session.slug);
6024
- _session = null;
6025
- return result;
6026
- }
6027
-
6028
6188
  // src/index.ts
6029
6189
  import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
6030
6190
  export {