toyon 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +9 -12
  2. package/dist/bridge.js +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/daemon.js +1745 -787
  5. package/dist/shell/assets/{Editor-CgKsVXHS.js → Editor-DR-GTFmf.js} +4 -4
  6. package/dist/shell/assets/{XTerm-CMRoVDAh.js → XTerm-B3kT_naP.js} +12 -12
  7. package/dist/shell/assets/{cssMode-Dg15kh0f.js → cssMode-CB4CczRa.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-CU21n-BA.js → freemarker2-Wjq3dVyB.js} +1 -1
  9. package/dist/shell/assets/{grammar-DGBFIuP2.js → grammar-yCr7BdNP.js} +1 -1
  10. package/dist/shell/assets/{handlebars-DVAbWe34.js → handlebars-Bjvi6pTu.js} +1 -1
  11. package/dist/shell/assets/{html-BkxU7kNX.js → html-iB2mKeN2.js} +1 -1
  12. package/dist/shell/assets/{htmlMode-Brl4kLPo.js → htmlMode-DtZVhckD.js} +1 -1
  13. package/dist/shell/assets/index-DVg10dTN.js +237 -0
  14. package/dist/shell/assets/index-wt4cLIYn.css +1 -0
  15. package/dist/shell/assets/{javascript-DoIo3Pg8.js → javascript-BpuIkItm.js} +1 -1
  16. package/dist/shell/assets/{jsonMode-B9qq6lCy.js → jsonMode-QCLe5ypz.js} +1 -1
  17. package/dist/shell/assets/{liquid-Cn1aqWby.js → liquid-CQI6mzhA.js} +1 -1
  18. package/dist/shell/assets/{lspLanguageFeatures-C5ez8E_9.js → lspLanguageFeatures-DqXzGsHQ.js} +1 -1
  19. package/dist/shell/assets/{mdx-1MuVz0xd.js → mdx-D2Unh-By.js} +1 -1
  20. package/dist/shell/assets/{python-DFhVdcGk.js → python-LLSqiJ4D.js} +1 -1
  21. package/dist/shell/assets/{razor-BitqUXpg.js → razor-CJ2qsTB8.js} +1 -1
  22. package/dist/shell/assets/{tsMode-loK_YDOL.js → tsMode-CwjjHUH5.js} +1 -1
  23. package/dist/shell/assets/{xml-Cc4ljERX.js → xml-BzqCyEOW.js} +1 -1
  24. package/dist/shell/assets/{yaml-0hpVC8PU.js → yaml-N1Bfi3ds.js} +1 -1
  25. package/dist/shell/index.html +3 -3
  26. package/package.json +1 -1
  27. package/dist/shell/assets/index-DUmxVabc.css +0 -1
  28. package/dist/shell/assets/index-WHwsRrlj.js +0 -232
package/dist/daemon.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // ../daemon/src/index.ts
3
- import { rmSync as rmSync6, writeFileSync as writeFileSync9 } from "fs";
4
- import { dirname as dirname9 } from "path";
3
+ import { rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
4
+ import { dirname as dirname10 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
 
7
7
  // ../shared/src/agent-tools.ts
@@ -12,6 +12,16 @@ var WRITE_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
12
12
  function isWriteTool(ev) {
13
13
  return ev.kind ? WRITE_KINDS.has(ev.kind) : WRITE_TOOLS.has(ev.name);
14
14
  }
15
+ var WRITTEN_KINDS = ["read", "edit", "delete", "move", "search", "execute", "fetch"];
16
+ function isWrittenKind(kind) {
17
+ return WRITTEN_KINDS.includes(kind ?? "");
18
+ }
19
+ function emptyInput(input) {
20
+ if (!input || typeof input !== "object")
21
+ return true;
22
+ const rec = input;
23
+ return Object.keys(rec).every((k) => k === "locations" && Array.isArray(rec[k]) && rec[k].length === 0);
24
+ }
15
25
 
16
26
  // ../shared/src/model.ts
17
27
  var DEFAULT_PERMISSION_MODE = "auto";
@@ -37,6 +47,12 @@ function streamKey(worktreeId, stream) {
37
47
  function isMain(wt) {
38
48
  return wt.kind === "main";
39
49
  }
50
+ function isProvisional(wt) {
51
+ return wt.kind === "spare";
52
+ }
53
+ function isLead(wt) {
54
+ return isProvisional(wt) || isMain(wt);
55
+ }
40
56
  function canArchive(wt) {
41
57
  return wt.kind === "worktree";
42
58
  }
@@ -52,8 +68,8 @@ function canGraft(wt) {
52
68
 
53
69
  // ../shared/src/railOrder.ts
54
70
  var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
55
- var tierOf = (w) => isMain(w) ? 0 : w.landed ? 2 : 1;
56
- var railUnitOf = (w) => w.variant ? `${tierOf(w)}:${w.variant.group}` : `row:${w.id}`;
71
+ var tierOf = (w) => isLead(w) ? 0 : 1;
72
+ var railUnitOf = (w) => w.variant ? `group:${w.variant.group}` : `row:${w.id}`;
57
73
  function railOrder(rows) {
58
74
  const units = [];
59
75
  const groups = new Map;
@@ -275,11 +291,6 @@ function middle(a, b) {
275
291
  return ops;
276
292
  }
277
293
 
278
- // ../shared/src/drafts.ts
279
- var DRAFT_PREFIX = "draft:";
280
- var draftKey = (repoId) => DRAFT_PREFIX + repoId;
281
- var draftRepoOf = (boxId) => boxId.startsWith(DRAFT_PREFIX) ? boxId.slice(DRAFT_PREFIX.length) : null;
282
-
283
294
  // ../shared/src/land.ts
284
295
  var DEFAULT_LAND_ROUTE = "merge";
285
296
  var DEFAULT_MERGE_METHOD = "merge";
@@ -297,7 +308,7 @@ var PAIR_TTL_MS = 120000;
297
308
  var pairLink = (host, code) => `https://${host}/#pair=${code}`;
298
309
 
299
310
  // ../shared/src/protocol/limits.ts
300
- var PROTOCOL_VERSION = 58;
311
+ var PROTOCOL_VERSION = 65;
301
312
  var FILE_MAX_CHARS = 5000000;
302
313
  var IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
303
314
  var IMAGE_MAX_EDGE = 2576;
@@ -6384,6 +6395,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6384
6395
  t: literal("create-worktree"),
6385
6396
  clientId: string2().max(64).optional(),
6386
6397
  repoId: id,
6398
+ worktreeId: id.optional(),
6387
6399
  prompt,
6388
6400
  variant: variantSchema.optional(),
6389
6401
  context: ambient.optional(),
@@ -6432,6 +6444,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6432
6444
  object({ t: literal("run-after-land"), repoId: id }),
6433
6445
  object({ t: literal("restart-daemon") }),
6434
6446
  object({ t: literal("update-now") }),
6447
+ object({ t: literal("check-update") }),
6435
6448
  object({ t: literal("pull-main"), worktreeId: id }),
6436
6449
  object({
6437
6450
  t: literal("write-file"),
@@ -6447,6 +6460,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6447
6460
  object({ t: literal("find-element"), worktreeId: id, seq, element: elementTraitsSchema }),
6448
6461
  object({ t: literal("design-scan"), worktreeId: id }),
6449
6462
  object({ t: literal("discard-file"), worktreeId: id, path: relPath }),
6463
+ object({ t: literal("judge"), worktreeId: id }),
6450
6464
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6451
6465
  object({ t: literal("stop-agent"), worktreeId: id }),
6452
6466
  object({ t: literal("seen"), worktreeId: id }),
@@ -6548,6 +6562,9 @@ function parseClientMsg(raw) {
6548
6562
  return { ok: false, reason: issueReason(r.error, "invalid message") };
6549
6563
  }
6550
6564
 
6565
+ // ../shared/src/restart.ts
6566
+ var RESTART_NOW = "now";
6567
+
6551
6568
  // ../shared/src/routes.ts
6552
6569
  var BASE = "http://preview.invalid";
6553
6570
  var TITLE_MAX = 200;
@@ -7722,7 +7739,7 @@ function slug(s) {
7722
7739
  // ../daemon/package.json
7723
7740
  var package_default = {
7724
7741
  name: "@toyon/daemon",
7725
- version: "0.4.0",
7742
+ version: "0.5.0",
7726
7743
  private: true,
7727
7744
  type: "module",
7728
7745
  main: "src/index.ts",
@@ -11840,11 +11857,18 @@ function fmtExtra(extra) {
11840
11857
  return ` ${String(extra)}`;
11841
11858
  }
11842
11859
  }
11860
+ var lastDay = "";
11843
11861
  function write(level, tag, message, extra) {
11844
11862
  if (LEVELS[level] < threshold)
11845
11863
  return;
11846
- const ts = new Date().toISOString().slice(11, 19);
11847
- process.stderr.write(`${ts} ${level.padEnd(5)} [${tag}] ${message}${fmtExtra(extra)}
11864
+ const iso = new Date().toISOString();
11865
+ const day = iso.slice(0, 10);
11866
+ if (day !== lastDay) {
11867
+ lastDay = day;
11868
+ process.stderr.write(`-------- ${day} --------
11869
+ `);
11870
+ }
11871
+ process.stderr.write(`${iso.slice(11, 19)} ${level.padEnd(5)} [${tag}] ${message}${fmtExtra(extra)}
11848
11872
  `);
11849
11873
  }
11850
11874
  var log = {
@@ -12001,6 +12025,27 @@ async function within(p, ms) {
12001
12025
  clearTimeout(timer);
12002
12026
  return done;
12003
12027
  }
12028
+ var GONE_POLL_MS = 100;
12029
+ function groupGone(pgid) {
12030
+ const deadline = Date.now() + KILL_GRACE_MS + EXIT_GRACE_MS + 500;
12031
+ return new Promise((resolve) => {
12032
+ const poll = () => {
12033
+ try {
12034
+ process.kill(-pgid, 0);
12035
+ } catch (e) {
12036
+ if (e.code === "ESRCH")
12037
+ return resolve();
12038
+ }
12039
+ if (Date.now() >= deadline)
12040
+ return resolve();
12041
+ setTimeout(poll, GONE_POLL_MS).unref?.();
12042
+ };
12043
+ poll();
12044
+ });
12045
+ }
12046
+ function reclaimGroup(pgid) {
12047
+ return killGroup(pgid, groupGone(pgid));
12048
+ }
12004
12049
 
12005
12050
  // ../daemon/src/agent/acp/transport.ts
12006
12051
  function spawnAcp(app, launch, cwd, tag) {
@@ -12035,7 +12080,7 @@ function spawnAcp(app, launch, cwd, tag) {
12035
12080
  exitInfo = `agent process exited (${why})${tail ? `: ${tail}` : ""}`;
12036
12081
  conn.close(new Error(exitInfo));
12037
12082
  });
12038
- return { conn, kill: () => killProcessGroup(child), exited, exitInfo: () => exitInfo };
12083
+ return { conn, pid: child.pid ?? null, kill: () => killProcessGroup(child), exited, exitInfo: () => exitInfo };
12039
12084
  }
12040
12085
 
12041
12086
  // ../daemon/src/agent/attachments.ts
@@ -12366,6 +12411,7 @@ var NO_PROMPT = {
12366
12411
  SSH_ASKPASS: "",
12367
12412
  GIT_SSH_COMMAND: "ssh -o BatchMode=yes"
12368
12413
  };
12414
+ var SPAWN_ENV = { GIT_OPTIONAL_LOCKS: "0" };
12369
12415
  async function run(cmd, args, cwd, env) {
12370
12416
  const started = Date.now();
12371
12417
  try {
@@ -12374,7 +12420,7 @@ async function run(cmd, args, cwd, env) {
12374
12420
  stdout: "pipe",
12375
12421
  stderr: "pipe",
12376
12422
  stdin: "ignore",
12377
- env: { ...process.env, ...env }
12423
+ env: { ...process.env, ...SPAWN_ENV, ...env }
12378
12424
  });
12379
12425
  const [rawOut, err, status] = await Promise.all([
12380
12426
  new Response(p.stdout).text(),
@@ -12402,7 +12448,7 @@ async function runLive(cmd, args, cwd, opts = {}) {
12402
12448
  stdout: "pipe",
12403
12449
  stderr: "pipe",
12404
12450
  stdin: "ignore",
12405
- env: { ...process.env, ...opts.env }
12451
+ env: { ...process.env, ...SPAWN_ENV, ...opts.env }
12406
12452
  });
12407
12453
  const stop = () => p.kill();
12408
12454
  opts.signal?.addEventListener("abort", stop, { once: true });
@@ -12694,6 +12740,29 @@ async function excludeFromGit(cwd, pattern) {
12694
12740
  ` : ""}${pattern}
12695
12741
  `);
12696
12742
  }
12743
+ async function excludeBlock(cwd, name, patterns) {
12744
+ const file = await excludeFile(cwd);
12745
+ if (!file)
12746
+ return;
12747
+ const open = `# toyon ${name}`;
12748
+ const close = `# toyon end ${name}`;
12749
+ const current = existsSync3(file) ? readFileSync3(file, "utf8") : "";
12750
+ const lines = current.split(`
12751
+ `);
12752
+ const from = lines.indexOf(open);
12753
+ const to = lines.indexOf(close);
12754
+ const kept = from >= 0 && to > from ? [...lines.slice(0, from), ...lines.slice(to + 1)] : lines;
12755
+ while (kept.length > 0 && kept[kept.length - 1] === "")
12756
+ kept.pop();
12757
+ const next = [...kept, ...patterns.length > 0 ? [open, ...patterns, close] : []];
12758
+ const text = next.length > 0 ? `${next.join(`
12759
+ `)}
12760
+ ` : "";
12761
+ if (text === current)
12762
+ return;
12763
+ mkdirSync2(dirname(file), { recursive: true });
12764
+ writeFileSync(file, text);
12765
+ }
12697
12766
  async function unexcludeFromGit(cwd, pattern) {
12698
12767
  const file = await excludeFile(cwd);
12699
12768
  if (!file || !existsSync3(file))
@@ -13197,6 +13266,7 @@ var SYSTEM_APPEND = [
13197
13266
  "Never run `git push`, delete branches, or create pull requests; shipping is handled by the Toyon UI.",
13198
13267
  "Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
13199
13268
  "Keep the scope tight: do the asked task well, then stop. Suggest follow-ups in chat instead of expanding scope.",
13269
+ "When referencing a file in this worktree in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor. A file outside the worktree has no page in Toyon: name its path in backticks instead of linking it.",
13200
13270
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
13201
13271
  `Toyon previews the project in a browser panel by running the commands in its settings file, .toyon/settings.json (or toyon.json at the repo root, in a repo that keeps it there), shaped like {"setup": ["bun install"], "run": {"web": "bun run dev --port $PORT"}, "check": "bun run check", "land": {"route": "merge"}}: setup runs once in every new worktree, every command in run keeps running and must listen on $PORT, which toyon sets differently for each worktree, check (optional) runs after each of your turns and must pass before the work can land, and land (optional) says how the user lands work on main: route "merge" (onto main here), "push" (onto main here, then pushed) or "pr" (a pull request); method "merge", "squash" or "rebase" for how the commits arrive on main; automerge true when GitHub should merge the pull request itself. A settings.local.json beside it (toyon.local.json at the root) holds one person's overrides and is never committed. Both files may carry comments and trailing commas; leave any you find.`,
13202
13272
  "Toyon runs those commands itself in every worktree, and the user watches the result live in a preview beside this chat, so never start a dev server or open a browser of your own to check your work. Each message says where this worktree's preview answers; fetch a page from there when you want to see it.",
@@ -13228,7 +13298,7 @@ function previewContext(p) {
13228
13298
  line = `The preview is running${at}, and the user sees it live beside this chat.`;
13229
13299
  break;
13230
13300
  case "asleep":
13231
- line = `The preview is asleep; it answers${at} once the user opens this worktree again.`;
13301
+ line = `The preview is asleep${p.detail ? ` (${p.detail})` : ""}; it answers${at} once the user opens this worktree again.`;
13232
13302
  break;
13233
13303
  default:
13234
13304
  line = `The preview is ${p.status}${p.detail ? `: ${p.detail}` : ""}; nothing answers${at} until it is back.`;
@@ -13619,11 +13689,14 @@ function asRecord(v) {
13619
13689
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
13620
13690
  }
13621
13691
  var NETWORK_ASK = "SandboxNetworkAccess";
13692
+ function isNetworkAsk(update) {
13693
+ return update.name === NETWORK_ASK || asRecord(asRecord(update._meta).claudeCode).toolName === NETWORK_ASK;
13694
+ }
13622
13695
  function heading(update) {
13623
- const name = update.name ?? update.title;
13696
+ const name = typeof update.name === "string" ? update.name : "";
13624
13697
  const host = asRecord(update.rawInput).host;
13625
- if (name === NETWORK_ASK && typeof host === "string")
13626
- return { name: "network", title: host, kind: "fetch" };
13698
+ if (isNetworkAsk(update) && typeof host === "string")
13699
+ return { name: "", title: host, kind: "fetch" };
13627
13700
  return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
13628
13701
  }
13629
13702
  function endOfAsk(req, res, memos) {
@@ -13641,40 +13714,59 @@ function endOfAsk(req, res, memos) {
13641
13714
  isError: !allowed
13642
13715
  };
13643
13716
  }
13717
+ function abandoned(memos, parent) {
13718
+ const out = [];
13719
+ for (const [toolId, memo] of memos) {
13720
+ if (memo.ended || !memo.writing || memo.parent !== parent)
13721
+ continue;
13722
+ memo.ended = true;
13723
+ out.push({ type: "tool-end", toolId });
13724
+ }
13725
+ return out;
13726
+ }
13644
13727
  function mapUpdate(update, memos, tag) {
13645
13728
  switch (update.sessionUpdate) {
13646
13729
  case "agent_message_chunk": {
13730
+ const { parentToolId } = spawnOf(update._meta);
13731
+ const out = abandoned(memos, parentToolId);
13647
13732
  const text = textOf(update.content, tag, "message");
13648
13733
  if (text === null)
13649
- return [];
13650
- const { parentToolId } = spawnOf(update._meta);
13651
- return [parentToolId ? { type: "tool-delta", toolId: parentToolId, text } : { type: "text-delta", text }];
13734
+ return out;
13735
+ out.push(parentToolId ? { type: "tool-delta", toolId: parentToolId, text } : { type: "text-delta", text });
13736
+ return out;
13652
13737
  }
13653
13738
  case "agent_thought_chunk": {
13654
- if (spawnOf(update._meta).parentToolId)
13655
- return [];
13739
+ const { parentToolId } = spawnOf(update._meta);
13740
+ const out = abandoned(memos, parentToolId);
13741
+ if (parentToolId)
13742
+ return out;
13656
13743
  const text = textOf(update.content, tag, "thought");
13657
- return text === null ? [] : [{ type: "thinking-delta", text }];
13744
+ if (text !== null)
13745
+ out.push({ type: "thinking-delta", text });
13746
+ return out;
13658
13747
  }
13659
13748
  case "tool_call": {
13749
+ const spawn = spawnOf(update._meta);
13750
+ const out = abandoned(memos, spawn.parentToolId);
13751
+ const input = update.rawInput ?? { locations: update.locations ?? [] };
13660
13752
  const memo = {
13661
13753
  ...heading(update),
13662
13754
  content: update.content ?? [],
13663
13755
  rawOutput: update.rawOutput,
13664
- ended: false
13756
+ ended: false,
13757
+ writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
13758
+ ...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
13665
13759
  };
13666
13760
  memos.set(update.toolCallId, memo);
13667
- const out = [
13668
- {
13669
- type: "tool-start",
13670
- toolId: update.toolCallId,
13671
- name: memo.name,
13672
- input: update.rawInput ?? { locations: update.locations ?? [] },
13673
- ...memo.kind ? { kind: memo.kind } : {},
13674
- title: memo.title,
13675
- ...spawnOf(update._meta)
13676
- }
13677
- ];
13761
+ out.push({
13762
+ type: "tool-start",
13763
+ toolId: update.toolCallId,
13764
+ name: memo.name,
13765
+ input,
13766
+ ...memo.kind ? { kind: memo.kind } : {},
13767
+ title: memo.title,
13768
+ ...spawn
13769
+ });
13678
13770
  if (update.status === "completed" || update.status === "failed")
13679
13771
  out.push(endOf(update.toolCallId, memo, update.status));
13680
13772
  return out;
@@ -13683,37 +13775,45 @@ function mapUpdate(update, memos, tag) {
13683
13775
  let memo = memos.get(update.toolCallId);
13684
13776
  const out = [];
13685
13777
  if (!memo) {
13778
+ const spawn = spawnOf(update._meta);
13779
+ const input = update.rawInput ?? { locations: update.locations ?? [] };
13686
13780
  memo = {
13687
- name: update.name ?? update.title ?? "tool",
13781
+ name: update.name ?? "",
13688
13782
  title: update.title ?? update.name ?? "tool",
13689
13783
  ...update.kind ? { kind: update.kind } : {},
13690
13784
  content: [],
13691
- ended: false
13785
+ ended: false,
13786
+ writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
13787
+ ...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
13692
13788
  };
13693
13789
  memos.set(update.toolCallId, memo);
13694
13790
  out.push({
13695
13791
  type: "tool-start",
13696
13792
  toolId: update.toolCallId,
13697
13793
  name: memo.name,
13698
- input: update.rawInput ?? { locations: update.locations ?? [] },
13794
+ input,
13699
13795
  ...memo.kind ? { kind: memo.kind } : {},
13700
13796
  title: memo.title,
13701
- ...spawnOf(update._meta)
13797
+ ...spawn
13702
13798
  });
13703
13799
  }
13800
+ if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
13801
+ return out;
13704
13802
  const refined = { type: "tool-update", toolId: update.toolCallId };
13705
13803
  if (update.title && update.title !== memo.title) {
13706
13804
  memo.title = update.title;
13707
13805
  refined.title = update.title;
13708
- if (!update.name && memo.name !== update.title)
13709
- refined.name = memo.name = update.title;
13710
13806
  }
13711
- if (update.name && update.name !== memo.name)
13807
+ if (update.name && update.name !== memo.name) {
13712
13808
  refined.name = memo.name = update.name;
13809
+ }
13713
13810
  if (update.kind && update.kind !== memo.kind)
13714
13811
  refined.kind = memo.kind = update.kind;
13715
- if (update.rawInput !== undefined)
13812
+ if (update.rawInput !== undefined) {
13716
13813
  refined.input = update.rawInput;
13814
+ if (!emptyInput(update.rawInput))
13815
+ memo.writing = false;
13816
+ }
13717
13817
  if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
13718
13818
  out.push(refined);
13719
13819
  if (update.content)
@@ -13827,6 +13927,7 @@ function steerOutcome(value) {
13827
13927
 
13828
13928
  // ../daemon/src/agent/acp/session.ts
13829
13929
  var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
13930
+ var OWN_SETTLE_MS = 1e4;
13830
13931
  function recorded(reply) {
13831
13932
  if (reply?.kind === "choice")
13832
13933
  return { choiceId: reply.choiceId };
@@ -13845,8 +13946,8 @@ class AcpSession {
13845
13946
  conn = null;
13846
13947
  connecting = null;
13847
13948
  live = null;
13848
- loading = false;
13849
13949
  reaper = null;
13950
+ own = null;
13850
13951
  asking = 0;
13851
13952
  asks = new Map;
13852
13953
  log;
@@ -13874,13 +13975,38 @@ class AcpSession {
13874
13975
  async warmCommands() {
13875
13976
  if (this.commandList.length > 0 || this.stopped)
13876
13977
  return;
13978
+ await this.warm();
13979
+ }
13980
+ async warm() {
13981
+ if (this.stopped)
13982
+ return;
13877
13983
  try {
13878
- await this.ensureLive();
13984
+ await this.ensureLive("warm-up");
13879
13985
  this.maybeArmReaper();
13880
13986
  } catch (e) {
13881
- log.debug(this.d.worktreeId, `commands warm-up skipped: ${this.describe(e)}`);
13987
+ log.debug(this.d.worktreeId, `agent warm-up skipped: ${this.describe(e)}`);
13882
13988
  }
13883
13989
  }
13990
+ get runningAgent() {
13991
+ return this.conn?.spec.id ?? null;
13992
+ }
13993
+ pgid_ = null;
13994
+ get pgid() {
13995
+ return this.pgid_;
13996
+ }
13997
+ noteProcess(link, up) {
13998
+ if (link.pid === null)
13999
+ return;
14000
+ if (up)
14001
+ this.pgid_ = link.pid;
14002
+ else if (this.pgid_ === link.pid)
14003
+ this.pgid_ = null;
14004
+ this.d.onProcess?.(link.pid, up);
14005
+ }
14006
+ async restart() {
14007
+ this.clearReaper();
14008
+ await this.dropConn("restart");
14009
+ }
13884
14010
  setCommands(next) {
13885
14011
  if (sameCommands(this.commandList, next))
13886
14012
  return;
@@ -13930,7 +14056,7 @@ class AcpSession {
13930
14056
  return log.warn(this.d.worktreeId, "send after close dropped");
13931
14057
  const { context, attachments } = opts;
13932
14058
  const item = { text, context, ...attachments?.length ? { attachments } : {} };
13933
- if (this.running && !this.interrupted && this.live?.conn.steering) {
14059
+ if ((this.running || this.own) && !this.interrupted && this.live?.conn.steering) {
13934
14060
  return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
13935
14061
  }
13936
14062
  this.enqueue(item);
@@ -13944,7 +14070,7 @@ class AcpSession {
13944
14070
  async steer(item) {
13945
14071
  item.recorded ??= await this.record(item);
13946
14072
  const live = this.live;
13947
- if (!live || !this.running || this.interrupted)
14073
+ if (!live || !(this.running || this.own) || this.interrupted)
13948
14074
  return this.enqueue(item);
13949
14075
  this.steered.push(item);
13950
14076
  let outcome;
@@ -13982,8 +14108,14 @@ class AcpSession {
13982
14108
  }
13983
14109
  stop() {
13984
14110
  this.cancelAsks();
13985
- if (!this.running)
14111
+ if (!this.running) {
14112
+ if (this.own && this.live) {
14113
+ const { conn, sessionId } = this.live;
14114
+ fireAndForget(this.d.worktreeId, conn.ctx.notify(methods.agent.session.cancel, { sessionId }), "cancel");
14115
+ this.endOwn("interrupted");
14116
+ }
13986
14117
  return;
14118
+ }
13987
14119
  this.interrupted = true;
13988
14120
  if (this.steered.length > 0) {
13989
14121
  this.queue.unshift(...this.steered);
@@ -14002,10 +14134,11 @@ class AcpSession {
14002
14134
  this.steered = [];
14003
14135
  this.queueChanged();
14004
14136
  this.stop();
14005
- await this.dropConn();
14137
+ await this.dropConn("closing");
14006
14138
  await this.log.flush();
14007
14139
  }
14008
14140
  async drain() {
14141
+ this.endOwn("end_turn");
14009
14142
  this.running = true;
14010
14143
  this.clearReaper();
14011
14144
  this.setStatus("working");
@@ -14043,7 +14176,7 @@ class AcpSession {
14043
14176
  } else {
14044
14177
  this.emit({ type: "agent-error", message: this.describe(e), ts: Date.now() });
14045
14178
  this.setStatus("error");
14046
- fireAndForget(this.d.worktreeId, this.dropConn(), "drop agent after error");
14179
+ fireAndForget(this.d.worktreeId, this.dropConn("error"), "drop agent after error");
14047
14180
  }
14048
14181
  } finally {
14049
14182
  this.interrupted = false;
@@ -14058,7 +14191,7 @@ class AcpSession {
14058
14191
  this.asking++;
14059
14192
  this.clearReaper();
14060
14193
  try {
14061
- const conn = await this.ensureConn();
14194
+ const conn = await this.ensureConn("an ask");
14062
14195
  return await askOnce(conn.ctx, {
14063
14196
  cwd: this.d.cwd,
14064
14197
  system,
@@ -14084,7 +14217,7 @@ class AcpSession {
14084
14217
  }
14085
14218
  }
14086
14219
  async authenticate(methodId, apiKey) {
14087
- const conn = await this.ensureConn();
14220
+ const conn = await this.ensureConn("login");
14088
14221
  const method = conn.authMethods.find((m) => m.id === methodId);
14089
14222
  if (!method)
14090
14223
  throw new UserError(`${conn.spec.name} offers no login method "${methodId}"`);
@@ -14160,7 +14293,7 @@ class AcpSession {
14160
14293
  async runTurn(item) {
14161
14294
  item.recorded ??= await this.record(item);
14162
14295
  this.emit({ type: "turn-start", ts: Date.now() });
14163
- const live = await this.ensureLive();
14296
+ const live = await this.ensureLive("a turn");
14164
14297
  await this.applyMode(live);
14165
14298
  await this.applyOption(live, "model");
14166
14299
  await this.applyOption(live, "thought_level");
@@ -14180,17 +14313,17 @@ class AcpSession {
14180
14313
  contextFor(item) {
14181
14314
  return ambientBlock([...item.context ?? [], this.d.preview?.()]);
14182
14315
  }
14183
- ensureConn() {
14316
+ ensureConn(why) {
14184
14317
  if (this.conn)
14185
14318
  return Promise.resolve(this.conn);
14186
14319
  if (this.connecting)
14187
14320
  return this.connecting;
14188
- this.connecting = this.openConn().finally(() => {
14321
+ this.connecting = this.openConn(why).finally(() => {
14189
14322
  this.connecting = null;
14190
14323
  });
14191
14324
  return this.connecting;
14192
14325
  }
14193
- async openConn() {
14326
+ async openConn(why) {
14194
14327
  const spec = this.d.spec();
14195
14328
  const prepared = await (this.d.prepare ?? prepareLaunch)(this.d.cwd, spec);
14196
14329
  const { bounds } = prepared;
@@ -14202,13 +14335,17 @@ class AcpSession {
14202
14335
  this.d.onAuth?.(spec.id, { status: c.params });
14203
14336
  });
14204
14337
  const link = this.d.connect(app, spec, prepared);
14338
+ this.noteProcess(link, true);
14339
+ log.info(this.d.worktreeId, `agent ${spec.id} started for ${why} (pid ${link.pid ?? "unknown"})`);
14205
14340
  const ctx = link.conn.agent;
14206
14341
  link.exited.then(() => {
14342
+ this.noteProcess(link, false);
14207
14343
  if (this.conn?.link === link) {
14208
14344
  log.warn(this.d.worktreeId, "agent process exited while idle");
14209
14345
  this.conn = null;
14210
14346
  this.live = null;
14211
14347
  this.cancelAsks();
14348
+ this.endOwn("interrupted");
14212
14349
  }
14213
14350
  });
14214
14351
  try {
@@ -14227,7 +14364,7 @@ class AcpSession {
14227
14364
  spec,
14228
14365
  bounds,
14229
14366
  authMethods: init.authMethods ?? [],
14230
- loadSession: !!init.agentCapabilities?.loadSession,
14367
+ resumeSession: !!init.agentCapabilities?.sessionCapabilities?.resume,
14231
14368
  closeSupported: !!init.agentCapabilities?.sessionCapabilities?.close,
14232
14369
  deleteSupported: !!init.agentCapabilities?.sessionCapabilities?.delete,
14233
14370
  acceptsImages: init.agentCapabilities?.promptCapabilities?.image === true,
@@ -14239,11 +14376,12 @@ class AcpSession {
14239
14376
  return this.conn;
14240
14377
  } catch (e) {
14241
14378
  fireAndForget(this.d.worktreeId, link.kill(), "kill agent after failed start");
14379
+ this.noteProcess(link, false);
14242
14380
  throw e;
14243
14381
  }
14244
14382
  }
14245
- async ensureLive() {
14246
- const conn = await this.ensureConn();
14383
+ async ensureLive(why) {
14384
+ const conn = await this.ensureConn(why);
14247
14385
  if (this.live?.conn === conn)
14248
14386
  return this.live;
14249
14387
  const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
@@ -14251,10 +14389,9 @@ class AcpSession {
14251
14389
  let modes;
14252
14390
  let configOptions;
14253
14391
  let resumed = false;
14254
- if (sessionId && conn.loadSession) {
14255
- this.loading = true;
14392
+ if (sessionId && conn.resumeSession) {
14256
14393
  try {
14257
- const r = await conn.ctx.request(methods.agent.session.load, {
14394
+ const r = await conn.ctx.request(methods.agent.session.resume, {
14258
14395
  sessionId,
14259
14396
  cwd: this.d.cwd,
14260
14397
  mcpServers: [],
@@ -14267,8 +14404,6 @@ class AcpSession {
14267
14404
  if (isAuthRequired(e))
14268
14405
  throw e;
14269
14406
  log.warn(this.d.worktreeId, `could not resume agent session ${sessionId}; starting a new one`, e);
14270
- } finally {
14271
- this.loading = false;
14272
14407
  }
14273
14408
  }
14274
14409
  if (!resumed) {
@@ -14365,8 +14500,6 @@ class AcpSession {
14365
14500
  this.setCommands(mapped);
14366
14501
  return;
14367
14502
  }
14368
- if (this.loading)
14369
- return;
14370
14503
  const listener = side.get(params.sessionId);
14371
14504
  if (listener) {
14372
14505
  const u = params.update;
@@ -14384,7 +14517,10 @@ class AcpSession {
14384
14517
  }
14385
14518
  if (params.update.sessionUpdate === "config_option_update")
14386
14519
  this.absorb(live, params.update.configOptions);
14387
- for (const ev of mapUpdate(params.update, live.tools, this.d.worktreeId)) {
14520
+ const events = mapUpdate(params.update, live.tools, this.d.worktreeId);
14521
+ if (!this.running)
14522
+ this.trackOwn(events);
14523
+ for (const ev of events) {
14388
14524
  if (ev.type === "text-delta" || ev.type === "tool-start")
14389
14525
  this.steered = [];
14390
14526
  this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
@@ -14417,17 +14553,18 @@ class AcpSession {
14417
14553
  }
14418
14554
  return pickOption(params.options, verdict);
14419
14555
  }
14420
- askPermission(params) {
14556
+ async askPermission(params) {
14421
14557
  const plan = params.toolCall.kind === "switch_mode";
14422
- if (plan)
14423
- this.d.onPlan?.();
14558
+ const detail = permissionDetail(params);
14559
+ const planPath = plan ? await this.d.onPlan?.(detail) ?? null : null;
14424
14560
  const offered = plan ? params.options : params.options.filter((o) => o.kind !== "allow_always");
14425
14561
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
14426
14562
  return this.openAsk((id) => ({
14427
14563
  type: "agent-permission",
14428
14564
  id,
14429
14565
  title: params.toolCall.title ?? params.toolCall.name ?? "the agent needs a decision",
14430
- ...permissionDetail(params) ? { detail: permissionDetail(params) } : {},
14566
+ ...detail ? { detail } : {},
14567
+ ...planPath ? { plan: planPath } : {},
14431
14568
  choices,
14432
14569
  ...params.toolCall.toolCallId ? { toolId: params.toolCall.toolCallId } : {},
14433
14570
  ts: Date.now()
@@ -14438,10 +14575,17 @@ class AcpSession {
14438
14575
  return { outcome: { outcome: "cancelled" } };
14439
14576
  if (plan && (choice.kind === "allow_once" || choice.kind === "allow_always")) {
14440
14577
  this.d.setMode?.(modeAfterPlan(choice.name));
14578
+ if (planPath)
14579
+ fireAndForget(this.d.worktreeId, this.sayPlanEdited(detail, planPath), "edited plan");
14441
14580
  }
14442
14581
  return { outcome: { outcome: "selected", optionId: choice.id } };
14443
14582
  });
14444
14583
  }
14584
+ async sayPlanEdited(proposed, path) {
14585
+ if (!await this.d.planEdited?.(path, proposed))
14586
+ return;
14587
+ this.send(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
14588
+ }
14445
14589
  onElicit(params, signal) {
14446
14590
  const live = this.live;
14447
14591
  if (!live || !("sessionId" in params) || params.sessionId !== live.sessionId) {
@@ -14499,21 +14643,56 @@ class AcpSession {
14499
14643
  syncStatus() {
14500
14644
  if (this.status === "error")
14501
14645
  return;
14502
- this.setStatus(this.asks.size > 0 ? "waiting" : this.running ? "working" : "idle");
14646
+ this.setStatus(this.asks.size > 0 ? "waiting" : this.running || this.own ? "working" : "idle");
14647
+ }
14648
+ trackOwn(events) {
14649
+ if (!this.own) {
14650
+ const doing = events.some((e) => e.type === "text-delta" || e.type === "thinking-delta" || e.type === "tool-start");
14651
+ if (!doing)
14652
+ return;
14653
+ this.own = { open: new Set, settle: null };
14654
+ this.clearReaper();
14655
+ this.emit({ type: "turn-start", ts: Date.now() });
14656
+ this.syncStatus();
14657
+ }
14658
+ for (const e of events) {
14659
+ if (e.type === "tool-start")
14660
+ this.own.open.add(e.toolId);
14661
+ else if (e.type === "tool-end")
14662
+ this.own.open.delete(e.toolId);
14663
+ }
14664
+ if (this.own.settle)
14665
+ clearTimeout(this.own.settle);
14666
+ this.own.settle = null;
14667
+ if (this.own.open.size > 0)
14668
+ return;
14669
+ const t = setTimeout(() => this.endOwn("end_turn"), this.d.ownSettleMs ?? OWN_SETTLE_MS);
14670
+ t.unref?.();
14671
+ this.own.settle = t;
14672
+ }
14673
+ endOwn(stopReason) {
14674
+ if (!this.own)
14675
+ return;
14676
+ if (this.own.settle)
14677
+ clearTimeout(this.own.settle);
14678
+ this.own = null;
14679
+ this.emit({ type: "turn-end", stopReason, ts: Date.now() });
14680
+ this.syncStatus();
14681
+ this.maybeArmReaper();
14503
14682
  }
14504
14683
  maybeArmReaper() {
14505
- if (!this.running && !this.asking && this.asks.size === 0 && this.conn && !this.stopped)
14684
+ if (!this.running && !this.own && !this.asking && this.asks.size === 0 && this.conn && !this.stopped)
14506
14685
  this.armReaper();
14507
14686
  }
14508
14687
  armReaper() {
14509
14688
  this.clearReaper();
14689
+ const idleMs = this.d.idleMs ?? DEFAULT_IDLE_MS;
14510
14690
  const t = setTimeout(() => {
14511
14691
  this.reaper = null;
14512
- if (this.running || this.asking || this.asks.size > 0 || !this.conn)
14692
+ if (this.running || this.own || this.asking || this.asks.size > 0 || !this.conn)
14513
14693
  return;
14514
- log.debug(this.d.worktreeId, "agent idle; stopping its process");
14515
- fireAndForget(this.d.worktreeId, this.dropConn(), "reap agent");
14516
- }, this.d.idleMs ?? DEFAULT_IDLE_MS);
14694
+ fireAndForget(this.d.worktreeId, this.dropConn(`idle for ${Math.round(idleMs / 60000)} min`), "reap agent");
14695
+ }, idleMs);
14517
14696
  t.unref?.();
14518
14697
  this.reaper = t;
14519
14698
  }
@@ -14522,15 +14701,17 @@ class AcpSession {
14522
14701
  clearTimeout(this.reaper);
14523
14702
  this.reaper = null;
14524
14703
  }
14525
- async dropConn() {
14704
+ async dropConn(why) {
14526
14705
  this.cancelAsks();
14527
14706
  const conn = this.conn;
14528
14707
  this.conn = null;
14529
14708
  this.live = null;
14530
14709
  if (!conn)
14531
14710
  return;
14711
+ log.info(this.d.worktreeId, `agent ${conn.spec.id} stopped: ${why} (pid ${conn.link.pid ?? "unknown"})`);
14532
14712
  conn.link.conn.close();
14533
14713
  await conn.link.kill();
14714
+ this.noteProcess(conn.link, false);
14534
14715
  }
14535
14716
  }
14536
14717
  function isAuthRequired(e) {
@@ -14593,6 +14774,217 @@ function sameCommands(a, b) {
14593
14774
  return a.length === b.length && a.every((c, i) => c.name === b[i]?.name && c.description === b[i]?.description && c.hint === b[i]?.hint);
14594
14775
  }
14595
14776
 
14777
+ // ../daemon/src/agent/planDoc.ts
14778
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
14779
+ import { join as join7 } from "path";
14780
+ var PLANS_DIR = join7(".toyon", "plans");
14781
+ var PLAN_NAME = /^(\d+)\.md$/;
14782
+ function isPlanPath(rel) {
14783
+ return rel.startsWith(`${PLANS_DIR}/`) && PLAN_NAME.test(rel.slice(PLANS_DIR.length + 1));
14784
+ }
14785
+ function nextNumber(dir) {
14786
+ if (!existsSync8(dir))
14787
+ return 1;
14788
+ let max = 0;
14789
+ for (const name of readdirSync2(dir)) {
14790
+ const n = Number(PLAN_NAME.exec(name)?.[1]);
14791
+ if (n > max)
14792
+ max = n;
14793
+ }
14794
+ return max + 1;
14795
+ }
14796
+ async function writePlanDoc(cwd, markdown) {
14797
+ const dir = join7(cwd, PLANS_DIR);
14798
+ const rel = join7(PLANS_DIR, `${nextNumber(dir)}.md`);
14799
+ try {
14800
+ mkdirSync5(dir, { recursive: true });
14801
+ writeFileSync4(join7(cwd, rel), markdown.endsWith(`
14802
+ `) ? markdown : `${markdown}
14803
+ `);
14804
+ } catch (e) {
14805
+ log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
14806
+ return null;
14807
+ }
14808
+ await excludeFromGit(cwd, `${PLANS_DIR}/`);
14809
+ return rel;
14810
+ }
14811
+ function planEdited(cwd, rel, proposed) {
14812
+ const file = join7(cwd, rel);
14813
+ if (!existsSync8(file))
14814
+ return false;
14815
+ try {
14816
+ return readFileSync7(file, "utf8").trim() !== proposed.trim();
14817
+ } catch (e) {
14818
+ log.warn(cwd, `could not read ${rel}`, e);
14819
+ return false;
14820
+ }
14821
+ }
14822
+
14823
+ // ../daemon/src/runtime/memory.ts
14824
+ var MAC_PRESSURE_CRITICAL = 4;
14825
+ var AVAILABLE_FLOOR = 0.15;
14826
+ var PAGING_FLOOR_BPS = 16 * 2 ** 20;
14827
+ var paging = [];
14828
+ var unsupportedLogged = false;
14829
+ async function memoryTight() {
14830
+ const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
14831
+ if (signal === null && !unsupportedLogged) {
14832
+ unsupportedLogged = true;
14833
+ log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
14834
+ }
14835
+ return signal;
14836
+ }
14837
+ async function darwin() {
14838
+ const [r, v] = await Promise.all([
14839
+ run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/"),
14840
+ run("vm_stat", [], "/")
14841
+ ]);
14842
+ if (!r.ok)
14843
+ return null;
14844
+ const [level, available] = r.out.split(/\s+/).map(Number);
14845
+ if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
14846
+ return null;
14847
+ const sample = v.ok ? parseVmStat(v.out, Date.now()) : null;
14848
+ if (sample)
14849
+ paging = [...paging, sample].slice(-3);
14850
+ return darwinSignal(level, available, sustainedPaging(paging));
14851
+ }
14852
+ function parseVmStat(text, at) {
14853
+ const pageSize = text.match(/page size of (\d+) bytes/);
14854
+ const swapouts = text.match(/^Swapouts:\s+(\d+)/m);
14855
+ if (!pageSize || !swapouts)
14856
+ return null;
14857
+ return { at, bytes: Number(swapouts[1]) * Number(pageSize[1]) };
14858
+ }
14859
+ function sustainedPaging(samples) {
14860
+ if (samples.length < 3)
14861
+ return null;
14862
+ const rates = [];
14863
+ for (let i = samples.length - 2;i < samples.length; i++) {
14864
+ const prev = samples[i - 1];
14865
+ const next = samples[i];
14866
+ const seconds = (next.at - prev.at) / 1000;
14867
+ const bytes = next.bytes - prev.bytes;
14868
+ if (seconds <= 0 || bytes < 0)
14869
+ return null;
14870
+ rates.push(bytes / seconds);
14871
+ }
14872
+ return Math.min(...rates);
14873
+ }
14874
+ function darwinSignal(level, available, pagingBps = null) {
14875
+ const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
14876
+ const swapping = pagingBps === null ? "" : `, swapping out ${Math.round(pagingBps / 2 ** 20)} MB/s`;
14877
+ return {
14878
+ tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100 || pagingBps !== null && pagingBps >= PAGING_FLOOR_BPS,
14879
+ why: `memory pressure ${pressure} with ${available}% available${swapping}`
14880
+ };
14881
+ }
14882
+ async function linux() {
14883
+ let text;
14884
+ try {
14885
+ text = await Bun.file("/proc/meminfo").text();
14886
+ } catch {
14887
+ return null;
14888
+ }
14889
+ const field = (name) => {
14890
+ const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
14891
+ return m ? Number(m[1]) : null;
14892
+ };
14893
+ const total = field("MemTotal");
14894
+ const available = field("MemAvailable");
14895
+ if (!total || available === null)
14896
+ return null;
14897
+ const percent = Math.round(available / total * 100);
14898
+ return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
14899
+ }
14900
+ async function sampleCosts(pgids) {
14901
+ const costs = new Map;
14902
+ if (pgids.length === 0)
14903
+ return costs;
14904
+ const wanted = new Set(pgids);
14905
+ const r = await run("ps", ["-axo", "pgid=,rss="], "/");
14906
+ if (!r.ok)
14907
+ return costs;
14908
+ for (const line of r.out.split(`
14909
+ `)) {
14910
+ const [pgid, rss] = line.trim().split(/\s+/).map(Number);
14911
+ if (pgid === undefined || rss === undefined || !wanted.has(pgid))
14912
+ continue;
14913
+ costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
14914
+ }
14915
+ return costs;
14916
+ }
14917
+ async function bootId() {
14918
+ if (process.platform === "darwin") {
14919
+ const r = await run("sysctl", ["-n", "kern.boottime"], "/");
14920
+ const m = r.ok ? r.out.match(/sec = (\d+)/) : null;
14921
+ return m ? m[1] : null;
14922
+ }
14923
+ if (process.platform === "linux") {
14924
+ try {
14925
+ const m = (await Bun.file("/proc/stat").text()).match(/^btime (\d+)/m);
14926
+ return m ? m[1] : null;
14927
+ } catch {
14928
+ return null;
14929
+ }
14930
+ }
14931
+ return null;
14932
+ }
14933
+ async function psGroups(now = Date.now()) {
14934
+ const r = await run("ps", ["-axo", "pid=,pgid=,ppid=,etime=,command="], "/");
14935
+ if (!r.ok)
14936
+ return [];
14937
+ const rows = [];
14938
+ for (const line of r.out.split(`
14939
+ `)) {
14940
+ const m = line.trim().match(/^(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s*(.*)$/);
14941
+ if (!m)
14942
+ continue;
14943
+ const elapsed = parseEtime(m[4]);
14944
+ if (elapsed === null)
14945
+ continue;
14946
+ rows.push({
14947
+ pid: Number(m[1]),
14948
+ pgid: Number(m[2]),
14949
+ ppid: Number(m[3]),
14950
+ startedAt: now - elapsed * 1000,
14951
+ command: m[5] ?? ""
14952
+ });
14953
+ }
14954
+ return rows;
14955
+ }
14956
+ function parseEtime(text) {
14957
+ const m = text.match(/^(?:(?:(\d+)-)?(\d+):)?(\d+):(\d+)$/);
14958
+ if (!m)
14959
+ return null;
14960
+ const [, days, hours, minutes, seconds] = m;
14961
+ return Number(days ?? 0) * 86400 + Number(hours ?? 0) * 3600 + Number(minutes) * 60 + Number(seconds);
14962
+ }
14963
+
14964
+ // ../daemon/src/runtime/orphans.ts
14965
+ var GROUP_TTL_MS = 24 * 60 * 60000;
14966
+ var LEADER_SLACK_MS = 1e4;
14967
+ function orphansIn(ledger, rows, bootId, ledgerBootId, now) {
14968
+ if (bootId === null || ledgerBootId === undefined || bootId !== ledgerBootId)
14969
+ return [];
14970
+ const out = [];
14971
+ for (const entry of ledger) {
14972
+ if (now - entry.startedAt > GROUP_TTL_MS)
14973
+ continue;
14974
+ const members = rows.filter((r) => r.pgid === entry.pgid);
14975
+ if (members.length === 0)
14976
+ continue;
14977
+ const leader = members.find((r) => r.pid === entry.pgid);
14978
+ if (leader && Math.abs(leader.startedAt - entry.startedAt) > LEADER_SLACK_MS)
14979
+ continue;
14980
+ out.push({ worktreeId: entry.worktreeId, pgid: entry.pgid, name: entry.name });
14981
+ }
14982
+ return out;
14983
+ }
14984
+ function strayAdapters(rows, agentsDir) {
14985
+ return rows.filter((r) => r.pid === r.pgid && r.ppid === 1 && r.command.includes(agentsDir));
14986
+ }
14987
+
14596
14988
  // ../daemon/src/runtime/ports.ts
14597
14989
  var allocated = new Set;
14598
14990
  var range = cloud.proxyPorts;
@@ -14705,7 +15097,7 @@ function expandEnv(env, vars) {
14705
15097
 
14706
15098
  // ../daemon/src/core/remote.ts
14707
15099
  import { createHmac, timingSafeEqual } from "crypto";
14708
- import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
15100
+ import { existsSync as existsSync9, readFileSync as readFileSync8 } from "fs";
14709
15101
  function loadRemote(file, env = process.env) {
14710
15102
  if (env.TOYON_CLOUD === "1") {
14711
15103
  const host = (env.TOYON_PUBLIC_HOST ?? "").toLowerCase();
@@ -14718,9 +15110,9 @@ function loadRemote(file, env = process.env) {
14718
15110
  throw new Error(`TOYON_PREVIEWS: ${why}`);
14719
15111
  return { host, previews, front: "edge" };
14720
15112
  }
14721
- if (!existsSync8(file))
15113
+ if (!existsSync9(file))
14722
15114
  return null;
14723
- const view = parseRemote(readFileSync7(file, "utf8"));
15115
+ const view = parseRemote(readFileSync8(file, "utf8"));
14724
15116
  if (!view) {
14725
15117
  log.warn("remote", `${file} names no valid host or previews; remote access is off`);
14726
15118
  return null;
@@ -15381,7 +15773,7 @@ class WorktreeProcs {
15381
15773
  async killProc(mp) {
15382
15774
  await mp.pty?.kill();
15383
15775
  }
15384
- async sleep() {
15776
+ async sleep(why) {
15385
15777
  if (this.asleep || this.stopped)
15386
15778
  return;
15387
15779
  this.asleep = true;
@@ -15389,6 +15781,7 @@ class WorktreeProcs {
15389
15781
  for (const mp of this.procs.values()) {
15390
15782
  mp.state.status = "asleep";
15391
15783
  mp.state.pid = undefined;
15784
+ mp.state.detail = why;
15392
15785
  this.onProc({ ...mp.state });
15393
15786
  exits.push(this.killProc(mp));
15394
15787
  }
@@ -15403,13 +15796,16 @@ class WorktreeProcs {
15403
15796
  this.spawnProc(mp);
15404
15797
  }
15405
15798
  }
15406
- pgids() {
15799
+ groups() {
15407
15800
  const out = [];
15408
- for (const mp of this.procs.values())
15801
+ for (const [name, mp] of this.procs)
15409
15802
  if (mp.pty?.alive)
15410
- out.push(mp.pty.pid);
15803
+ out.push({ name, pgid: mp.pty.pid });
15411
15804
  return out;
15412
15805
  }
15806
+ pgids() {
15807
+ return this.groups().map((g) => g.pgid);
15808
+ }
15413
15809
  async stopAll() {
15414
15810
  this.stopped = true;
15415
15811
  const exits = [];
@@ -15478,11 +15874,12 @@ function terminalEnv(base, own, urls) {
15478
15874
  var DEFAULT_AGENT_ID = "claude";
15479
15875
  var PREVIEW_WAKE_MS = 8000;
15480
15876
  var PREVIEW_POLL_MS = 100;
15481
- function defaultAgent(wt, d, preview) {
15877
+ function defaultAgent(wt, d, preview, onProcess) {
15482
15878
  const agent = new AcpSession({
15483
15879
  worktreeId: wt.id,
15484
15880
  cwd: wt.path,
15485
15881
  preview: () => previewContext(preview()),
15882
+ onProcess,
15486
15883
  spec: () => {
15487
15884
  const w = d.state.requireWorktree(wt.id);
15488
15885
  if (!w.agent) {
@@ -15512,13 +15909,22 @@ function defaultAgent(wt, d, preview) {
15512
15909
  d.state.save();
15513
15910
  d.hub.emit("worktreesChanged");
15514
15911
  },
15515
- onPlan: () => {
15912
+ onPlan: async (markdown) => {
15516
15913
  const w = d.state.worktree(wt.id);
15517
- if (!w || w.planned)
15518
- return;
15519
- w.planned = true;
15520
- d.state.save();
15914
+ if (w && !w.planned) {
15915
+ w.planned = true;
15916
+ d.state.save();
15917
+ }
15918
+ const path = markdown.trim() ? await writePlanDoc(wt.path, markdown) : null;
15919
+ const fresh = path ? d.state.worktree(wt.id) : undefined;
15920
+ if (fresh && path && fresh.plan !== path) {
15921
+ fresh.plan = path;
15922
+ d.state.save();
15923
+ d.hub.emit("worktreesChanged");
15924
+ }
15925
+ return path;
15521
15926
  },
15927
+ planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
15522
15928
  option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
15523
15929
  onOptionsLearned: (category, choices) => {
15524
15930
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
@@ -15568,8 +15974,10 @@ class RuntimeRegistry {
15568
15974
  holds = new Map;
15569
15975
  settingUp = new Set;
15570
15976
  starting = new Set;
15977
+ stopping = new Map;
15571
15978
  constructor(deps) {
15572
15979
  this.deps = deps;
15980
+ deps.hub.on("proc", (id) => this.recordGroups(id));
15573
15981
  }
15574
15982
  hold(id, tag) {
15575
15983
  let tags = this.holds.get(id);
@@ -15638,16 +16046,18 @@ class RuntimeRegistry {
15638
16046
  if (repo)
15639
16047
  await this.start(wt, repo);
15640
16048
  }
15641
- async sleep(id) {
16049
+ async sleep(id, why) {
15642
16050
  const rt = this.runtimes.get(id);
15643
16051
  if (!rt?.procs || rt.procs.asleep)
15644
16052
  return;
16053
+ log.info(id, `asleep: ${why}`);
15645
16054
  if (this.ports.ranged()) {
15646
16055
  rt.proxy?.stop();
15647
16056
  rt.proxy = null;
15648
16057
  this.returnLease(id);
15649
16058
  }
15650
- await rt.procs.sleep();
16059
+ await rt.procs.sleep(why);
16060
+ this.recordGroups(id);
15651
16061
  this.deps.hub.emit("worktreesChanged");
15652
16062
  }
15653
16063
  get ports() {
@@ -15704,11 +16114,15 @@ class RuntimeRegistry {
15704
16114
  args: ["-l"]
15705
16115
  };
15706
16116
  try {
15707
- term = (this.deps.makeTerminal ?? defaultTerminal)({ id }, opts, (data) => this.deps.hub.emit("termData", id, SHELL_STREAM, data), (code) => this.deps.hub.emit("termExit", id, SHELL_STREAM, code));
16117
+ term = (this.deps.makeTerminal ?? defaultTerminal)({ id }, opts, (data) => this.deps.hub.emit("termData", id, SHELL_STREAM, data), (code) => {
16118
+ this.deps.hub.emit("termExit", id, SHELL_STREAM, code);
16119
+ this.recordGroups(id);
16120
+ });
15708
16121
  } catch (e) {
15709
16122
  throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
15710
16123
  }
15711
16124
  this.looseShells.set(id, term);
16125
+ this.recordGroups(id);
15712
16126
  } else if (term.cols !== cols || term.rows !== rows) {
15713
16127
  term.resize(cols, rows);
15714
16128
  }
@@ -15723,6 +16137,7 @@ class RuntimeRegistry {
15723
16137
  continue;
15724
16138
  this.looseShells.delete(id);
15725
16139
  fireAndForget(id, Promise.resolve(term.kill()), "loose shell cleanup");
16140
+ this.recordGroups(id);
15726
16141
  }
15727
16142
  }
15728
16143
  get(id) {
@@ -15744,7 +16159,7 @@ class RuntimeRegistry {
15744
16159
  return existing;
15745
16160
  const rt = {
15746
16161
  info: wt,
15747
- agent: (this.deps.makeAgent ?? defaultAgent)(wt, this.deps, () => this.previewStanding(wt.id)),
16162
+ agent: (this.deps.makeAgent ?? defaultAgent)(wt, this.deps, () => this.previewStanding(wt.id), () => this.recordGroups(wt.id)),
15748
16163
  procs: null,
15749
16164
  proxy: null,
15750
16165
  previewName: undefined,
@@ -15760,6 +16175,8 @@ class RuntimeRegistry {
15760
16175
  const rt = this.ensureAgent(wt);
15761
16176
  if (rt.procs)
15762
16177
  return;
16178
+ if (wt.kind === "main" && this.deps.mainLeads && !this.deps.mainLeads(wt.repoId))
16179
+ return;
15763
16180
  const live = this.deps.state.requireWorktree(wt.id);
15764
16181
  const port = this.leasePort(live);
15765
16182
  if (port === null)
@@ -15810,12 +16227,11 @@ class RuntimeRegistry {
15810
16227
  leased = new Map;
15811
16228
  leasePort(wt) {
15812
16229
  let port = this.ports.lease(wt.proxyPort);
15813
- if (port === null && wt.kind !== "spare") {
16230
+ if (port === null && (wt.kind !== "spare" || this.deps.shown?.(wt.id))) {
15814
16231
  const holders = [...this.leased.keys()].filter((id) => id !== wt.id && !this.starting.has(id) && !this.busy(id) && !this.deps.viewed?.(id));
15815
16232
  const victim = oldestViewed(holders, this.deps.state);
15816
16233
  if (victim) {
15817
- log.info(victim.id, `asleep: its preview port went to ${wt.title}`);
15818
- fireAndForget(victim.id, this.sleep(victim.id), "sleep for a preview port");
16234
+ fireAndForget(victim.id, this.sleep(victim.id, `its preview port went to ${wt.title || wt.branch}`), "sleep for a preview port");
15819
16235
  port = this.ports.lease(wt.proxyPort);
15820
16236
  }
15821
16237
  if (port === null) {
@@ -15843,6 +16259,7 @@ class RuntimeRegistry {
15843
16259
  proxy?.stop();
15844
16260
  this.returnLease(id);
15845
16261
  await procs?.stopAll();
16262
+ this.recordGroups(id);
15846
16263
  }
15847
16264
  async stop(id) {
15848
16265
  this.holds.delete(id);
@@ -15851,11 +16268,18 @@ class RuntimeRegistry {
15851
16268
  if (!rt)
15852
16269
  return;
15853
16270
  this.runtimes.delete(id);
16271
+ this.stopping.set(id, rt);
15854
16272
  rt.proxy?.stop();
15855
16273
  this.returnLease(id);
15856
16274
  const login = rt.login;
15857
16275
  rt.login = null;
15858
- await Promise.all([rt.agent.close(), rt.procs?.stopAll(), rt.shell?.kill(), login?.pty.kill()]);
16276
+ try {
16277
+ await Promise.all([rt.agent.close(), rt.procs?.stopAll(), rt.shell?.kill(), login?.pty.kill()]);
16278
+ } finally {
16279
+ if (this.stopping.get(id) === rt)
16280
+ this.stopping.delete(id);
16281
+ this.recordGroups(id);
16282
+ }
15859
16283
  }
15860
16284
  openTerminal(id, stream, cols, rows) {
15861
16285
  if (stream === LOGIN_STREAM)
@@ -15863,12 +16287,10 @@ class RuntimeRegistry {
15863
16287
  if (stream !== SHELL_STREAM)
15864
16288
  return this.openProcStream(id, stream, cols, rows);
15865
16289
  const wt = this.deps.state.requireWorktree(id);
15866
- if (wt.kind === "spare")
15867
- throw new UserError("no terminal for a spare worktree");
15868
16290
  const rt = this.ensureAgent(wt);
15869
16291
  let term = rt.shell;
15870
16292
  if (!term?.alive) {
15871
- const cwd = wt.linkPath ?? wt.path;
16293
+ const cwd = wt.path;
15872
16294
  const opts = {
15873
16295
  cwd,
15874
16296
  env: { ...this.shellEnv(wt), PWD: cwd },
@@ -15878,11 +16300,15 @@ class RuntimeRegistry {
15878
16300
  args: ["-l"]
15879
16301
  };
15880
16302
  try {
15881
- term = (this.deps.makeTerminal ?? defaultTerminal)(wt, opts, (data) => this.deps.hub.emit("termData", wt.id, SHELL_STREAM, data), (code) => this.deps.hub.emit("termExit", wt.id, SHELL_STREAM, code));
16303
+ term = (this.deps.makeTerminal ?? defaultTerminal)(wt, opts, (data) => this.deps.hub.emit("termData", wt.id, SHELL_STREAM, data), (code) => {
16304
+ this.deps.hub.emit("termExit", wt.id, SHELL_STREAM, code);
16305
+ this.recordGroups(wt.id);
16306
+ });
15882
16307
  } catch (e) {
15883
16308
  throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
15884
16309
  }
15885
16310
  rt.shell = term;
16311
+ this.recordGroups(wt.id);
15886
16312
  } else if (term.cols !== cols || term.rows !== rows) {
15887
16313
  term.resize(cols, rows);
15888
16314
  }
@@ -15895,7 +16321,7 @@ class RuntimeRegistry {
15895
16321
  rt.login = null;
15896
16322
  if (prev)
15897
16323
  fireAndForget(id, Promise.resolve(prev.pty.kill()), "replace the agent login");
15898
- const cwd = wt.linkPath ?? wt.path;
16324
+ const cwd = wt.path;
15899
16325
  let pty;
15900
16326
  try {
15901
16327
  pty = (this.deps.makeTerminal ?? defaultTerminal)(wt, {
@@ -15907,6 +16333,7 @@ class RuntimeRegistry {
15907
16333
  args: run.args
15908
16334
  }, (data) => this.deps.hub.emit("termData", id, LOGIN_STREAM, data), (code) => {
15909
16335
  this.deps.hub.emit("termExit", id, LOGIN_STREAM, code);
16336
+ this.recordGroups(id);
15910
16337
  if (rt.login?.pty !== pty)
15911
16338
  return;
15912
16339
  if (code === 0) {
@@ -15919,6 +16346,7 @@ class RuntimeRegistry {
15919
16346
  throw new UserError(`could not start the login: ${e instanceof Error ? e.message : String(e)}`);
15920
16347
  }
15921
16348
  rt.login = { pty, run };
16349
+ this.recordGroups(id);
15922
16350
  this.deps.hub.emit("worktreesChanged");
15923
16351
  }
15924
16352
  openLoginStream(id, cols, rows) {
@@ -15994,12 +16422,93 @@ class RuntimeRegistry {
15994
16422
  const url = `http://${host.includes(":") ? `[${host}]` : host}:${st.boundPort ?? st.port}`;
15995
16423
  return { status: st.status, url, ...st.detail ? { detail: st.detail } : {} };
15996
16424
  }
16425
+ recordGroups(id) {
16426
+ const rt = this.runtimes.get(id) ?? this.stopping.get(id);
16427
+ const live = [];
16428
+ if (rt) {
16429
+ for (const g of rt.procs?.groups() ?? [])
16430
+ live.push(g);
16431
+ if (rt.agent.pgid !== null) {
16432
+ const agentId = this.deps.state.worktree(id)?.agent ?? this.deps.state.defaultAgent ?? DEFAULT_AGENT_ID;
16433
+ live.push({ name: `agent:${agentId}`, pgid: rt.agent.pgid });
16434
+ }
16435
+ if (rt.shell?.alive)
16436
+ live.push({ name: "shell", pgid: rt.shell.pid });
16437
+ if (rt.login?.pty.alive)
16438
+ live.push({ name: "login", pgid: rt.login.pty.pid });
16439
+ }
16440
+ const loose = this.looseShells.get(id);
16441
+ if (loose?.alive)
16442
+ live.push({ name: "shell", pgid: loose.pid });
16443
+ const before = new Map(this.deps.state.groups(id).map((g) => [g.pgid, g]));
16444
+ const now = Date.now();
16445
+ const next = live.map((g) => ({ ...g, startedAt: before.get(g.pgid)?.startedAt ?? now }));
16446
+ this.deps.state.setGroups(id, next);
16447
+ }
16448
+ async reclaimOrphans() {
16449
+ const machine = this.deps.machine ?? { bootId, psGroups };
16450
+ const [boot, rows] = await Promise.all([machine.bootId(), machine.psGroups()]);
16451
+ const state = this.deps.state;
16452
+ const found = orphansIn(state.allGroups(), rows, boot, state.bootAt, Date.now());
16453
+ const seen = new Set(found.map((o) => o.pgid));
16454
+ for (const row of strayAdapters(rows, this.deps.paths.agentsDir)) {
16455
+ if (seen.has(row.pgid))
16456
+ continue;
16457
+ seen.add(row.pgid);
16458
+ found.push({ worktreeId: "daemon", pgid: row.pgid, name: "adapter" });
16459
+ }
16460
+ const reclaim = this.deps.reclaim ?? reclaimGroup;
16461
+ await Promise.all(found.map(async (o) => {
16462
+ await reclaim(o.pgid);
16463
+ log.info(o.worktreeId, `reclaimed ${o.name} group ${o.pgid} left by the last daemon`);
16464
+ }));
16465
+ state.clearGroups();
16466
+ state.setBootAt(boot);
16467
+ }
15997
16468
  async shutdown() {
15998
16469
  this.pruneLooseShells(new Set);
15999
16470
  await Promise.all([...this.runtimes.keys()].map((id) => this.stop(id)));
16000
16471
  }
16001
16472
  }
16002
16473
 
16474
+ // ../daemon/src/worktrees/naming.ts
16475
+ import { randomBytes } from "crypto";
16476
+ import { existsSync as existsSync10 } from "fs";
16477
+ import { join as join8 } from "path";
16478
+ function shortId() {
16479
+ return randomBytes(5).toString("hex");
16480
+ }
16481
+ function freeSlot(parent) {
16482
+ for (;; ) {
16483
+ const id = shortId();
16484
+ const dir = `wt-${id.slice(0, 4)}`;
16485
+ if (!existsSync10(join8(parent, dir)))
16486
+ return { id, dir };
16487
+ }
16488
+ }
16489
+ function titleFrom(prompt) {
16490
+ const words = prompt.split(/\s+/).filter(Boolean).slice(0, 3).join(" ");
16491
+ return sentenceCase(cleanTitle2(words) || "task");
16492
+ }
16493
+ function cleanTitle2(title) {
16494
+ return title.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").slice(0, 40).replace(/^[\s:;,.!?]+|[\s:;,.!?]+$/g, "");
16495
+ }
16496
+ function branchSlug(title) {
16497
+ return title.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
16498
+ }
16499
+ function sentenceCase(title) {
16500
+ return title.charAt(0).toUpperCase() + title.slice(1);
16501
+ }
16502
+ var VARIANT_LENSES = [
16503
+ "Take the most direct approach: the change most people would expect, in the smallest diff.",
16504
+ "Lean toward how it looks and reads: polish what the task touches using only the design tokens, components and voice the project already has. Add no new colours, fonts or effects, and restyle nothing outside the task.",
16505
+ "Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
16506
+ ];
16507
+ function variantLens(index) {
16508
+ const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
16509
+ return `(You are attempt ${index} of several parallel attempts at this task. Stay within the task and follow the project's existing styles, components, tokens and writing conventions; the attempts differ in emphasis, not in how much they change. ${lens})`;
16510
+ }
16511
+
16003
16512
  // ../daemon/src/agent/recap.ts
16004
16513
  var REPLY_KEEP = 2000;
16005
16514
  var CLIP = 160;
@@ -16015,6 +16524,7 @@ function turnsSince(entries, seenAt) {
16015
16524
  let pending = [];
16016
16525
  let open = null;
16017
16526
  const writes = new Set;
16527
+ const refusals = new Map;
16018
16528
  const start = (ts) => {
16019
16529
  const t = slice(pending, ts);
16020
16530
  pending = [];
@@ -16035,16 +16545,35 @@ function turnsSince(entries, seenAt) {
16035
16545
  open = start(e.ts);
16036
16546
  break;
16037
16547
  case "text-delta":
16038
- if (open)
16548
+ if (open) {
16039
16549
  open.reply = (open.reply + e.text).slice(-REPLY_KEEP);
16550
+ open.refused = undefined;
16551
+ }
16040
16552
  break;
16041
16553
  case "tool-start":
16042
16554
  if (!open)
16043
16555
  break;
16044
16556
  if (!e.parentToolId)
16045
16557
  open.reply = "";
16558
+ open.refused = undefined;
16046
16559
  countWrite(open, writes, e.toolId, e);
16047
16560
  break;
16561
+ case "agent-permission":
16562
+ refusals.set(e.id, new Set(e.choices.filter((c) => c.kind.startsWith("reject")).map((c) => c.id)));
16563
+ if (open)
16564
+ open.refused = undefined;
16565
+ break;
16566
+ case "agent-question":
16567
+ if (open)
16568
+ open.refused = undefined;
16569
+ break;
16570
+ case "agent-ask-end": {
16571
+ const refusing = refusals.get(e.id);
16572
+ refusals.delete(e.id);
16573
+ if (open && e.outcome === "answered" && e.choiceId && refusing?.has(e.choiceId))
16574
+ open.refused = true;
16575
+ break;
16576
+ }
16048
16577
  case "tool-update":
16049
16578
  if (open && e.kind)
16050
16579
  countWrite(open, writes, e.toolId, { name: e.name ?? "", kind: e.kind });
@@ -16101,7 +16630,8 @@ function factsOf(turns, end, ask) {
16101
16630
  ...end === "failed" && last?.error ? { error: last.error } : {},
16102
16631
  ...end === "failed" && last?.auth ? { auth: true } : {},
16103
16632
  ...end === "done" && cut ? { cut } : {},
16104
- ...end === "asking" && ask ? { ask } : {}
16633
+ ...end === "asking" && ask ? { ask } : {},
16634
+ ...end === "stopped" && last?.refused ? { planBack: true } : {}
16105
16635
  };
16106
16636
  }
16107
16637
  function firstAskOf(entries) {
@@ -16110,59 +16640,6 @@ function firstAskOf(entries) {
16110
16640
  return e.text;
16111
16641
  return;
16112
16642
  }
16113
- var RECAP_SYSTEM = "You write one-line status recaps for a developer coming back to a coding task. Reply with only the recap.";
16114
- var RECAP_ASK = "The user stepped away and is coming back. Recap in one sentence, under 20 words, no markdown. Say what the task is, then what just happened or what to do next. Skip root-cause narrative, fix internals and secondary to-dos.";
16115
- var RECAP_TURNS = 6;
16116
- var RECAP_CHARS = 4000;
16117
- function recapPrompt(i) {
16118
- const head = [RECAP_ASK, "", `Task: ${clip(i.title, 200)}`];
16119
- if (i.firstAsk)
16120
- head.push(`First request: ${clip(i.firstAsk, 300)}`);
16121
- const now = `Now: ${standing(i.end, i.facts)}`;
16122
- let blocks = i.turns.slice(-RECAP_TURNS).map(turnBlock);
16123
- const size = () => [...head, "", ...blocks, "", now].join(`
16124
-
16125
- `).length;
16126
- while (blocks.length > 1 && size() > RECAP_CHARS)
16127
- blocks = blocks.slice(1);
16128
- return [head.join(`
16129
- `), blocks.join(`
16130
-
16131
- `), now].join(`
16132
-
16133
- `);
16134
- }
16135
- function turnBlock(t) {
16136
- const facts = [t.edits ? plural(t.edits, "edit") : "", t.toolErrors ? plural(t.toolErrors, "failed tool") : ""].filter(Boolean).join(", ");
16137
- const lines = [];
16138
- if (t.asks.length)
16139
- lines.push(`You asked: ${clip(t.asks.join(" / "), 400)}`);
16140
- if (t.reply.trim())
16141
- lines.push(`Agent ended with: ${tail(t.reply, 800)}`);
16142
- if (facts)
16143
- lines.push(`Facts: ${facts}`);
16144
- return lines.join(`
16145
- `);
16146
- }
16147
- function standing(end, f) {
16148
- switch (end) {
16149
- case "asking":
16150
- return f.ask ? `waiting for the user's answer to "${f.ask}"` : "waiting for the user";
16151
- case "failed":
16152
- return f.error ? `failed: ${f.error}` : "failed";
16153
- case "stopped":
16154
- return "stopped before it finished";
16155
- case "done":
16156
- return f.cut ? `finished early (${f.cut})` : "finished";
16157
- }
16158
- }
16159
- function plural(n, word) {
16160
- return `${n} ${word}${n === 1 ? "" : "s"}`;
16161
- }
16162
- function tail(text, max) {
16163
- const line = text.replace(/\s+/g, " ").trim();
16164
- return line.length > max ? `\u2026${line.slice(-(max - 1))}` : line;
16165
- }
16166
16643
  var RECAP_MAX = 160;
16167
16644
  function parseRecap(text) {
16168
16645
  if (!text)
@@ -16179,15 +16656,17 @@ function parseRecap(text) {
16179
16656
  }
16180
16657
 
16181
16658
  // ../daemon/src/agent/landing.ts
16182
- var LAND_SYSTEM = "You judge whether a coding task is finished and write its commit message. Reply in exactly the format asked, nothing else.";
16659
+ var LAND_SYSTEM = "You judge whether a coding task is finished, recap where it stands, and write its commit message. Reply in exactly the format asked, nothing else.";
16183
16660
  var LAND_ASK = [
16184
16661
  "A coding agent just stopped in a git worktree. Decide whether the work is finished and ready to merge, or still in progress: a question the agent is waiting on, a step it said it would do next, or a part of the request it did not get to.",
16185
16662
  "The agent never commits: every change is left uncommitted for the user, and the agent saying so is normal, not a sign the work is unfinished.",
16186
16663
  "Reply with exactly this shape and nothing else:",
16187
- "Line 1: READY, or NOT READY: <reason under 12 words>",
16664
+ "Line 1: READY, or NOT READY: <what is still left to do, under 8 words, about the change itself and not the conversation>",
16188
16665
  "Line 2: blank",
16189
- "Line 3: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
16666
+ "Line 3: Recap: one sentence under 20 words for the user coming back to this task, saying what the task is and then what just happened or what to do next. Skip root-cause narrative, fix internals and secondary to-dos.",
16190
16667
  "Line 4: blank",
16668
+ "Line 5: a commit subject under 60 characters, lowercase, imperative, in the style of the recent subjects when given",
16669
+ "Line 6: blank",
16191
16670
  "Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
16192
16671
  ].join(`
16193
16672
  `);
@@ -16203,7 +16682,7 @@ ${i.recentSubjects.map((s) => ` ${s}`).join(`
16203
16682
  `)}`);
16204
16683
  const diff = `Diff summary:
16205
16684
  ${i.diffStat.trim() || "(none)"}`;
16206
- let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock2);
16685
+ let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
16207
16686
  const size = () => [...head, "", ...blocks, "", diff].join(`
16208
16687
 
16209
16688
  `).length;
@@ -16216,13 +16695,36 @@ ${i.diffStat.trim() || "(none)"}`;
16216
16695
 
16217
16696
  `);
16218
16697
  }
16219
- function turnBlock2(t) {
16698
+ function turnBlock(t) {
16220
16699
  const lines = [];
16221
16700
  if (t.asks.length)
16222
16701
  lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
16223
16702
  if (t.reply.trim())
16224
16703
  lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
16225
16704
  return lines.join(`
16705
+ `);
16706
+ }
16707
+ var ANSWER_SYSTEM = "You recap where a coding task stands in one sentence. Reply with the sentence only.";
16708
+ var ANSWER_ASK = [
16709
+ "A coding agent just replied in a git worktree without changing any files: it answered a question, reviewed something, or explained what it found.",
16710
+ "Write one sentence under 20 words for the user coming back to this task, saying what was asked and what the agent answered or recommended. Skip narrative and secondary points. No markdown, no dashes as punctuation."
16711
+ ].join(`
16712
+ `);
16713
+ function answerPrompt(i) {
16714
+ const head = [ANSWER_ASK, "", `Task: ${clip(i.title, 200)}`];
16715
+ if (i.firstAsk)
16716
+ head.push(`First request: ${clip(i.firstAsk, 300)}`);
16717
+ let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
16718
+ const size = () => [...head, "", ...blocks].join(`
16719
+
16720
+ `).length;
16721
+ while (blocks.length > 1 && size() > LAND_CHARS)
16722
+ blocks = blocks.slice(1);
16723
+ return [head.join(`
16724
+ `), blocks.join(`
16725
+
16726
+ `)].join(`
16727
+
16226
16728
  `);
16227
16729
  }
16228
16730
  var SUBJECT_MAX = 72;
@@ -16239,13 +16741,17 @@ function parseLanding(text) {
16239
16741
  const rest = lines.slice(lines.indexOf(lines.find((l) => l.trim() === first) ?? "") + 1);
16240
16742
  const paragraphs = rest.join(`
16241
16743
  `).split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
16242
- const subject = paragraphs[0]?.replace(/\s+/g, " ").replace(/^(subject|title)\s*:\s*/i, "").replace(/[.]+$/, "").slice(0, SUBJECT_MAX).trim();
16243
- const body = paragraphs.slice(1).join(`
16744
+ const hasRecap = /^recap\s*:/i.test(paragraphs[0] ?? "") || paragraphs.length >= 3;
16745
+ const recap = hasRecap ? parseRecap(paragraphs[0] ?? null) : null;
16746
+ const message = hasRecap ? paragraphs.slice(1) : paragraphs;
16747
+ const subject = message[0]?.replace(/\s+/g, " ").replace(/^(subject|title)\s*:\s*/i, "").replace(/[.]+$/, "").slice(0, SUBJECT_MAX).trim();
16748
+ const body = message.slice(1).join(`
16244
16749
 
16245
16750
  `).replace(/^(body)\s*:\s*/i, "").trim();
16246
16751
  return {
16247
16752
  ready: verdict === "ready",
16248
16753
  ...why ? { why: clip(why, 120) } : {},
16754
+ ...recap ? { recap } : {},
16249
16755
  ...subject && subject.split(" ").length >= 2 ? { subject } : {},
16250
16756
  ...body ? { body: body.slice(0, 1500) } : {}
16251
16757
  };
@@ -16299,7 +16805,7 @@ async function askFreshAgent(agents, agentId, cwd, system, prompt, opts = {}) {
16299
16805
 
16300
16806
  // ../daemon/src/agent/tasks.ts
16301
16807
  var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
16302
- var namePrompt = (task) => `Name this coding task the way a teammate would say it out loud: 1 or 2 lowercase kebab-case words, 16 characters at most (like "sticky-header", "dark-mode", "price-badge"). Leave out words every task in the project would share, like the product or the app. Reply with ONLY the name, nothing else.
16808
+ var namePrompt = (task) => `Name this coding task the way a teammate would say it out loud: 1 or 2 plain lowercase words, 16 characters at most (like "sticky header", "dark mode", "price badge"). Leave out words every task in the project would share, like the product or the app. Reply with ONLY the name, nothing else.
16303
16809
 
16304
16810
  Task: ${task.slice(0, 500)}`;
16305
16811
  function taskText(text, attachments = []) {
@@ -16321,13 +16827,13 @@ function parseName(text) {
16321
16827
  return null;
16322
16828
  const line = text.trim().split(`
16323
16829
  `).at(-1) ?? "";
16324
- const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
16325
- const words = name.split("-").filter(Boolean);
16830
+ const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
16831
+ const words = name.split(" ").filter(Boolean);
16326
16832
  if (words.length < 1 || words.length > 3 || name.length < 3 || name.length > 20)
16327
16833
  return null;
16328
16834
  if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
16329
16835
  return null;
16330
- return name;
16836
+ return sentenceCase(name);
16331
16837
  }
16332
16838
  var PLAN_SYSTEM = "You are a task-planning assistant. Reply with only the requested JSON.";
16333
16839
  var planPrompt = (request) => [
@@ -16357,15 +16863,6 @@ function parsePlan(text) {
16357
16863
  function makeNamer(runtime) {
16358
16864
  return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
16359
16865
  }
16360
- function makeRecapper(runtime, agents, state) {
16361
- return async (wt, prompt) => {
16362
- const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
16363
- if (!spec || lacksQuickModel(spec, state))
16364
- return null;
16365
- const agent = runtime.agentFor(wt.id);
16366
- return agent ? parseRecap(await agent.ask(RECAP_SYSTEM, prompt, { quick: "require" })) : null;
16367
- };
16368
- }
16369
16866
  function lacksQuickModel(spec, state) {
16370
16867
  const quick = spec.quickModel;
16371
16868
  if (!quick)
@@ -16375,43 +16872,51 @@ function lacksQuickModel(spec, state) {
16375
16872
  const offered = state.cachedOptions(spec.id, "model");
16376
16873
  return offered.length > 0 && !offered.some((m) => m.id === quick);
16377
16874
  }
16378
- function makeLander(runtime, agents, state) {
16379
- return async (wt, prompt) => {
16875
+ function makeQuickAsker(runtime, agents, state) {
16876
+ return async (wt, system, prompt) => {
16380
16877
  const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
16381
16878
  if (!spec || lacksQuickModel(spec, state))
16382
16879
  return null;
16383
16880
  const agent = runtime.agentFor(wt.id);
16384
- return agent ? parseLanding(await agent.ask(LAND_SYSTEM, prompt, { quick: "require" })) : null;
16881
+ return agent ? agent.ask(system, prompt, { quick: "require" }) : null;
16385
16882
  };
16386
16883
  }
16884
+ function makeLander(...deps) {
16885
+ const ask = makeQuickAsker(...deps);
16886
+ return async (wt, prompt) => parseLanding(await ask(wt, LAND_SYSTEM, prompt));
16887
+ }
16888
+ function makeAnswerRecapper(...deps) {
16889
+ const ask = makeQuickAsker(...deps);
16890
+ return async (wt, prompt) => parseRecap(await ask(wt, ANSWER_SYSTEM, prompt));
16891
+ }
16387
16892
  function makePlanner(agents) {
16388
16893
  return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
16389
16894
  }
16390
16895
 
16391
16896
  // ../daemon/src/core/assets.ts
16392
- import { existsSync as existsSync9, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
16393
- import { join as join7, resolve as resolve5 } from "path";
16897
+ import { existsSync as existsSync11, readdirSync as readdirSync3, rmSync, statSync as statSync2 } from "fs";
16898
+ import { join as join9, resolve as resolve5 } from "path";
16394
16899
  function locateAssets(here, env = process.env) {
16395
- const packaged = existsSync9(join7(here, "shell", "index.html"));
16900
+ const packaged = existsSync11(join9(here, "shell", "index.html"));
16396
16901
  return {
16397
- shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join7(here, "shell") : join7(here, "../../shell/dist")),
16398
- bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join7(here, "bridge.js") : join7(here, "../../bridge/dist/bridge.js")),
16902
+ shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join9(here, "shell") : join9(here, "../../shell/dist")),
16903
+ bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join9(here, "bridge.js") : join9(here, "../../bridge/dist/bridge.js")),
16399
16904
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
16400
- packageJson: packaged ? join7(here, "..", "package.json") : null
16905
+ packageJson: packaged ? join9(here, "..", "package.json") : null
16401
16906
  };
16402
16907
  }
16403
16908
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
16404
16909
  function pruneAssets(shellDist) {
16405
- const dir = join7(shellDist, "assets");
16910
+ const dir = join9(shellDist, "assets");
16406
16911
  let names;
16407
16912
  try {
16408
- names = readdirSync2(dir);
16913
+ names = readdirSync3(dir);
16409
16914
  } catch {
16410
16915
  return 0;
16411
16916
  }
16412
16917
  const found = [];
16413
16918
  for (const name of names) {
16414
- const path = join7(dir, name);
16919
+ const path = join9(dir, name);
16415
16920
  try {
16416
16921
  found.push({ path, mtime: statSync2(path).mtimeMs });
16417
16922
  } catch {}
@@ -16432,7 +16937,7 @@ function pruneAssets(shellDist) {
16432
16937
  }
16433
16938
 
16434
16939
  // ../daemon/src/core/dialog.ts
16435
- import { existsSync as existsSync10 } from "fs";
16940
+ import { existsSync as existsSync12 } from "fs";
16436
16941
  import { homedir as homedir4 } from "os";
16437
16942
  var PROMPTS = {
16438
16943
  location: "Choose where the new project goes",
@@ -16450,7 +16955,7 @@ function folderDialog() {
16450
16955
  let cancelled = false;
16451
16956
  const ask = async (start, purpose) => {
16452
16957
  const typed = start === "~" || start.startsWith("~/") ? `${homedir4()}${start.slice(1)}` : start;
16453
- const from = typed.startsWith("/") && existsSync10(typed) ? typed : homedir4();
16958
+ const from = typed.startsWith("/") && existsSync12(typed) ? typed : homedir4();
16454
16959
  cancelled = false;
16455
16960
  const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
16456
16961
  stdout: "pipe",
@@ -16554,12 +17059,12 @@ class Restarter {
16554
17059
  d.hub.on("agentStatus", settle);
16555
17060
  d.hub.on("worktreesChanged", settle);
16556
17061
  }
16557
- request() {
17062
+ request(opts = {}) {
16558
17063
  const refused = this.d.refusal();
16559
17064
  if (refused)
16560
17065
  return refused;
16561
17066
  this.asked = true;
16562
- this.attempt();
17067
+ this.attempt(opts.now);
16563
17068
  return null;
16564
17069
  }
16565
17070
  waitingOn() {
@@ -16568,13 +17073,18 @@ class Restarter {
16568
17073
  return this.gone ? [] : this.working();
16569
17074
  }
16570
17075
  working() {
16571
- return this.d.state.worktrees.filter((w) => this.d.runtime.agentFor(w.id)?.status === "working").map((w) => w.title);
17076
+ return this.titled("working");
17077
+ }
17078
+ asking() {
17079
+ return this.titled("waiting");
16572
17080
  }
16573
- attempt() {
17081
+ titled(status) {
17082
+ return this.d.state.worktrees.filter((w) => this.d.runtime.agentFor(w.id)?.status === status).map((w) => w.title);
17083
+ }
17084
+ attempt(now = false) {
16574
17085
  if (this.gone)
16575
17086
  return;
16576
- const names = this.working();
16577
- if (names.length === 0)
17087
+ if (now || this.working().length === 0)
16578
17088
  this.gone = true;
16579
17089
  const key = JSON.stringify(this.waitingOn());
16580
17090
  if (key !== this.announced) {
@@ -16666,18 +17176,19 @@ class SelfWatch {
16666
17176
  }
16667
17177
 
16668
17178
  // ../daemon/src/core/state.ts
16669
- import { randomBytes } from "crypto";
16670
- import { existsSync as existsSync11, readFileSync as readFileSync8, renameSync as renameSync2, writeFileSync as writeFileSync4 } from "fs";
17179
+ import { randomBytes as randomBytes2 } from "crypto";
17180
+ import { existsSync as existsSync13, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync5 } from "fs";
17181
+ var GROUPS_SAVE_MS = 250;
16671
17182
  var SEEN_WORKTREES = 50;
16672
17183
  var empty = { repos: [], worktrees: [], sessions: {} };
16673
17184
  function loadState(paths) {
16674
17185
  ensureDirs(paths);
16675
17186
  const STATE_FILE = paths.stateFile;
16676
- if (!existsSync11(STATE_FILE))
17187
+ if (!existsSync13(STATE_FILE))
16677
17188
  return structuredClone(empty);
16678
17189
  let raw;
16679
17190
  try {
16680
- raw = readFileSync8(STATE_FILE, "utf8");
17191
+ raw = readFileSync9(STATE_FILE, "utf8");
16681
17192
  } catch (e) {
16682
17193
  log.error("state", `cannot read ${STATE_FILE}; starting empty`, e);
16683
17194
  return structuredClone(empty);
@@ -16688,7 +17199,7 @@ function loadState(paths) {
16688
17199
  } catch (e) {
16689
17200
  const backup = `${STATE_FILE}.corrupt-${Date.now()}`;
16690
17201
  try {
16691
- writeFileSync4(backup, raw);
17202
+ writeFileSync5(backup, raw);
16692
17203
  } catch (be) {
16693
17204
  log.warn("state", "could not write the corrupt-state backup", be);
16694
17205
  }
@@ -16703,6 +17214,10 @@ function loadState(paths) {
16703
17214
  if (!ids.has(id) && !id.startsWith("disc-"))
16704
17215
  delete state.seen?.[id];
16705
17216
  }
17217
+ for (const id of Object.keys(state.groups ?? {})) {
17218
+ if (!ids.has(id) && !id.startsWith("disc-"))
17219
+ delete state.groups?.[id];
17220
+ }
16706
17221
  if (state.modelCache) {
16707
17222
  state.optionCache ??= {};
16708
17223
  for (const [agentId, models] of Object.entries(state.modelCache)) {
@@ -16715,22 +17230,23 @@ function loadState(paths) {
16715
17230
  }
16716
17231
  function saveState(paths, state) {
16717
17232
  const tmp = `${paths.stateFile}.tmp`;
16718
- writeFileSync4(tmp, JSON.stringify(state, null, 2));
17233
+ writeFileSync5(tmp, JSON.stringify(state, null, 2));
16719
17234
  renameSync2(tmp, paths.stateFile);
16720
17235
  }
16721
17236
  function loadOrCreateToken(paths) {
16722
17237
  ensureDirs(paths);
16723
17238
  const TOKEN_FILE = paths.tokenFile;
16724
- if (existsSync11(TOKEN_FILE))
16725
- return readFileSync8(TOKEN_FILE, "utf8").trim();
16726
- const token = randomBytes(32).toString("hex");
16727
- writeFileSync4(TOKEN_FILE, token, { mode: 384 });
17239
+ if (existsSync13(TOKEN_FILE))
17240
+ return readFileSync9(TOKEN_FILE, "utf8").trim();
17241
+ const token = randomBytes2(32).toString("hex");
17242
+ writeFileSync5(TOKEN_FILE, token, { mode: 384 });
16728
17243
  return token;
16729
17244
  }
16730
17245
 
16731
17246
  class StateStore {
16732
17247
  paths;
16733
17248
  state;
17249
+ groupsTimer = null;
16734
17250
  constructor(paths, state) {
16735
17251
  this.paths = paths;
16736
17252
  this.state = state ?? loadState(paths);
@@ -16813,6 +17329,55 @@ class StateStore {
16813
17329
  delete this.state.sessions[id];
16814
17330
  if (this.state.seen)
16815
17331
  delete this.state.seen[id];
17332
+ if (this.state.groups)
17333
+ delete this.state.groups[id];
17334
+ this.save();
17335
+ }
17336
+ groups(worktreeId) {
17337
+ return this.state.groups?.[worktreeId] ?? [];
17338
+ }
17339
+ setGroups(worktreeId, list) {
17340
+ this.state.groups ??= {};
17341
+ if (list.length === 0)
17342
+ delete this.state.groups[worktreeId];
17343
+ else
17344
+ this.state.groups[worktreeId] = list;
17345
+ if (this.groupsTimer)
17346
+ return;
17347
+ const t = setTimeout(() => {
17348
+ try {
17349
+ this.save();
17350
+ } catch (e) {
17351
+ log.warn("state", "could not write the process group ledger", e);
17352
+ }
17353
+ }, GROUPS_SAVE_MS);
17354
+ t.unref?.();
17355
+ this.groupsTimer = t;
17356
+ }
17357
+ flushGroups() {
17358
+ if (this.groupsTimer)
17359
+ this.save();
17360
+ }
17361
+ allGroups() {
17362
+ const out = [];
17363
+ for (const [worktreeId, list] of Object.entries(this.state.groups ?? {})) {
17364
+ for (const g of list)
17365
+ out.push({ ...g, worktreeId });
17366
+ }
17367
+ return out;
17368
+ }
17369
+ clearGroups() {
17370
+ delete this.state.groups;
17371
+ this.save();
17372
+ }
17373
+ get bootAt() {
17374
+ return this.state.bootAt;
17375
+ }
17376
+ setBootAt(id) {
17377
+ if (id === null)
17378
+ delete this.state.bootAt;
17379
+ else
17380
+ this.state.bootAt = id;
16816
17381
  this.save();
16817
17382
  }
16818
17383
  session(worktreeId) {
@@ -16873,12 +17438,15 @@ class StateStore {
16873
17438
  this.save();
16874
17439
  }
16875
17440
  save() {
17441
+ if (this.groupsTimer)
17442
+ clearTimeout(this.groupsTimer);
17443
+ this.groupsTimer = null;
16876
17444
  saveState(this.paths, this.state);
16877
17445
  }
16878
17446
  }
16879
17447
 
16880
17448
  // ../daemon/src/design/service.ts
16881
- import { join as join8 } from "path";
17449
+ import { join as join10 } from "path";
16882
17450
 
16883
17451
  // ../daemon/src/design/scan.ts
16884
17452
  var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
@@ -17328,7 +17896,7 @@ class DesignService {
17328
17896
  async function readAll(root, paths) {
17329
17897
  const out = [];
17330
17898
  for (const path of paths) {
17331
- const file = Bun.file(join8(root, path));
17899
+ const file = Bun.file(join10(root, path));
17332
17900
  if (file.size === 0 || file.size > MAX_FILE_BYTES)
17333
17901
  continue;
17334
17902
  try {
@@ -17443,14 +18011,14 @@ function typicalUse(classes) {
17443
18011
  var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
17444
18012
  async function loadProjectTypescript(root) {
17445
18013
  try {
17446
- return await import(join8(root, "node_modules", "typescript", "lib", "typescript.js"));
18014
+ return await import(join10(root, "node_modules", "typescript", "lib", "typescript.js"));
17447
18015
  } catch {
17448
18016
  return null;
17449
18017
  }
17450
18018
  }
17451
18019
 
17452
18020
  // ../daemon/src/drafts/store.ts
17453
- import { existsSync as existsSync12, readFileSync as readFileSync9, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "fs";
18021
+ import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
17454
18022
  var SAVE_DELAY_MS = 1000;
17455
18023
 
17456
18024
  class DraftStore {
@@ -17459,10 +18027,10 @@ class DraftStore {
17459
18027
  saveTimer = null;
17460
18028
  constructor(d) {
17461
18029
  this.d = d;
17462
- if (!existsSync12(d.file))
18030
+ if (!existsSync14(d.file))
17463
18031
  return;
17464
18032
  try {
17465
- const raw = JSON.parse(readFileSync9(d.file, "utf8"));
18033
+ const raw = JSON.parse(readFileSync10(d.file, "utf8"));
17466
18034
  if (!raw || typeof raw !== "object")
17467
18035
  return;
17468
18036
  for (const [id, text] of Object.entries(raw)) {
@@ -17479,6 +18047,9 @@ class DraftStore {
17479
18047
  has(boxId) {
17480
18048
  return !!this.drafts[boxId]?.trim();
17481
18049
  }
18050
+ text(boxId) {
18051
+ return this.drafts[boxId] ?? "";
18052
+ }
17482
18053
  set(boxId, text, clientId) {
17483
18054
  if ((this.drafts[boxId] ?? "") === text)
17484
18055
  return;
@@ -17522,7 +18093,7 @@ class DraftStore {
17522
18093
  write() {
17523
18094
  const tmp = `${this.d.file}.tmp`;
17524
18095
  try {
17525
- writeFileSync5(tmp, JSON.stringify(this.drafts));
18096
+ writeFileSync6(tmp, JSON.stringify(this.drafts));
17526
18097
  renameSync3(tmp, this.d.file);
17527
18098
  } catch (e) {
17528
18099
  log.error("drafts", "could not write the drafts", e);
@@ -17546,16 +18117,20 @@ class ExecService {
17546
18117
  run(worktreeId, command) {
17547
18118
  fireAndForget(worktreeId, this.exec(worktreeId, command), `exec ${command}`);
17548
18119
  }
17549
- exec(worktreeId, command, name = SHELL_TOOL) {
18120
+ exec(worktreeId, command, name = SHELL_TOOL, opts = {}) {
17550
18121
  const wt = this.deps.state.requireWorktree(worktreeId);
17551
18122
  if (wt.kind === "spare")
17552
18123
  throw new UserError("no shell for a spare worktree");
18124
+ if (wt.kind === "main")
18125
+ throw new UserError("nothing runs on main: the plus starts a worktree for it");
17553
18126
  const agent = this.deps.runtime.agentFor(worktreeId);
17554
18127
  if (!agent)
17555
18128
  throw new UserError("worktree still starting; try again in a moment");
17556
18129
  const toolId = `${name}-${Date.now().toString(36)}-${++this.n}`;
17557
- agent.note({ type: "tool-start", toolId, name, input: { command }, kind: "execute" });
17558
- const cwd = wt.linkPath ?? wt.path;
18130
+ const start = { type: "tool-start", toolId, name, input: { command }, kind: "execute" };
18131
+ if (!opts.quiet)
18132
+ agent.note(start);
18133
+ const cwd = wt.path;
17559
18134
  const env = { ...this.deps.runtime.shellEnv(wt), PWD: cwd, TERM: "dumb", NO_COLOR: "1" };
17560
18135
  let proc;
17561
18136
  try {
@@ -17568,13 +18143,28 @@ class ExecService {
17568
18143
  });
17569
18144
  } catch (e) {
17570
18145
  const reason = e instanceof Error ? e.message : String(e);
18146
+ if (opts.quiet)
18147
+ agent.note(start);
17571
18148
  agent.note({ type: "tool-end", toolId, output: `could not run: ${reason}`, isError: true });
17572
18149
  return Promise.resolve({ exit: reason, text: "" });
17573
18150
  }
17574
18151
  const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
17575
18152
  this.track(worktreeId, toolId, { proc, timer });
17576
18153
  this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
17577
- return this.collect(worktreeId, toolId, proc, agent);
18154
+ return this.collect(worktreeId, toolId, proc, agent, opts.quiet ? start : undefined);
18155
+ }
18156
+ record(worktreeId, command, text, exit) {
18157
+ const wt = this.deps.state.requireWorktree(worktreeId);
18158
+ if (wt.kind === "spare")
18159
+ throw new UserError("no shell for a spare worktree");
18160
+ const agent = this.deps.runtime.agentFor(worktreeId);
18161
+ if (!agent)
18162
+ throw new UserError("worktree still starting; try again in a moment");
18163
+ const toolId = `${SHELL_TOOL}-${Date.now().toString(36)}-${++this.n}`;
18164
+ agent.note({ type: "tool-start", toolId, name: SHELL_TOOL, input: { command }, kind: "execute" });
18165
+ const truncated = text.length > OUTPUT_CAP;
18166
+ const shown = truncated ? text.slice(0, OUTPUT_CAP) : text;
18167
+ agent.note({ type: "tool-end", toolId, output: formatOutput(shown, exit, truncated), isError: exit !== 0 });
17578
18168
  }
17579
18169
  stop(worktreeId) {
17580
18170
  for (const toolId of this.running.get(worktreeId)?.keys() ?? [])
@@ -17606,7 +18196,7 @@ class ExecService {
17606
18196
  r.proc.kill("SIGKILL");
17607
18197
  }, KILL_GRACE_MS2);
17608
18198
  }
17609
- async collect(worktreeId, toolId, proc, agent) {
18199
+ async collect(worktreeId, toolId, proc, agent, heldStart) {
17610
18200
  let text = "";
17611
18201
  let truncated = false;
17612
18202
  const drain = async (stream) => {
@@ -17641,6 +18231,10 @@ class ExecService {
17641
18231
  clearTimeout(r.timer);
17642
18232
  this.deps.runtime.release(worktreeId, `exec:${toolId}`);
17643
18233
  }
18234
+ if (heldStart && exit === 0)
18235
+ return { exit, text };
18236
+ if (heldStart)
18237
+ agent.note(heldStart);
17644
18238
  agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
17645
18239
  return { exit, text };
17646
18240
  }
@@ -17687,7 +18281,7 @@ function withRepoLock(repoPath, fn) {
17687
18281
  var fileLockKey = (absPath) => `file:${absPath}`;
17688
18282
 
17689
18283
  // ../daemon/src/git/status.ts
17690
- import { join as join9 } from "path";
18284
+ import { join as join11 } from "path";
17691
18285
  async function treeFingerprint(worktreePath) {
17692
18286
  const [head, diff, untracked] = await Promise.all([
17693
18287
  git(worktreePath, "rev-parse", "HEAD"),
@@ -17697,7 +18291,7 @@ async function treeFingerprint(worktreePath) {
17697
18291
  const others = await Promise.all(untracked.out.split(`
17698
18292
  `).filter(Boolean).map(async (p) => {
17699
18293
  try {
17700
- const f = Bun.file(join9(worktreePath, p));
18294
+ const f = Bun.file(join11(worktreePath, p));
17701
18295
  const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
17702
18296
  return `${p}:${body}`;
17703
18297
  } catch {
@@ -17748,7 +18342,7 @@ async function numstat(worktreePath, ...range) {
17748
18342
  }
17749
18343
  async function untrackedLines(worktreePath, file) {
17750
18344
  try {
17751
- const f = Bun.file(join9(worktreePath, file));
18345
+ const f = Bun.file(join11(worktreePath, file));
17752
18346
  if (f.size > 2000000)
17753
18347
  return {};
17754
18348
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -17767,14 +18361,16 @@ async function untrackedLines(worktreePath, file) {
17767
18361
  return {};
17768
18362
  }
17769
18363
  }
18364
+ var MAX_UNTRACKED_COUNTED = 500;
17770
18365
  async function statusFilesWithCounts(worktreePath) {
17771
18366
  const files = await statusFiles(worktreePath);
17772
18367
  if (files.length === 0)
17773
18368
  return files;
17774
18369
  const counts = files.some((f) => f.xy !== "??") ? await numstat(worktreePath, "HEAD") : new Map;
18370
+ const countUntracked = files.filter((f) => f.xy === "??").length <= MAX_UNTRACKED_COUNTED;
17775
18371
  return Promise.all(files.map(async (f) => ({
17776
18372
  ...f,
17777
- ...f.xy === "??" ? await untrackedLines(worktreePath, f.path) : counts.get(f.path) ?? {}
18373
+ ...f.xy === "??" ? countUntracked ? await untrackedLines(worktreePath, f.path) : {} : counts.get(f.path) ?? {}
17778
18374
  })));
17779
18375
  }
17780
18376
  async function committedFiles(worktreePath, defaultBr) {
@@ -17824,6 +18420,10 @@ async function behindUpstream(worktreePath) {
17824
18420
  const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
17825
18421
  return r.ok ? Number(r.out) || 0 : null;
17826
18422
  }
18423
+ async function aheadUpstream(worktreePath) {
18424
+ const r = await git(worktreePath, "rev-list", "--count", "@{upstream}..HEAD");
18425
+ return r.ok ? Number(r.out) || 0 : null;
18426
+ }
17827
18427
  async function aheadBehind(worktreePath, defaultBr) {
17828
18428
  const [a, b] = await Promise.all([
17829
18429
  git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`),
@@ -18042,7 +18642,7 @@ function rankElementSources(el, rows) {
18042
18642
  // ../daemon/src/files/listing.ts
18043
18643
  var SUBMODULE_MODE = "160000";
18044
18644
  var entries = (out) => out.split("\x00").filter(Boolean);
18045
- function parseListing(staged, untracked, deleted) {
18645
+ function parseListing(staged, untracked, deleted, ignored = "") {
18046
18646
  const gone = new Set(entries(deleted));
18047
18647
  const paths = new Set;
18048
18648
  const submodules = new Set;
@@ -18060,7 +18660,18 @@ function parseListing(staged, untracked, deleted) {
18060
18660
  }
18061
18661
  for (const path of entries(untracked))
18062
18662
  paths.add(path);
18063
- return { paths: [...paths], submodules: [...submodules] };
18663
+ return { paths: [...paths], submodules: [...submodules], ignored: collapse(entries(ignored)) };
18664
+ }
18665
+ function collapse(list) {
18666
+ const out = [];
18667
+ let under = null;
18668
+ for (const p of [...list].sort()) {
18669
+ if (under !== null && p.startsWith(under))
18670
+ continue;
18671
+ under = p.endsWith("/") ? p : null;
18672
+ out.push(p);
18673
+ }
18674
+ return out;
18064
18675
  }
18065
18676
 
18066
18677
  // ../daemon/src/files/vite-offset.ts
@@ -18236,12 +18847,13 @@ class FileService {
18236
18847
  }
18237
18848
  async list(worktreeId) {
18238
18849
  const cwd = this.require(worktreeId).path;
18239
- const [staged, untracked, deleted] = await Promise.all([
18850
+ const [staged, untracked, deleted, ignored] = await Promise.all([
18240
18851
  git(cwd, "ls-files", "-z", "-s"),
18241
18852
  git(cwd, "ls-files", "-z", "-o", "--exclude-standard"),
18242
- git(cwd, "ls-files", "-z", "-d")
18853
+ git(cwd, "ls-files", "-z", "-d"),
18854
+ git(cwd, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory")
18243
18855
  ]);
18244
- return parseListing(staged.out, untracked.out, deleted.out);
18856
+ return parseListing(staged.out, untracked.out, deleted.out, ignored.out);
18245
18857
  }
18246
18858
  async search(worktreeId, query) {
18247
18859
  const { path: cwd } = this.require(worktreeId);
@@ -18464,6 +19076,7 @@ class AfterLand {
18464
19076
  fireAndForget(repo.id, done, "afterLand");
18465
19077
  }
18466
19078
  async exec(repo, commands) {
19079
+ await this.d.settled?.();
18467
19080
  if (this.d.self.building(true))
18468
19081
  this.d.hub.emit("selfChanged");
18469
19082
  let failure;
@@ -18504,41 +19117,18 @@ class AfterLand {
18504
19117
  }
18505
19118
 
18506
19119
  // ../daemon/src/repos/registry.ts
18507
- import { existsSync as existsSync17, mkdirSync as mkdirSync5, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
19120
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
18508
19121
  import { homedir as homedir7 } from "os";
18509
- import { basename as basename3, dirname as dirname6, join as join14 } from "path";
18510
-
18511
- // ../daemon/src/worktrees/naming.ts
18512
- import { randomBytes as randomBytes2 } from "crypto";
18513
- function shortId() {
18514
- return randomBytes2(5).toString("hex");
18515
- }
18516
- function slugify2(prompt, withRandom = true) {
18517
- const words = prompt.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter(Boolean).slice(0, 3);
18518
- const base = words.join("-").slice(0, 20).replace(/-+$/g, "") || "task";
18519
- return withRandom ? `${base}-${randomBytes2(2).toString("hex")}` : base;
18520
- }
18521
- function cleanTitle2(title) {
18522
- return title.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
18523
- }
18524
- var VARIANT_LENSES = [
18525
- "Take the most direct approach: the change most people would expect, in the smallest diff.",
18526
- "Lean toward how it looks and reads: polish what the task touches using only the design tokens, components and voice the project already has. Add no new colours, fonts or effects, and restyle nothing outside the task.",
18527
- "Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
18528
- ];
18529
- function variantLens(index) {
18530
- const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
18531
- return `(You are attempt ${index} of several parallel attempts at this task. Stay within the task and follow the project's existing styles, components, tokens and writing conventions; the attempts differ in emphasis, not in how much they change. ${lens})`;
18532
- }
19122
+ import { basename as basename3, dirname as dirname6, join as join16 } from "path";
18533
19123
 
18534
19124
  // ../daemon/src/repos/browse.ts
18535
- import { existsSync as existsSync13, statSync as statSync3 } from "fs";
19125
+ import { existsSync as existsSync15, statSync as statSync3 } from "fs";
18536
19126
  import { readdir } from "fs/promises";
18537
19127
  import { homedir as homedir5 } from "os";
18538
- import { dirname as dirname4, join as join10 } from "path";
19128
+ import { dirname as dirname4, join as join12 } from "path";
18539
19129
  var MAX_ENTRIES = 40;
18540
19130
  function expandTilde(raw) {
18541
- return raw === "~" || raw.startsWith("~/") ? join10(homedir5(), raw.slice(1)) : raw;
19131
+ return raw === "~" || raw.startsWith("~/") ? join12(homedir5(), raw.slice(1)) : raw;
18542
19132
  }
18543
19133
  function collapseTilde(abs) {
18544
19134
  const home = homedir5();
@@ -18549,7 +19139,7 @@ function holdsNothing(names) {
18549
19139
  }
18550
19140
  async function describeFolder(abs) {
18551
19141
  const path = collapseTilde(abs);
18552
- if (existsSync13(join10(abs, ".git")))
19142
+ if (existsSync15(join12(abs, ".git")))
18553
19143
  return { path, kind: "project" };
18554
19144
  try {
18555
19145
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -18564,10 +19154,10 @@ function describe2(typed, parent) {
18564
19154
  isDir = statSync3(typed).isDirectory();
18565
19155
  } catch {}
18566
19156
  return {
18567
- exists: existsSync13(typed),
19157
+ exists: existsSync15(typed),
18568
19158
  isDir,
18569
- isRepo: isDir && existsSync13(join10(typed, ".git")),
18570
- parentExists: existsSync13(parent)
19159
+ isRepo: isDir && existsSync15(join12(typed, ".git")),
19160
+ parentExists: existsSync15(parent)
18571
19161
  };
18572
19162
  }
18573
19163
  async function browsePath(raw) {
@@ -18591,16 +19181,16 @@ async function browsePath(raw) {
18591
19181
  continue;
18592
19182
  if (!name.toLowerCase().startsWith(prefix))
18593
19183
  continue;
18594
- const abs = join10(dir, name);
18595
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync13(join10(abs, ".git")) });
19184
+ const abs = join12(dir, name);
19185
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync15(join12(abs, ".git")) });
18596
19186
  }
18597
19187
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
18598
19188
  return { entries: entries.slice(0, MAX_ENTRIES), target };
18599
19189
  }
18600
19190
 
18601
19191
  // ../daemon/src/repos/config.ts
18602
- import { closeSync, existsSync as existsSync14, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync10, readSync } from "fs";
18603
- import { join as join11 } from "path";
19192
+ import { closeSync, existsSync as existsSync16, openSync as openSync2, readdirSync as readdirSync4, readFileSync as readFileSync11, readSync } from "fs";
19193
+ import { join as join13 } from "path";
18604
19194
 
18605
19195
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
18606
19196
  function createScanner(text, ignoreTrivia = false) {
@@ -19964,7 +20554,7 @@ function applyEdits(text, edits) {
19964
20554
 
19965
20555
  // ../daemon/src/repos/config.ts
19966
20556
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
19967
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync14(join11(repoPath, rel)));
20557
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync16(join13(repoPath, rel)));
19968
20558
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
19969
20559
  function parseSettings(text) {
19970
20560
  const errors = [];
@@ -19989,7 +20579,7 @@ function readConfigFile(repoPath) {
19989
20579
  for (const rel of files) {
19990
20580
  let raw;
19991
20581
  try {
19992
- raw = parseSettings(readFileSync10(join11(repoPath, rel), "utf8"));
20582
+ raw = parseSettings(readFileSync11(join13(repoPath, rel), "utf8"));
19993
20583
  } catch (e) {
19994
20584
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
19995
20585
  }
@@ -20046,12 +20636,12 @@ function configTarget(repoPath, made) {
20046
20636
  }
20047
20637
  function configBody(repoPath, rel, config) {
20048
20638
  const place = PLACES.find((p) => p.local === rel);
20049
- const abs = place && join11(repoPath, place.shared);
20050
- if (!abs || !existsSync14(abs))
20639
+ const abs = place && join13(repoPath, place.shared);
20640
+ if (!abs || !existsSync16(abs))
20051
20641
  return config;
20052
20642
  let shared;
20053
20643
  try {
20054
- shared = parseSettings(readFileSync10(abs, "utf8"));
20644
+ shared = parseSettings(readFileSync11(abs, "utf8"));
20055
20645
  } catch {
20056
20646
  return config;
20057
20647
  }
@@ -20064,7 +20654,7 @@ function configText(repoPath, rel, config) {
20064
20654
  let text;
20065
20655
  let was;
20066
20656
  try {
20067
- text = readFileSync10(join11(repoPath, rel), "utf8");
20657
+ text = readFileSync11(join13(repoPath, rel), "utf8");
20068
20658
  was = parseSettings(text);
20069
20659
  } catch {
20070
20660
  return fresh;
@@ -20094,9 +20684,9 @@ function detectConfig(repoPath) {
20094
20684
  return { config: explicit.config, needsSetup: false };
20095
20685
  if (explicit)
20096
20686
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
20097
- const pkgPath = join11(repoPath, "package.json");
20098
- if (existsSync14(pkgPath)) {
20099
- const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
20687
+ const pkgPath = join13(repoPath, "package.json");
20688
+ if (existsSync16(pkgPath)) {
20689
+ const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
20100
20690
  const scripts = pkg.scripts ?? {};
20101
20691
  const runner = detectRunner(repoPath, pkg.packageManager);
20102
20692
  const pages = detectPages(repoPath, scripts, runner);
@@ -20117,7 +20707,7 @@ function detectConfig(repoPath) {
20117
20707
  };
20118
20708
  }
20119
20709
  }
20120
- if (existsSync14(join11(repoPath, "start.sh"))) {
20710
+ if (existsSync16(join13(repoPath, "start.sh"))) {
20121
20711
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
20122
20712
  }
20123
20713
  const assumed = assumeNothingToRun(repoPath);
@@ -20160,24 +20750,24 @@ function assumeNothingToRun(repoPath) {
20160
20750
  return marker && !servesPage(repoPath) ? marker : undefined;
20161
20751
  }
20162
20752
  function markerIn(repoPath) {
20163
- const named = NO_PAGE_MARKERS.find((f) => existsSync14(join11(repoPath, f)));
20753
+ const named = NO_PAGE_MARKERS.find((f) => existsSync16(join13(repoPath, f)));
20164
20754
  if (named)
20165
20755
  return named;
20166
20756
  let entries;
20167
20757
  try {
20168
- entries = readdirSync3(repoPath);
20758
+ entries = readdirSync4(repoPath);
20169
20759
  } catch {
20170
20760
  return;
20171
20761
  }
20172
20762
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
20173
20763
  }
20174
20764
  function servesPage(repoPath) {
20175
- if (WEB_FILES.some((f) => existsSync14(join11(repoPath, f))))
20765
+ if (WEB_FILES.some((f) => existsSync16(join13(repoPath, f))))
20176
20766
  return true;
20177
- if (/^web:/m.test(head(join11(repoPath, "Procfile"))))
20767
+ if (/^web:/m.test(head(join13(repoPath, "Procfile"))))
20178
20768
  return true;
20179
20769
  for (const [files, names] of WEB_DEPENDENCIES) {
20180
- const text = files.map((f) => head(join11(repoPath, f))).join(`
20770
+ const text = files.map((f) => head(join13(repoPath, f))).join(`
20181
20771
  `);
20182
20772
  if (text.trim() && names.some((n) => mentions(text, n)))
20183
20773
  return true;
@@ -20187,14 +20777,14 @@ function servesPage(repoPath) {
20187
20777
  function frontEndBeside(repoPath) {
20188
20778
  let dirs;
20189
20779
  try {
20190
- dirs = readdirSync3(repoPath, { withFileTypes: true });
20780
+ dirs = readdirSync4(repoPath, { withFileTypes: true });
20191
20781
  } catch {
20192
20782
  return false;
20193
20783
  }
20194
20784
  return dirs.some((d) => {
20195
20785
  if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
20196
20786
  return false;
20197
- const pkg = head(join11(repoPath, d.name, "package.json"));
20787
+ const pkg = head(join13(repoPath, d.name, "package.json"));
20198
20788
  if (!pkg)
20199
20789
  return false;
20200
20790
  try {
@@ -20209,7 +20799,7 @@ function mentions(text, name) {
20209
20799
  return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
20210
20800
  }
20211
20801
  function head(path) {
20212
- if (!existsSync14(path))
20802
+ if (!existsSync16(path))
20213
20803
  return "";
20214
20804
  try {
20215
20805
  const fd = openSync2(path, "r");
@@ -20237,31 +20827,31 @@ function procCommand(runner, script, body) {
20237
20827
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
20238
20828
  }
20239
20829
  function detectPages(repoPath, scripts, runner) {
20240
- if (!scripts.build || !existsSync14(join11(repoPath, "functions")))
20830
+ if (!scripts.build || !existsSync16(join13(repoPath, "functions")))
20241
20831
  return null;
20242
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join11(repoPath, f)).find((f) => existsSync14(f));
20832
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join13(repoPath, f)).find((f) => existsSync16(f));
20243
20833
  if (!cfg)
20244
20834
  return null;
20245
- const out = readFileSync10(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
20835
+ const out = readFileSync11(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
20246
20836
  if (!out)
20247
20837
  return null;
20248
20838
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
20249
20839
  }
20250
20840
  function detectRunner(repoPath, packageManager) {
20251
- if (existsSync14(join11(repoPath, "bun.lock")) || existsSync14(join11(repoPath, "bun.lockb")))
20841
+ if (existsSync16(join13(repoPath, "bun.lock")) || existsSync16(join13(repoPath, "bun.lockb")))
20252
20842
  return "bun";
20253
- if (existsSync14(join11(repoPath, "pnpm-lock.yaml")))
20843
+ if (existsSync16(join13(repoPath, "pnpm-lock.yaml")))
20254
20844
  return "pnpm";
20255
- if (existsSync14(join11(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
20845
+ if (existsSync16(join13(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
20256
20846
  return "yarn";
20257
20847
  return "npm";
20258
20848
  }
20259
20849
 
20260
20850
  // ../daemon/src/repos/create.ts
20261
- import { existsSync as existsSync15, readdirSync as readdirSync4, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
20851
+ import { existsSync as existsSync17, readdirSync as readdirSync5, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
20262
20852
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
20263
20853
  import { homedir as homedir6 } from "os";
20264
- import { dirname as dirname5, join as join12, resolve as resolve7 } from "path";
20854
+ import { dirname as dirname5, join as join14, resolve as resolve7 } from "path";
20265
20855
  function isInside(dir, parent) {
20266
20856
  const p = parent.replace(/\/+$/, "");
20267
20857
  return dir === p || dir.startsWith(`${p}/`);
@@ -20272,29 +20862,29 @@ function planProject(opts, home = homedir6()) {
20272
20862
  throw new UserError(nameError);
20273
20863
  const first = firstProjectsFolder(opts.parent, home);
20274
20864
  const parent = first ?? resolveParent(opts.parent);
20275
- const dir = join12(parent, opts.name.trim());
20276
- if (existsSync15(dir))
20865
+ const dir = join14(parent, opts.name.trim());
20866
+ if (existsSync17(dir))
20277
20867
  throw new UserError(`${dir} already exists`);
20278
20868
  return { parent, dir, makeParent: first !== null };
20279
20869
  }
20280
20870
  function firstProjectsFolder(raw, home) {
20281
20871
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
20282
- if (typed !== join12(home, PROJECTS_FOLDER) || existsSync15(typed) || !existsSync15(home))
20872
+ if (typed !== join14(home, PROJECTS_FOLDER) || existsSync17(typed) || !existsSync17(home))
20283
20873
  return null;
20284
- return join12(realpathSync2(home), PROJECTS_FOLDER);
20874
+ return join14(realpathSync2(home), PROJECTS_FOLDER);
20285
20875
  }
20286
20876
  function planInPlace(opts) {
20287
20877
  const name = opts.name.trim();
20288
20878
  if (!name || name === "." || name === ".." || name.includes("/")) {
20289
20879
  throw new UserError(`${opts.name} is not a folder name`);
20290
20880
  }
20291
- const typed = join12(resolveParent(opts.parent), name);
20292
- if (!existsSync15(typed) || !statSync4(typed).isDirectory())
20881
+ const typed = join14(resolveParent(opts.parent), name);
20882
+ if (!existsSync17(typed) || !statSync4(typed).isDirectory())
20293
20883
  throw new UserError(`${typed} is not a folder`);
20294
20884
  const dir = realpathSync2(typed);
20295
- if (existsSync15(join12(dir, ".git")))
20885
+ if (existsSync17(join14(dir, ".git")))
20296
20886
  throw new UserError(`${dir} is already a project; open it instead`);
20297
- if (!holdsNothing(readdirSync4(dir))) {
20887
+ if (!holdsNothing(readdirSync5(dir))) {
20298
20888
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
20299
20889
  }
20300
20890
  return { parent: dirname5(dir), dir, makeParent: false };
@@ -20303,7 +20893,7 @@ function resolveParent(raw) {
20303
20893
  const typed = expandTilde(raw.trim());
20304
20894
  if (!typed.startsWith("/"))
20305
20895
  throw new UserError(`${raw} is not a folder path`);
20306
- if (!existsSync15(typed))
20896
+ if (!existsSync17(typed))
20307
20897
  throw new UserError(`${raw} does not exist; make it first`);
20308
20898
  if (!statSync4(typed).isDirectory())
20309
20899
  throw new UserError(`${raw} is not a folder`);
@@ -20383,13 +20973,13 @@ async function undoCreate(dir) {
20383
20973
  }
20384
20974
  async function undoInit(dir) {
20385
20975
  try {
20386
- await rm(join12(dir, ".git"), { recursive: true, force: true });
20976
+ await rm(join14(dir, ".git"), { recursive: true, force: true });
20387
20977
  } catch (e) {
20388
20978
  log.warn(dir, "could not take back a half-made .git", e);
20389
20979
  }
20390
20980
  }
20391
20981
  async function unmakeProject(dir, made) {
20392
- const target = made === "folder" ? dir : join12(dir, ".git");
20982
+ const target = made === "folder" ? dir : join14(dir, ".git");
20393
20983
  try {
20394
20984
  await rm(target, { recursive: true, force: true });
20395
20985
  } catch (e) {
@@ -20419,8 +21009,8 @@ async function ok(p, what) {
20419
21009
  }
20420
21010
 
20421
21011
  // ../daemon/src/repos/watcher.ts
20422
- import { existsSync as existsSync16, watch } from "fs";
20423
- import { basename as basename2, join as join13 } from "path";
21012
+ import { existsSync as existsSync18, watch } from "fs";
21013
+ import { basename as basename2, join as join15 } from "path";
20424
21014
  function watchDefaultBranch(repoPath, branch, onMove) {
20425
21015
  let last = null;
20426
21016
  fireAndForget(repoPath, git(repoPath, "rev-parse", branch).then((r) => {
@@ -20450,8 +21040,8 @@ function watchDefaultBranch(repoPath, branch, onMove) {
20450
21040
  log.debug(repoPath, `not watching ${p}`, e);
20451
21041
  }
20452
21042
  };
20453
- tryWatch(join13(repoPath, ".git"));
20454
- tryWatch(join13(repoPath, ".git", "refs", "heads"));
21043
+ tryWatch(join15(repoPath, ".git"));
21044
+ tryWatch(join15(repoPath, ".git", "refs", "heads"));
20455
21045
  return () => {
20456
21046
  for (const w of watchers)
20457
21047
  w.close();
@@ -20473,7 +21063,7 @@ function watchWorktreeDir(repoPath, onChange) {
20473
21063
  }, 300);
20474
21064
  };
20475
21065
  const arm = (commonDir) => {
20476
- const dir = join13(commonDir, "worktrees");
21066
+ const dir = join15(commonDir, "worktrees");
20477
21067
  const armInner = () => {
20478
21068
  inner?.close();
20479
21069
  inner = null;
@@ -20516,7 +21106,7 @@ function watchWorktreeDir(repoPath, onChange) {
20516
21106
  function watchConfigFile(repoPath, onChange) {
20517
21107
  const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
20518
21108
  const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename2(rel)));
20519
- const dir = join13(repoPath, CONFIG_DIR);
21109
+ const dir = join15(repoPath, CONFIG_DIR);
20520
21110
  let timer = null;
20521
21111
  let outer = null;
20522
21112
  let inner = null;
@@ -20531,7 +21121,7 @@ function watchConfigFile(repoPath, onChange) {
20531
21121
  const armInner = () => {
20532
21122
  inner?.close();
20533
21123
  inner = null;
20534
- if (!existsSync16(dir))
21124
+ if (!existsSync18(dir))
20535
21125
  return;
20536
21126
  try {
20537
21127
  inner = watch(dir, (_event, filename) => {
@@ -20585,7 +21175,7 @@ class RepoRegistry {
20585
21175
  }
20586
21176
  async boot() {
20587
21177
  const { state } = this.d;
20588
- const gone = state.worktrees.filter((wt) => !existsSync17(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
21178
+ const gone = state.worktrees.filter((wt) => !existsSync19(wt.path) || !state.repos.some((r) => r.id === wt.repoId));
20589
21179
  for (const wt of gone)
20590
21180
  await this.d.worktrees.forgetGone(wt);
20591
21181
  for (const repo of state.repos) {
@@ -20652,7 +21242,7 @@ class RepoRegistry {
20652
21242
  return `${repo.name} is set up now`;
20653
21243
  if (main.promptedAt || main.lastTurn)
20654
21244
  return `${repo.name} has a chat now`;
20655
- if (existsSync17(join14(repo.path, ".claude")))
21245
+ if (existsSync19(join16(repo.path, ".claude")))
20656
21246
  return `an agent has already run in ${repo.name}`;
20657
21247
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
20658
21248
  return `${repo.name} has files in it now`;
@@ -20729,7 +21319,7 @@ class RepoRegistry {
20729
21319
  }
20730
21320
  async register(rawPath, made) {
20731
21321
  const path = expandTilde(rawPath);
20732
- if (!existsSync17(path))
21322
+ if (!existsSync19(path))
20733
21323
  throw new UserError(`${path} does not exist`);
20734
21324
  if (!await isGitRepo(path))
20735
21325
  throw new UserError(`${path} is not a git repository`);
@@ -20767,7 +21357,6 @@ class RepoRegistry {
20767
21357
  };
20768
21358
  this.d.state.addWorktree(main);
20769
21359
  this.warmed.add(repo.id);
20770
- await this.d.runtime.start(main, repo);
20771
21360
  this.startWatcher(repo);
20772
21361
  fireAndForget(repo.id, this.d.worktrees.spare.ensure(repo.id), "spare warm-up");
20773
21362
  this.d.hub.emit("reposChanged");
@@ -20789,8 +21378,8 @@ class RepoRegistry {
20789
21378
  this.d.state.removeWorktree(wt.id);
20790
21379
  this.d.worktrees.deleteChat(wt.id);
20791
21380
  releasePort(wt.proxyPort);
21381
+ this.d.drafts?.drop(wt.id);
20792
21382
  }
20793
- this.d.drafts?.drop(draftKey(repoId));
20794
21383
  this.d.state.removeRepo(repoId);
20795
21384
  this.d.hub.emit("reposChanged");
20796
21385
  this.d.hub.emit("worktreesChanged");
@@ -20802,10 +21391,10 @@ class RepoRegistry {
20802
21391
  throw new UserError(current.reason);
20803
21392
  const was = configTarget(repo.path, !!repo.made);
20804
21393
  const rel = configSibling(was, kind);
20805
- const moved = rel !== was && existsSync17(join14(repo.path, was));
21394
+ const moved = rel !== was && existsSync19(join16(repo.path, was));
20806
21395
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
20807
21396
  try {
20808
- rmSync2(join14(repo.path, was));
21397
+ rmSync2(join16(repo.path, was));
20809
21398
  } catch (e) {
20810
21399
  log.warn(repoId, `could not remove ${was}`, e);
20811
21400
  }
@@ -20814,10 +21403,10 @@ class RepoRegistry {
20814
21403
  }
20815
21404
  if (isLocalConfigFile(rel))
20816
21405
  await excludeFromGit(repo.path, rel);
20817
- const file = join14(repo.path, rel);
21406
+ const file = join16(repo.path, rel);
20818
21407
  try {
20819
- mkdirSync5(dirname6(file), { recursive: true });
20820
- writeFileSync6(file, configText(repo.path, rel, config));
21408
+ mkdirSync6(dirname6(file), { recursive: true });
21409
+ writeFileSync7(file, configText(repo.path, rel, config));
20821
21410
  } catch (e) {
20822
21411
  log.warn(repoId, `could not write ${rel}`, e);
20823
21412
  }
@@ -20829,7 +21418,7 @@ class RepoRegistry {
20829
21418
  repo.assumed = undefined;
20830
21419
  this.d.state.save();
20831
21420
  this.warmed.add(repoId);
20832
- for (const wt of this.d.state.worktrees.filter((w) => w.repoId === repoId)) {
21421
+ for (const wt of this.d.state.worktrees.filter((w) => w.repoId === repoId && (w.kind !== "main" || this.d.runtime.get(w.id)?.procs))) {
20833
21422
  fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.start(wt, repo)), "runtime restart");
20834
21423
  }
20835
21424
  fireAndForget(repoId, this.d.worktrees.spare.ensure(repoId), "spare warm-up");
@@ -20891,7 +21480,7 @@ class RepoRegistry {
20891
21480
  }
20892
21481
  placeConfig(repo) {
20893
21482
  const rel = configTarget(repo.path, !!repo.made);
20894
- if (isLocalConfigFile(rel) && existsSync17(join14(repo.path, rel))) {
21483
+ if (isLocalConfigFile(rel) && existsSync19(join16(repo.path, rel))) {
20895
21484
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
20896
21485
  }
20897
21486
  if (repo.configFile === rel)
@@ -20966,7 +21555,7 @@ class RepoRegistry {
20966
21555
  }
20967
21556
 
20968
21557
  // ../daemon/src/routes/service.ts
20969
- import { join as join15 } from "path";
21558
+ import { join as join17 } from "path";
20970
21559
 
20971
21560
  // ../daemon/src/routes/fileRouters.ts
20972
21561
  var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
@@ -22166,7 +22755,7 @@ class RouteService {
22166
22755
  return mergeRoutes(fileRoutes(paths, roots), code);
22167
22756
  }
22168
22757
  async cachedRead(root, path, had, kept, decode) {
22169
- const abs = join15(root, path);
22758
+ const abs = join17(root, path);
22170
22759
  const stat = await this.fs.stat(abs);
22171
22760
  if (!stat)
22172
22761
  return null;
@@ -22191,7 +22780,7 @@ class RouteService {
22191
22780
  }
22192
22781
  }
22193
22782
  async hash(root, file) {
22194
- const abs = join15(root, file);
22783
+ const abs = join17(root, file);
22195
22784
  const stat = await this.fs.stat(abs);
22196
22785
  if (!stat)
22197
22786
  return null;
@@ -22235,7 +22824,7 @@ class RouteService {
22235
22824
  }
22236
22825
 
22237
22826
  // ../daemon/src/runtime/bridge-script.ts
22238
- import { existsSync as existsSync18, readFileSync as readFileSync11, statSync as statSync5 } from "fs";
22827
+ import { existsSync as existsSync20, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
22239
22828
  var LEARNED_MAX = 8;
22240
22829
 
22241
22830
  class BridgeScript {
@@ -22265,7 +22854,7 @@ class BridgeScript {
22265
22854
  this.cache = null;
22266
22855
  }
22267
22856
  get() {
22268
- if (!existsSync18(this.path))
22857
+ if (!existsSync20(this.path))
22269
22858
  return "// toyon bridge not built";
22270
22859
  const s = statSync5(this.path);
22271
22860
  const stamp = `${s.mtimeMs}:${s.size}`;
@@ -22274,7 +22863,7 @@ class BridgeScript {
22274
22863
  this.stamp = stamp;
22275
22864
  const origins = [...this.origins, ...this.learned];
22276
22865
  this.cache = `window.__toyonShellOrigins=${JSON.stringify(origins)};
22277
- ${readFileSync11(this.path, "utf8")}`;
22866
+ ${readFileSync12(this.path, "utf8")}`;
22278
22867
  return this.cache;
22279
22868
  }
22280
22869
  }
@@ -22291,70 +22880,6 @@ function servedLocally(origin) {
22291
22880
  return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
22292
22881
  }
22293
22882
 
22294
- // ../daemon/src/runtime/memory.ts
22295
- var MAC_PRESSURE_CRITICAL = 4;
22296
- var AVAILABLE_FLOOR = 0.15;
22297
- var unsupportedLogged = false;
22298
- async function memoryTight() {
22299
- const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
22300
- if (signal === null && !unsupportedLogged) {
22301
- unsupportedLogged = true;
22302
- log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
22303
- }
22304
- return signal;
22305
- }
22306
- async function darwin() {
22307
- const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
22308
- if (!r.ok)
22309
- return null;
22310
- const [level, available] = r.out.split(/\s+/).map(Number);
22311
- if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
22312
- return null;
22313
- return darwinSignal(level, available);
22314
- }
22315
- function darwinSignal(level, available) {
22316
- const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
22317
- return {
22318
- tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
22319
- why: `memory pressure ${pressure} with ${available}% available`
22320
- };
22321
- }
22322
- async function linux() {
22323
- let text;
22324
- try {
22325
- text = await Bun.file("/proc/meminfo").text();
22326
- } catch {
22327
- return null;
22328
- }
22329
- const field = (name) => {
22330
- const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
22331
- return m ? Number(m[1]) : null;
22332
- };
22333
- const total = field("MemTotal");
22334
- const available = field("MemAvailable");
22335
- if (!total || available === null)
22336
- return null;
22337
- const percent = Math.round(available / total * 100);
22338
- return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
22339
- }
22340
- async function sampleCosts(pgids) {
22341
- const costs = new Map;
22342
- if (pgids.length === 0)
22343
- return costs;
22344
- const wanted = new Set(pgids);
22345
- const r = await run("ps", ["-axo", "pgid=,rss="], "/");
22346
- if (!r.ok)
22347
- return costs;
22348
- for (const line of r.out.split(`
22349
- `)) {
22350
- const [pgid, rss] = line.trim().split(/\s+/).map(Number);
22351
- if (pgid === undefined || rss === undefined || !wanted.has(pgid))
22352
- continue;
22353
- costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
22354
- }
22355
- return costs;
22356
- }
22357
-
22358
22883
  // ../daemon/src/runtime/idle.ts
22359
22884
  var LOCAL_SLEEP_MS = 2 * 60 * 60000;
22360
22885
  var CLOUD_SLEEP_MS = 5 * 60000;
@@ -22377,6 +22902,7 @@ class IdlePolicy {
22377
22902
  viewing = new Map;
22378
22903
  queue = [];
22379
22904
  draining = false;
22905
+ drained = [];
22380
22906
  sleepMs;
22381
22907
  now;
22382
22908
  constructor(d) {
@@ -22470,6 +22996,9 @@ class IdlePolicy {
22470
22996
  const wt = this.d.state.worktree(id);
22471
22997
  return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
22472
22998
  }
22999
+ isShown(id) {
23000
+ return (this.entries.get(id)?.viewers.size ?? 0) > 0;
23001
+ }
22473
23002
  isViewed(id) {
22474
23003
  if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
22475
23004
  return true;
@@ -22492,7 +23021,8 @@ class IdlePolicy {
22492
23021
  const pick = this.d.state.worktrees.filter((w) => this.candidate(w.id)).sort((a, b) => this.entry(a.id).activeAt - this.entry(b.id).activeAt)[0];
22493
23022
  if (!pick)
22494
23023
  return;
22495
- this.sleep(pick.id, `${signal.why}; least recently used`);
23024
+ log.info("idle", `${signal.why}; ${pick.id} is the least recently used`);
23025
+ this.sleep(pick.id, "short of memory");
22496
23026
  }
22497
23027
  async sampleCosts() {
22498
23028
  this.recordViewed();
@@ -22590,13 +23120,12 @@ class IdlePolicy {
22590
23120
  this.arm(id, this.sleepMs - idle);
22591
23121
  return;
22592
23122
  }
22593
- this.sleep(id, `idle ${humanMs(this.sleepMs)}`);
23123
+ this.sleep(id, `nobody looked for ${humanMs(this.sleepMs)}`);
22594
23124
  }
22595
23125
  sleep(id, why) {
22596
23126
  this.disarm(id);
22597
23127
  this.entry(id).costKb = undefined;
22598
- log.info(id, `asleep: ${why}`);
22599
- fireAndForget(id, this.d.runtime.sleep(id), "sleep");
23128
+ fireAndForget(id, this.d.runtime.sleep(id, why), "sleep");
22600
23129
  }
22601
23130
  enqueue(key, run) {
22602
23131
  if (this.queue.some((q) => q.key === key))
@@ -22612,15 +23141,22 @@ class IdlePolicy {
22612
23141
  await next.run();
22613
23142
  } finally {
22614
23143
  this.draining = false;
23144
+ for (const r of this.drained.splice(0))
23145
+ r();
22615
23146
  }
22616
23147
  }
23148
+ settled() {
23149
+ if (!this.draining)
23150
+ return Promise.resolve();
23151
+ return new Promise((r) => this.drained.push(r));
23152
+ }
22617
23153
  }
22618
23154
  function humanMs(ms) {
22619
23155
  if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
22620
- return `${ms / (60 * 60000)}h`;
23156
+ return `${ms / (60 * 60000)} h`;
22621
23157
  if (ms >= 60000)
22622
23158
  return `${Math.round(ms / 60000)} min`;
22623
- return `${Math.round(ms / 1000)}s`;
23159
+ return `${Math.round(ms / 1000)} s`;
22624
23160
  }
22625
23161
 
22626
23162
  // ../daemon/src/server/ws.ts
@@ -22647,50 +23183,50 @@ class PairCodes {
22647
23183
  }
22648
23184
 
22649
23185
  // ../daemon/src/agent/config.ts
22650
- import { existsSync as existsSync19, readFileSync as readFileSync12 } from "fs";
23186
+ import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
22651
23187
  import { homedir as homedir8 } from "os";
22652
- import { join as join16 } from "path";
23188
+ import { join as join18 } from "path";
22653
23189
  function agentConfigFiles(spec, repoPath, home = homedir8()) {
22654
23190
  const file = (id, label, path) => ({
22655
23191
  id,
22656
23192
  label,
22657
23193
  path,
22658
- exists: existsSync19(path)
23194
+ exists: existsSync21(path)
22659
23195
  });
22660
23196
  if (spec.id === "claude") {
22661
23197
  return [
22662
- file("user-config", "user config (MCP servers, projects)", join16(home, ".claude.json")),
22663
- file("user-settings", "user settings (permissions, hooks)", join16(home, ".claude", "settings.json")),
22664
- ...repoPath ? [file("project-mcp", "project MCP servers", join16(repoPath, ".mcp.json"))] : [],
22665
- ...repoPath ? [file("project-settings", "project settings", join16(repoPath, ".claude", "settings.json"))] : []
23198
+ file("user-config", "user config (MCP servers, projects)", join18(home, ".claude.json")),
23199
+ file("user-settings", "user settings (permissions, hooks)", join18(home, ".claude", "settings.json")),
23200
+ ...repoPath ? [file("project-mcp", "project MCP servers", join18(repoPath, ".mcp.json"))] : [],
23201
+ ...repoPath ? [file("project-settings", "project settings", join18(repoPath, ".claude", "settings.json"))] : []
22666
23202
  ];
22667
23203
  }
22668
23204
  if (spec.id === "codex")
22669
- return [file("user-config", "config", join16(home, ".codex", "config.toml"))];
23205
+ return [file("user-config", "config", join18(home, ".codex", "config.toml"))];
22670
23206
  return [];
22671
23207
  }
22672
23208
  function describeAgentConfig(spec, repoPath, home = homedir8()) {
22673
23209
  const files = agentConfigFiles(spec, repoPath, home);
22674
23210
  const servers = [];
22675
23211
  if (spec.id === "claude") {
22676
- const user = readJson(join16(home, ".claude.json"));
23212
+ const user = readJson(join18(home, ".claude.json"));
22677
23213
  servers.push(...serversOf(user?.mcpServers, "user"));
22678
23214
  if (repoPath) {
22679
23215
  const projects = user?.projects;
22680
23216
  const mine = projects?.[repoPath];
22681
23217
  servers.push(...serversOf(mine?.mcpServers, "local"));
22682
- servers.push(...serversOf(readJson(join16(repoPath, ".mcp.json"))?.mcpServers, "project"));
23218
+ servers.push(...serversOf(readJson(join18(repoPath, ".mcp.json"))?.mcpServers, "project"));
22683
23219
  }
22684
23220
  } else if (spec.id === "codex") {
22685
- servers.push(...codexServers(join16(home, ".codex", "config.toml")));
23221
+ servers.push(...codexServers(join18(home, ".codex", "config.toml")));
22686
23222
  }
22687
23223
  return { agent: spec.id, files, servers };
22688
23224
  }
22689
23225
  function readJson(path) {
22690
- if (!existsSync19(path))
23226
+ if (!existsSync21(path))
22691
23227
  return null;
22692
23228
  try {
22693
- const v = JSON.parse(readFileSync12(path, "utf8"));
23229
+ const v = JSON.parse(readFileSync13(path, "utf8"));
22694
23230
  return v && typeof v === "object" && !Array.isArray(v) ? v : null;
22695
23231
  } catch (e) {
22696
23232
  log.warn("agent-config", `${path} is not valid JSON`, e);
@@ -22708,11 +23244,11 @@ function serversOf(raw, scope) {
22708
23244
  });
22709
23245
  }
22710
23246
  function codexServers(path) {
22711
- if (!existsSync19(path))
23247
+ if (!existsSync21(path))
22712
23248
  return [];
22713
23249
  const out = [];
22714
23250
  let current = null;
22715
- for (const raw of readFileSync12(path, "utf8").split(`
23251
+ for (const raw of readFileSync13(path, "utf8").split(`
22716
23252
  `)) {
22717
23253
  const line = raw.trim();
22718
23254
  const header = /^\[mcp_servers\.("?)([^\]"]+)\1\]$/.exec(line);
@@ -23304,8 +23840,11 @@ var handlers = {
23304
23840
  unsubscribe(msg, ctx) {
23305
23841
  ctx.unsubscribe(msg.worktreeId);
23306
23842
  },
23307
- view(msg, ctx) {
23843
+ view(msg, ctx, s) {
23308
23844
  ctx.view(msg.worktreeId);
23845
+ const wt = msg.worktreeId ? s.state.worktree(msg.worktreeId) : undefined;
23846
+ if (wt && isLead(wt))
23847
+ fireAndForget(wt.repoId, s.worktrees.syncTrunk(wt.repoId), "trunk sync");
23309
23848
  },
23310
23849
  seen(msg, _ctx, s) {
23311
23850
  s.turns.markSeen(msg.worktreeId);
@@ -23315,6 +23854,7 @@ var handlers = {
23315
23854
  },
23316
23855
  "set-draft"(msg, _ctx, s) {
23317
23856
  s.drafts.set(msg.boxId, msg.text, msg.clientId);
23857
+ s.worktrees.spare.typed(msg.boxId, msg.text);
23318
23858
  },
23319
23859
  "refresh-git"(msg, _ctx, s) {
23320
23860
  s.worktrees.recount(msg.repoId);
@@ -23339,6 +23879,7 @@ var handlers = {
23339
23879
  async "create-worktree"(msg, _ctx, s) {
23340
23880
  await s.worktrees.create(msg.repoId, msg.prompt, {
23341
23881
  createdBy: msg.clientId,
23882
+ worktreeId: msg.worktreeId,
23342
23883
  variant: msg.variant,
23343
23884
  context: msg.context,
23344
23885
  attachments: msg.attachments,
@@ -23462,6 +24003,9 @@ var handlers = {
23462
24003
  "update-now"(_msg, _ctx, s) {
23463
24004
  return s.update.updateNow();
23464
24005
  },
24006
+ "check-update"(_msg, _ctx, s) {
24007
+ return s.update.checkNow();
24008
+ },
23465
24009
  async commit(msg, ctx, s) {
23466
24010
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
23467
24011
  await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
@@ -23516,6 +24060,10 @@ var handlers = {
23516
24060
  async "discard-file"(msg, _ctx, s) {
23517
24061
  await s.files.discard(msg.worktreeId, msg.path);
23518
24062
  s.hub.emit("filesChanged", msg.worktreeId);
24063
+ s.landing.recheck(msg.worktreeId);
24064
+ },
24065
+ async judge(msg, _ctx, s) {
24066
+ await s.landing.judge(msg.worktreeId);
23519
24067
  },
23520
24068
  async "write-file"(msg, ctx, s) {
23521
24069
  const head = { t: "file-written", worktreeId: msg.worktreeId, path: msg.path, seq: msg.seq };
@@ -23698,8 +24246,8 @@ async function dispatch(msg, ctx, s) {
23698
24246
  }
23699
24247
 
23700
24248
  // ../daemon/src/server/http.ts
23701
- import { existsSync as existsSync20 } from "fs";
23702
- import { join as join17 } from "path";
24249
+ import { existsSync as existsSync22 } from "fs";
24250
+ import { join as join19 } from "path";
23703
24251
  var IMMUTABLE = "private, max-age=31536000, immutable";
23704
24252
  var NO_STORE = "no-store";
23705
24253
  function createFetch(opts) {
@@ -23769,10 +24317,12 @@ function createFetch(opts) {
23769
24317
  headers["set-cookie"] = grantCookie(grant, remote.host);
23770
24318
  return Response.json(await opts.bootstrap(), { headers });
23771
24319
  }
23772
- if (url.pathname === "/restart" && req.method === "POST") {
24320
+ if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
23773
24321
  if (!sameSecret(url.searchParams.get("token"), opts.token))
23774
24322
  return new Response("unauthorized", { status: 401 });
23775
- const refused = opts.restart();
24323
+ if (req.method === "GET")
24324
+ return Response.json(opts.restartWait(), { headers: { "cache-control": NO_STORE } });
24325
+ const refused = opts.restart(url.searchParams.has(RESTART_NOW));
23776
24326
  return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
23777
24327
  }
23778
24328
  if (url.pathname === "/pair" && req.method === "POST") {
@@ -23821,7 +24371,7 @@ function createFetch(opts) {
23821
24371
  return new Response("unauthorized", { status: 401 });
23822
24372
  const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
23823
24373
  const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
23824
- const path = places.find((p) => !!p && existsSync20(p));
24374
+ const path = places.find((p) => !!p && existsSync22(p));
23825
24375
  if (!path)
23826
24376
  return new Response("not found", { status: 404 });
23827
24377
  return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
@@ -23842,15 +24392,15 @@ function createFetch(opts) {
23842
24392
  });
23843
24393
  }
23844
24394
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
23845
- const file = join17(opts.shellDist, rel.replaceAll("..", ""));
24395
+ const file = join19(opts.shellDist, rel.replaceAll("..", ""));
23846
24396
  const hashed = rel.startsWith("/assets/");
23847
- if (existsSync20(file) && Bun.file(file).size > 0) {
24397
+ if (existsSync22(file) && Bun.file(file).size > 0) {
23848
24398
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
23849
24399
  }
23850
24400
  if (hashed)
23851
24401
  return new Response("not found", { status: 404 });
23852
- const index = join17(opts.shellDist, "index.html");
23853
- if (existsSync20(index))
24402
+ const index = join19(opts.shellDist, "index.html");
24403
+ if (existsSync22(index))
23854
24404
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
23855
24405
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
23856
24406
  };
@@ -23962,7 +24512,7 @@ function startServer(opts) {
23962
24512
  fireAndForget("ws", (async () => {
23963
24513
  do {
23964
24514
  statusesDirty = false;
23965
- broadcast({ t: "worktrees", rows: await s.worktrees.rows(), spares: s.worktrees.spares() });
24515
+ broadcast({ t: "worktrees", rows: await s.worktrees.rows(), trunks: await s.worktrees.trunks() });
23966
24516
  } while (statusesDirty);
23967
24517
  })().finally(() => {
23968
24518
  statusesInFlight = false;
@@ -24068,10 +24618,11 @@ function startServer(opts) {
24068
24618
  return {
24069
24619
  t: "hello",
24070
24620
  version,
24621
+ install: s.update.install(),
24071
24622
  protocol: PROTOCOL_VERSION,
24072
24623
  repos: s.state.repos,
24073
24624
  rows: await s.worktrees.rows({ quick: true }),
24074
- spares: s.worktrees.spares(),
24625
+ trunks: await s.worktrees.trunks({ quick: true }),
24075
24626
  themes: s.themes.themes,
24076
24627
  themePrefs: s.themes.prefs,
24077
24628
  agents: agentInfos(),
@@ -24106,7 +24657,8 @@ function startServer(opts) {
24106
24657
  remote: opts.remote,
24107
24658
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
24108
24659
  bootstrap: helloFrame,
24109
- restart: () => s.restarter.request(),
24660
+ restart: (now) => s.restarter.request({ now }),
24661
+ restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
24110
24662
  pair: new PairCodes,
24111
24663
  onPaired: () => {
24112
24664
  s.state.notePaired();
@@ -24218,16 +24770,16 @@ function startServer(opts) {
24218
24770
  }
24219
24771
 
24220
24772
  // ../daemon/src/themes/store.ts
24221
- import { existsSync as existsSync21, readdirSync as readdirSync5, readFileSync as readFileSync13, statSync as statSync6, writeFileSync as writeFileSync7 } from "fs";
24773
+ import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync8 } from "fs";
24222
24774
  import { homedir as homedir10 } from "os";
24223
- import { basename as basename4, dirname as dirname7, join as join18, resolve as resolve8 } from "path";
24775
+ import { basename as basename4, dirname as dirname7, join as join20, resolve as resolve8 } from "path";
24224
24776
  var home = homedir10();
24225
24777
  var defaultExtensionDirs = [
24226
- join18(home, ".vscode", "extensions"),
24227
- join18(home, ".vscode-insiders", "extensions"),
24228
- join18(home, ".vscode-oss", "extensions"),
24229
- join18(home, ".cursor", "extensions"),
24230
- join18(home, ".windsurf", "extensions"),
24778
+ join20(home, ".vscode", "extensions"),
24779
+ join20(home, ".vscode-insiders", "extensions"),
24780
+ join20(home, ".vscode-oss", "extensions"),
24781
+ join20(home, ".cursor", "extensions"),
24782
+ join20(home, ".windsurf", "extensions"),
24231
24783
  "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
24232
24784
  "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
24233
24785
  "/Applications/Cursor.app/Contents/Resources/app/extensions",
@@ -24311,14 +24863,14 @@ class ThemeStore {
24311
24863
  name: json?.name ?? base,
24312
24864
  source: "file"
24313
24865
  });
24314
- writeFileSync7(join18(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
24866
+ writeFileSync8(join20(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
24315
24867
  `);
24316
24868
  this.load();
24317
24869
  return theme;
24318
24870
  }
24319
24871
  }
24320
24872
  function readJsonc(file) {
24321
- return parse6(readFileSync13(file, "utf8"), [], { allowTrailingComma: true });
24873
+ return parse6(readFileSync14(file, "utf8"), [], { allowTrailingComma: true });
24322
24874
  }
24323
24875
  function readVscodeTheme(file, depth = 0) {
24324
24876
  const json = readJsonc(file);
@@ -24340,13 +24892,13 @@ function readVscodeTheme(file, depth = 0) {
24340
24892
  return t;
24341
24893
  }
24342
24894
  function loadDir(dir) {
24343
- if (!existsSync21(dir))
24895
+ if (!existsSync23(dir))
24344
24896
  return [];
24345
24897
  const out = [];
24346
- for (const f of readdirSync5(dir)) {
24898
+ for (const f of readdirSync6(dir)) {
24347
24899
  if (!/\.jsonc?$/i.test(f))
24348
24900
  continue;
24349
- const file = join18(dir, f);
24901
+ const file = join20(dir, f);
24350
24902
  const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
24351
24903
  try {
24352
24904
  const json = readJsonc(file);
@@ -24362,31 +24914,31 @@ function loadDir(dir) {
24362
24914
  return out;
24363
24915
  }
24364
24916
  function discover(dir) {
24365
- if (!existsSync21(dir))
24917
+ if (!existsSync23(dir))
24366
24918
  return [];
24367
24919
  const out = [];
24368
24920
  let entries;
24369
24921
  try {
24370
- entries = readdirSync5(dir);
24922
+ entries = readdirSync6(dir);
24371
24923
  } catch {
24372
24924
  return out;
24373
24925
  }
24374
24926
  for (const name of entries) {
24375
- const extDir = join18(dir, name);
24376
- const pkgFile = join18(extDir, "package.json");
24927
+ const extDir = join20(dir, name);
24928
+ const pkgFile = join20(extDir, "package.json");
24377
24929
  try {
24378
- if (!statSync6(extDir).isDirectory() || !existsSync21(pkgFile))
24930
+ if (!statSync6(extDir).isDirectory() || !existsSync23(pkgFile))
24379
24931
  continue;
24380
- const pkg = JSON.parse(readFileSync13(pkgFile, "utf8"));
24932
+ const pkg = JSON.parse(readFileSync14(pkgFile, "utf8"));
24381
24933
  const themes = pkg.contributes?.themes;
24382
24934
  if (!Array.isArray(themes))
24383
24935
  continue;
24384
24936
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
24385
24937
  let nls = {};
24386
- const nlsFile = join18(extDir, "package.nls.json");
24387
- if (existsSync21(nlsFile)) {
24938
+ const nlsFile = join20(extDir, "package.nls.json");
24939
+ if (existsSync23(nlsFile)) {
24388
24940
  try {
24389
- nls = JSON.parse(readFileSync13(nlsFile, "utf8"));
24941
+ nls = JSON.parse(readFileSync14(nlsFile, "utf8"));
24390
24942
  } catch {}
24391
24943
  }
24392
24944
  for (const c of themes) {
@@ -24525,6 +25077,21 @@ class UpdateService {
24525
25077
  this.wanted = true;
24526
25078
  await this.proceed();
24527
25079
  }
25080
+ async checkNow() {
25081
+ if (this.d.managed)
25082
+ throw new UserError("Updates are turned off for this machine (TOYON_UPDATES=off)");
25083
+ if (this.d.method === "none") {
25084
+ throw new UserError(`Toyon ${this.d.running} runs from a checkout, which does not update itself`);
25085
+ }
25086
+ await this.check();
25087
+ if (this.unreachable)
25088
+ throw new UserError(`Could not reach ${this.unreachable}`);
25089
+ if (this.target() === null)
25090
+ throw new UserError(`Toyon ${this.d.running} is the newest version`);
25091
+ }
25092
+ install() {
25093
+ return this.d.method;
25094
+ }
24528
25095
  get() {
24529
25096
  const restarting = this.d.restarter.waitingOn() ?? (this.wanted && !this.installing ? this.d.restarter.working() : null);
24530
25097
  if (this.installed === null && this.latest === null && !this.installing && this.failed === null && restarting === null) {
@@ -24745,14 +25312,22 @@ var DIFF_CHARS = 2000;
24745
25312
  class LandingService {
24746
25313
  d;
24747
25314
  judging = new Map;
25315
+ busy = new Set;
24748
25316
  constructor(d) {
24749
25317
  this.d = d;
24750
25318
  d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
24751
25319
  d.hub.on("agentStatus", (id, status) => {
25320
+ this.note(id, status);
24752
25321
  if (status === "working")
24753
25322
  this.clear(id);
24754
25323
  });
24755
25324
  }
25325
+ note(worktreeId, status) {
25326
+ if (status === "working" || status === "waiting")
25327
+ this.busy.add(worktreeId);
25328
+ else
25329
+ this.busy.delete(worktreeId);
25330
+ }
24756
25331
  clear(worktreeId) {
24757
25332
  this.judging.delete(worktreeId);
24758
25333
  this.d.worktrees.setLanding(worktreeId, undefined);
@@ -24763,28 +25338,91 @@ class LandingService {
24763
25338
  return;
24764
25339
  if (turn.end !== "done")
24765
25340
  return this.clear(worktreeId);
25341
+ if (!await this.hasWork(wt)) {
25342
+ this.clear(worktreeId);
25343
+ return this.recapAnswer(wt, turn);
25344
+ }
25345
+ await this.run(wt, { at: turn.at, ask: true, quiet: false });
25346
+ }
25347
+ async recapAnswer(wt, turn) {
25348
+ if (!this.d.recap)
25349
+ return;
25350
+ const worktreeId = wt.id;
25351
+ const entries = this.d.transcript(worktreeId);
25352
+ const turns = turnsSince(entries, 0);
25353
+ if (!turns.at(-1)?.reply.trim())
25354
+ return;
25355
+ this.judging.set(worktreeId, turn.at);
25356
+ const started = Date.now();
25357
+ let text = null;
25358
+ try {
25359
+ text = await this.d.recap(wt, answerPrompt({ title: wt.title, firstAsk: firstAskOf(entries), turns }));
25360
+ } catch (e) {
25361
+ log.warn(worktreeId, "answer recap failed", e);
25362
+ }
25363
+ if (this.judging.get(worktreeId) !== turn.at)
25364
+ return;
25365
+ this.judging.delete(worktreeId);
25366
+ const record = this.d.state.worktree(worktreeId)?.lastTurn;
25367
+ if (!text || !record || record.at !== turn.at)
25368
+ return;
25369
+ record.recap = { at: Date.now(), text };
25370
+ log.info(worktreeId, `answer recap, ${Date.now() - started}ms`);
25371
+ this.d.state.save();
25372
+ this.d.hub.emit("worktreesChanged");
25373
+ }
25374
+ async judge(worktreeId) {
25375
+ const wt = this.d.state.requireWorktree(worktreeId);
25376
+ if (!canLand(wt))
25377
+ throw new UserError("nothing to land from here");
25378
+ if (this.busy.has(worktreeId))
25379
+ throw new UserError("wait for the turn to end");
25380
+ if (!await this.hasWork(wt))
25381
+ throw new UserError("nothing to check: no changes here");
25382
+ fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: true, quiet: false }), "landing verdict by hand");
25383
+ }
25384
+ recheck(worktreeId) {
25385
+ const wt = this.d.state.worktree(worktreeId);
25386
+ if (!wt?.landing || !canLand(wt) || this.busy.has(worktreeId))
25387
+ return;
25388
+ fireAndForget(worktreeId, this.run(wt, { at: Date.now(), ask: false, quiet: true }), "landing recheck");
25389
+ }
25390
+ async hasWork(wt) {
24766
25391
  const repo = this.d.state.requireRepo(wt.repoId);
24767
25392
  const files = await statusFiles(wt.path);
25393
+ if (files.length > 0)
25394
+ return true;
24768
25395
  const { ahead } = await aheadBehind(wt.path, repo.defaultBranch);
24769
- if (files.length === 0 && ahead === 0)
25396
+ return ahead > 0;
25397
+ }
25398
+ async run(wt, opts) {
25399
+ const worktreeId = wt.id;
25400
+ const repo = this.d.state.requireRepo(wt.repoId);
25401
+ if (!await this.hasWork(wt))
24770
25402
  return this.clear(worktreeId);
24771
- this.judging.set(worktreeId, turn.at);
25403
+ const held = opts.ask ? undefined : wt.landing;
25404
+ const kept = {
25405
+ ...held?.why ? { why: held.why } : {},
25406
+ ...held?.subject ? { subject: held.subject } : {},
25407
+ ...held?.body ? { body: held.body } : {}
25408
+ };
25409
+ this.judging.set(worktreeId, opts.at);
24772
25410
  const started = Date.now();
24773
- this.d.worktrees.setLanding(worktreeId, { at: turn.at, check: "pending", ready: false, fingerprint: "" });
24774
- const live = () => this.judging.get(worktreeId) === turn.at && this.d.state.worktree(worktreeId)?.lastTurn?.at === turn.at;
25411
+ this.d.worktrees.setLanding(worktreeId, { at: opts.at, check: "pending", ready: false, fingerprint: "", ...kept });
25412
+ const live = () => this.judging.get(worktreeId) === opts.at && !!this.d.state.worktree(worktreeId);
24775
25413
  let check = "none";
24776
25414
  let checkTail;
24777
25415
  const command = repo.config.check?.trim();
24778
25416
  if (command) {
24779
- const r = await this.d.check(worktreeId, command);
25417
+ const r = await this.d.check(worktreeId, command, { quiet: opts.quiet });
24780
25418
  if (!live())
24781
25419
  return;
24782
25420
  check = r.exit === 0 ? "pass" : "fail";
24783
25421
  if (check === "fail")
24784
- checkTail = tail2(r.text) || `exit ${r.exit}`;
25422
+ checkTail = tail(r.text) || `exit ${r.exit}`;
24785
25423
  }
24786
25424
  let verdict = null;
24787
- if (check !== "fail" && this.d.judge) {
25425
+ if (opts.ask && check !== "fail" && this.d.judge) {
24788
25426
  const entries = this.d.transcript(worktreeId);
24789
25427
  const prompt = landPrompt({
24790
25428
  title: wt.title,
@@ -24802,10 +25440,11 @@ class LandingService {
24802
25440
  return;
24803
25441
  }
24804
25442
  const landing = {
24805
- at: turn.at,
25443
+ at: opts.at,
24806
25444
  check,
24807
25445
  ...checkTail ? { checkTail } : {},
24808
25446
  ready: check !== "fail",
25447
+ ...kept,
24809
25448
  ...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
24810
25449
  ...verdict?.subject ? { subject: verdict.subject } : {},
24811
25450
  ...verdict?.body ? { body: verdict.body } : {},
@@ -24814,11 +25453,14 @@ class LandingService {
24814
25453
  if (!live())
24815
25454
  return;
24816
25455
  this.judging.delete(worktreeId);
24817
- log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}, ${Date.now() - started}ms`);
25456
+ const record = this.d.state.worktree(worktreeId)?.lastTurn;
25457
+ if (record && verdict?.recap)
25458
+ record.recap = { at: Date.now(), text: verdict.recap };
25459
+ log.info(worktreeId, `landing: check ${check}${landing.why ? ", doubted" : ""}${opts.ask ? "" : ", check only"}, ${Date.now() - started}ms`);
24818
25460
  this.d.worktrees.setLanding(worktreeId, landing);
24819
25461
  }
24820
25462
  }
24821
- function tail2(text) {
25463
+ function tail(text) {
24822
25464
  const t = text.replace(/\s+$/, "");
24823
25465
  return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
24824
25466
  }
@@ -25028,11 +25670,8 @@ function discoveredId(path) {
25028
25670
  }
25029
25671
  function knownPaths(known) {
25030
25672
  const paths = new Set;
25031
- for (const wt of known) {
25673
+ for (const wt of known)
25032
25674
  paths.add(canonical(wt.path));
25033
- if (wt.linkPath)
25034
- paths.add(canonical(wt.linkPath));
25035
- }
25036
25675
  return paths;
25037
25676
  }
25038
25677
  function subtractKnown(repoId, listed, known) {
@@ -25100,7 +25739,7 @@ class RefSearch {
25100
25739
  rowIdForPath = (path) => {
25101
25740
  const want = canonical(path);
25102
25741
  for (const wt of this.d.state.worktrees) {
25103
- if (canonical(wt.path) === want || wt.linkPath && canonical(wt.linkPath) === want)
25742
+ if (canonical(wt.path) === want)
25104
25743
  return wt.id;
25105
25744
  }
25106
25745
  return discoveredId(path);
@@ -25210,8 +25849,8 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
25210
25849
  }
25211
25850
 
25212
25851
  // ../daemon/src/worktrees/service.ts
25213
- import { existsSync as existsSync23, lstatSync, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
25214
- import { basename as basename6, dirname as dirname8, join as join21 } from "path";
25852
+ import { cpSync, existsSync as existsSync26, readFileSync as readFileSync16, rmSync as rmSync5 } from "fs";
25853
+ import { dirname as dirname9, join as join23 } from "path";
25215
25854
 
25216
25855
  // ../daemon/src/git/archive.ts
25217
25856
  import { rmSync as rmSync3 } from "fs";
@@ -25220,12 +25859,16 @@ async function keepState(repoPath, wtPath, ref, indexFile) {
25220
25859
  const head = await git(wtPath, "rev-parse", "--verify", "HEAD");
25221
25860
  if (!head.ok)
25222
25861
  return null;
25223
- const status = await git(wtPath, "status", "--porcelain");
25224
- const dirty = !status.ok || status.out !== "";
25862
+ const status = await gitRaw(wtPath, "status", "--porcelain", "-uall");
25863
+ const files = status.ok ? parsePorcelain(status.out).length : null;
25864
+ const dirty = files === null || files > 0;
25225
25865
  const snapshot = dirty ? await snapshotCommit(wtPath, head.out, indexFile) : null;
25226
25866
  if (!(await git(repoPath, "update-ref", ref, snapshot ?? head.out)).ok)
25227
25867
  return null;
25228
- return { head: head.out, ...snapshot ? { snapshot } : dirty ? { lost: true } : {} };
25868
+ return {
25869
+ head: head.out,
25870
+ ...snapshot ? { snapshot, ...files ? { dirty: files } : {} } : dirty ? { lost: true } : {}
25871
+ };
25229
25872
  }
25230
25873
  async function snapshotCommit(wtPath, head, indexFile) {
25231
25874
  const env = { GIT_INDEX_FILE: indexFile };
@@ -25277,6 +25920,32 @@ async function landingMark(worktreePath, defaultBr) {
25277
25920
  return null;
25278
25921
  return { base: base.out, tip: tip.out };
25279
25922
  }
25923
+ async function handLanding(worktreePath, branch, defaultBr, since) {
25924
+ const log = await git(worktreePath, "reflog", "show", "--format=%H %gs", branch);
25925
+ if (!log.ok || !log.out)
25926
+ return null;
25927
+ let own = 0;
25928
+ for (const line of log.out.split(`
25929
+ `)) {
25930
+ const subject = line.slice(line.indexOf(" ") + 1);
25931
+ if (/^(reset|branch):/.test(subject))
25932
+ break;
25933
+ if (/^(commit|cherry-pick)\b/.test(subject) && !subject.startsWith("commit (amend)"))
25934
+ own++;
25935
+ }
25936
+ if (own === 0)
25937
+ return null;
25938
+ const [tip, walked, onMain, fromSince] = await Promise.all([
25939
+ git(worktreePath, "rev-parse", "HEAD"),
25940
+ git(worktreePath, "rev-parse", `HEAD~${own}`),
25941
+ git(worktreePath, "merge-base", "--is-ancestor", "HEAD", defaultBr),
25942
+ since ? git(worktreePath, "merge-base", "--is-ancestor", since, "HEAD") : null
25943
+ ]);
25944
+ if (!tip.ok || !onMain.ok || !walked.ok)
25945
+ return null;
25946
+ const base = fromSince?.ok && since ? since : walked.out;
25947
+ return base === tip.out ? null : { base, tip: tip.out };
25948
+ }
25280
25949
  async function dropLandRefs(repoPath, worktreeId) {
25281
25950
  const r = await git(repoPath, "for-each-ref", "--format=%(refname)", `refs/toyon/lands/${worktreeId}/`);
25282
25951
  if (!r.ok || !r.out)
@@ -25292,8 +25961,11 @@ async function commitWorktree(worktreePath, message) {
25292
25961
  return { ok: false, message: "nothing to commit" };
25293
25962
  await git(worktreePath, "add", "-A");
25294
25963
  const c = await git(worktreePath, "commit", "-m", message);
25295
- if (!c.ok)
25296
- return { ok: false, message: `commit failed: ${c.err.slice(0, 200)}` };
25964
+ if (!c.ok) {
25965
+ const output = [c.out, c.err].filter(Boolean).join(`
25966
+ `);
25967
+ return { ok: false, message: `commit failed: ${c.err.slice(0, 200)}`, output, exit: c.exit };
25968
+ }
25297
25969
  return { ok: true, message: `committed: ${message}` };
25298
25970
  }
25299
25971
  async function requireClean(worktreePath) {
@@ -25399,21 +26071,37 @@ async function fastForwardMain(repoPath, defaultBr) {
25399
26071
  const f = await run(GIT, ["fetch", "--quiet"], repoPath, NO_PROMPT);
25400
26072
  if (!f.ok)
25401
26073
  return { ok: false, message: `fetch failed: ${f.err.slice(0, 200)}` };
26074
+ return fastForwardFetched(repoPath, defaultBr);
26075
+ }
26076
+ async function fastForwardFetched(repoPath, defaultBr) {
26077
+ const current = await git(repoPath, "branch", "--show-current");
26078
+ if (current.out !== defaultBr) {
26079
+ return { ok: false, message: `main checkout is on '${current.out}', not ${defaultBr}; switch it first` };
26080
+ }
25402
26081
  const behind = await behindUpstream(repoPath);
25403
- if (behind === null)
25404
- return { ok: false, message: `${defaultBr} has no upstream to pull from` };
26082
+ if (behind === null) {
26083
+ return { ok: false, stale: "no-upstream", message: `${defaultBr} has no upstream to pull from` };
26084
+ }
25405
26085
  if (behind === 0)
25406
- return { ok: true, message: "already up to date with origin" };
26086
+ return { ok: true, moved: false, message: "already up to date with origin" };
26087
+ if ((await statusFiles(repoPath)).length > 0) {
26088
+ return {
26089
+ ok: false,
26090
+ stale: "dirty",
26091
+ message: `${defaultBr} has uncommitted changes: commit or stash them there first`
26092
+ };
26093
+ }
25407
26094
  const m = await git(repoPath, "merge", "--ff-only", "@{upstream}");
25408
26095
  if (!m.ok) {
25409
26096
  const clobber = /overwritten by merge|would be overwritten/.test(`${m.out}
25410
26097
  ${m.err}`);
25411
- return {
26098
+ return clobber ? {
25412
26099
  ok: false,
25413
- message: clobber ? `${defaultBr} here has edits the pull would overwrite; commit or stash them first` : `${defaultBr} has diverged from origin; reconcile it in a terminal`
25414
- };
26100
+ stale: "dirty",
26101
+ message: `${defaultBr} here has edits the pull would overwrite; commit or stash them first`
26102
+ } : { ok: false, stale: "diverged", message: `${defaultBr} has diverged from origin; reconcile it in a terminal` };
25415
26103
  }
25416
- return { ok: true, message: `pulled ${behind} commit(s) from origin` };
26104
+ return { ok: true, moved: true, message: `pulled ${behind} commit(s) from origin` };
25417
26105
  }
25418
26106
  async function pushMain(repoPath, defaultBr) {
25419
26107
  const p = await run(GIT, ["push", "origin", defaultBr], repoPath, NO_PROMPT);
@@ -25435,6 +26123,12 @@ async function mergeFailure(cwd, m, conflictMessage) {
25435
26123
  }
25436
26124
  return { ok: false, message: `merge failed: ${said.slice(0, 300)}` };
25437
26125
  }
26126
+ async function pushBranch(worktreePath, branch) {
26127
+ const push = await run(GIT, ["push", "-u", "--force-with-lease", "origin", branch], worktreePath, NO_PROMPT);
26128
+ if (!push.ok)
26129
+ return { ok: false, message: `push failed: ${push.err.slice(0, 300)}` };
26130
+ return { ok: true, message: `pushed ${branch}` };
26131
+ }
25438
26132
  async function openPr(o) {
25439
26133
  const cErr = await requireClean(o.worktreePath);
25440
26134
  if (cErr)
@@ -25445,9 +26139,9 @@ async function openPr(o) {
25445
26139
  const remote = await git(o.worktreePath, "remote", "get-url", "origin");
25446
26140
  if (!remote.ok)
25447
26141
  return { ok: false, message: "no 'origin' remote: add one, or land by merging here" };
25448
- const push = await run(GIT, ["push", "-u", "--force-with-lease", "origin", o.branch], o.worktreePath, NO_PROMPT);
26142
+ const push = await pushBranch(o.worktreePath, o.branch);
25449
26143
  if (!push.ok)
25450
- return { ok: false, message: `push failed: ${push.err.slice(0, 300)}` };
26144
+ return push;
25451
26145
  const titled = o.subject ? ["--title", o.subject, "--body", o.body ?? ""] : ["--fill"];
25452
26146
  const created = await gh(["pr", "create", "--head", o.branch, "--base", o.defaultBr, ...titled], o.worktreePath);
25453
26147
  let url = null;
@@ -25497,20 +26191,20 @@ function compareUrl(remoteUrl, base, branch) {
25497
26191
  }
25498
26192
 
25499
26193
  // ../daemon/src/worktrees/archive.ts
25500
- import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync6, readFileSync as readFileSync14, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync8 } from "fs";
25501
- import { join as join19 } from "path";
26194
+ import { existsSync as existsSync24, mkdirSync as mkdirSync7, readdirSync as readdirSync7, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
26195
+ import { basename as basename6, dirname as dirname8, join as join21 } from "path";
25502
26196
  class WorktreeArchive {
25503
26197
  dir;
25504
26198
  records = new Map;
25505
26199
  constructor(dir) {
25506
26200
  this.dir = dir;
25507
- mkdirSync6(dir, { recursive: true });
25508
- for (const id of readdirSync6(dir)) {
26201
+ mkdirSync7(dir, { recursive: true });
26202
+ for (const id of readdirSync7(dir)) {
25509
26203
  const file = this.filesOf(id).record;
25510
- if (!existsSync22(file))
26204
+ if (!existsSync24(file))
25511
26205
  continue;
25512
26206
  try {
25513
- this.records.set(id, JSON.parse(readFileSync14(file, "utf8")));
26207
+ this.records.set(id, JSON.parse(readFileSync15(file, "utf8")));
25514
26208
  } catch (e) {
25515
26209
  log.warn("archive", `skipped an unreadable record: ${file}`, e);
25516
26210
  }
@@ -25521,11 +26215,13 @@ class WorktreeArchive {
25521
26215
  }
25522
26216
  put(rec, from) {
25523
26217
  const at = this.filesOf(rec.worktree.id);
25524
- mkdirSync6(at.dir, { recursive: true });
26218
+ mkdirSync7(at.dir, { recursive: true });
25525
26219
  move(from.transcript, at.transcript);
25526
26220
  move(from.attachments, at.attachments);
25527
- writeFileSync8(at.record, JSON.stringify(rec, null, 2));
26221
+ move(from.plans, at.plans);
26222
+ writeFileSync9(at.record, JSON.stringify(rec, null, 2));
25528
26223
  this.records.set(rec.worktree.id, rec);
26224
+ return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
25529
26225
  }
25530
26226
  take(id, to) {
25531
26227
  if (!this.records.has(id))
@@ -25533,6 +26229,7 @@ class WorktreeArchive {
25533
26229
  const at = this.filesOf(id);
25534
26230
  move(at.transcript, to.transcript);
25535
26231
  move(at.attachments, to.attachments);
26232
+ move(at.plans, to.plans);
25536
26233
  this.delete(id);
25537
26234
  }
25538
26235
  delete(id) {
@@ -25542,25 +26239,39 @@ class WorktreeArchive {
25542
26239
  this.records.delete(id);
25543
26240
  }
25544
26241
  list(repo) {
25545
- return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id));
26242
+ return [...this.records.values()].filter((r) => r.worktree.repoId === repo.id || r.repoPath === repo.path).sort((a, b) => b.archivedAt - a.archivedAt).map((r) => summarize(r, repo.id, this.filesOf(r.worktree.id).transcript));
25546
26243
  }
25547
26244
  chatFiles(id) {
25548
26245
  if (!this.records.has(id))
25549
26246
  return;
25550
26247
  const at = this.filesOf(id);
25551
- return { transcript: at.transcript, attachments: at.attachments };
26248
+ return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
26249
+ }
26250
+ planFile(id, rel) {
26251
+ if (!this.records.has(id) || !isPlanPath(rel))
26252
+ return null;
26253
+ const file = join21(this.filesOf(id).plans, basename6(rel));
26254
+ if (!existsSync24(file))
26255
+ return null;
26256
+ try {
26257
+ return readFileSync15(file, "utf8");
26258
+ } catch (e) {
26259
+ log.warn(id, `could not read its archived plan ${rel}`, e);
26260
+ return null;
26261
+ }
25552
26262
  }
25553
26263
  filesOf(id) {
25554
- const dir = join19(this.dir, id);
26264
+ const dir = join21(this.dir, id);
25555
26265
  return {
25556
26266
  dir,
25557
- record: join19(dir, "record.json"),
25558
- transcript: join19(dir, "transcript.jsonl"),
25559
- attachments: join19(dir, "attachments")
26267
+ record: join21(dir, "record.json"),
26268
+ transcript: join21(dir, "transcript.jsonl"),
26269
+ attachments: join21(dir, "attachments"),
26270
+ plans: join21(dir, "plans")
25560
26271
  };
25561
26272
  }
25562
26273
  }
25563
- function summarize(r, repoId) {
26274
+ function summarize(r, repoId, transcript) {
25564
26275
  return {
25565
26276
  id: r.worktree.id,
25566
26277
  repoId,
@@ -25571,16 +26282,18 @@ function summarize(r, repoId) {
25571
26282
  archivedAt: r.archivedAt,
25572
26283
  ...r.prompt ? { prompt: r.prompt } : {},
25573
26284
  restorable: !!r.kept,
25574
- ...r.kept?.snapshot ? { uncommitted: true } : {},
26285
+ transcript,
26286
+ ...r.sessionId ? { sessionId: r.sessionId } : {},
26287
+ ...r.kept?.snapshot ? { uncommitted: true, ...r.kept.dirty ? { dirty: r.kept.dirty } : {} } : {},
25575
26288
  ...r.worktree.landed ? { landed: true } : {},
25576
26289
  ...r.cost !== undefined ? { cost: r.cost } : {},
25577
26290
  ...r.auto ? { auto: r.auto } : {}
25578
26291
  };
25579
26292
  }
25580
26293
  function lastUsage(transcript) {
25581
- if (!existsSync22(transcript))
26294
+ if (!existsSync24(transcript))
25582
26295
  return null;
25583
- const lines = readFileSync14(transcript, "utf8").split(`
26296
+ const lines = readFileSync15(transcript, "utf8").split(`
25584
26297
  `);
25585
26298
  for (let i = lines.length - 1;i >= 0; i--) {
25586
26299
  if (!lines[i]?.includes('"usage"'))
@@ -25594,9 +26307,9 @@ function lastUsage(transcript) {
25594
26307
  return null;
25595
26308
  }
25596
26309
  function firstPrompt(transcript) {
25597
- if (!existsSync22(transcript))
26310
+ if (!existsSync24(transcript))
25598
26311
  return;
25599
- for (const line of readFileSync14(transcript, "utf8").split(`
26312
+ for (const line of readFileSync15(transcript, "utf8").split(`
25600
26313
  `)) {
25601
26314
  if (!line.includes('"user-message"'))
25602
26315
  continue;
@@ -25609,8 +26322,9 @@ function firstPrompt(transcript) {
25609
26322
  return;
25610
26323
  }
25611
26324
  function move(from, to) {
25612
- if (!existsSync22(from))
26325
+ if (!existsSync24(from))
25613
26326
  return;
26327
+ mkdirSync7(dirname8(to), { recursive: true });
25614
26328
  rmSync4(to, { recursive: true, force: true });
25615
26329
  renameSync4(from, to);
25616
26330
  }
@@ -25673,16 +26387,31 @@ class ArchivedGit {
25673
26387
  }
25674
26388
 
25675
26389
  // ../daemon/src/worktrees/spare.ts
25676
- import { join as join20 } from "path";
26390
+ import { existsSync as existsSync25 } from "fs";
26391
+ import { basename as basename7, join as join22 } from "path";
25677
26392
  class SparePool {
25678
26393
  d;
25679
- spares = new Map;
26394
+ inflight = new Map;
26395
+ refreshing = new Map;
25680
26396
  constructor(d) {
25681
26397
  this.d = d;
25682
26398
  }
26399
+ spareOf(repoId) {
26400
+ return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
26401
+ }
25683
26402
  current(repoId) {
25684
- const entry = this.spares.get(repoId);
25685
- return entry?.worktreeId ? { worktreeId: entry.worktreeId, ready: entry.ready } : null;
26403
+ const wt = this.spareOf(repoId);
26404
+ return wt ? { worktreeId: wt.id, ready: wt.phase === "ready" } : null;
26405
+ }
26406
+ typed(boxId, text) {
26407
+ if (!text.trim())
26408
+ return;
26409
+ const wt = this.d.state.worktree(boxId);
26410
+ if (wt?.kind !== "spare" || !existsSync25(wt.path))
26411
+ return;
26412
+ const agent = this.d.runtime.ensureAgent(wt).agent;
26413
+ if (agent.runningAgent === null)
26414
+ fireAndForget(wt.id, agent.warm(), "warm on the first keystroke");
25686
26415
  }
25687
26416
  adopt(repoId) {
25688
26417
  const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
@@ -25691,76 +26420,125 @@ class SparePool {
25691
26420
  const spare = persisted[0];
25692
26421
  if (!spare)
25693
26422
  return;
25694
- this.spares.set(repoId, {
25695
- worktreeId: spare.id,
25696
- lockHash: lockfileHash(spare.path),
25697
- refreshing: null,
25698
- ready: true
25699
- });
26423
+ spare.phase ??= "ready";
26424
+ spare.lockfile ??= lockfileHash(spare.path);
25700
26425
  }
25701
26426
  warm(repoId) {
25702
- if (this.spares.has(repoId))
26427
+ const wt = this.spareOf(repoId);
26428
+ if (wt?.phase === "ready")
25703
26429
  fireAndForget(repoId, this.revive(repoId), "spare refresh");
25704
26430
  else
25705
26431
  fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
25706
26432
  }
25707
26433
  async revive(repoId) {
25708
26434
  await this.refresh(repoId);
25709
- const entry = this.spares.get(repoId);
25710
- const wt = entry?.ready ? this.d.state.worktree(entry.worktreeId) : undefined;
25711
- if (wt?.kind !== "spare" || this.d.runtime.get(wt.id)?.procs)
26435
+ const wt = this.spareOf(repoId);
26436
+ if (wt?.phase !== "ready" || this.d.runtime.get(wt.id)?.procs)
25712
26437
  return;
25713
26438
  await this.d.runtime.start(wt, this.d.state.requireRepo(repoId));
25714
26439
  }
25715
- async ensure(repoId) {
25716
- if (this.spares.has(repoId))
25717
- return;
26440
+ ensure(repoId) {
26441
+ const running = this.inflight.get(repoId);
26442
+ if (running)
26443
+ return running.done;
26444
+ const have = this.spareOf(repoId);
26445
+ if (have?.phase === "ready")
26446
+ return Promise.resolve();
25718
26447
  const repo = this.d.state.requireRepo(repoId);
26448
+ if (have)
26449
+ return this.warmUp(have, repo, {});
25719
26450
  if (repo.needsSetup)
25720
- return;
25721
- const entry = { worktreeId: "", lockHash: "", refreshing: null, ready: false };
25722
- this.spares.set(repoId, entry);
25723
- try {
25724
- const slug = `wt-${shortId().slice(0, 4)}`;
25725
- const wtPath = join20(this.d.paths.worktreesDir, repo.name, slug);
25726
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wtPath, repo.defaultBranch));
25727
- const wt = {
25728
- id: shortId(),
25729
- repoId,
25730
- path: wtPath,
25731
- branch: repo.defaultBranch,
25732
- kind: "spare",
25733
- proxyPort: await allocateProxyPort(),
25734
- title: slug,
25735
- createdAt: Date.now()
25736
- };
25737
- entry.worktreeId = wt.id;
25738
- this.d.state.addWorktree(wt);
25739
- await this.d.setupAndStart(wt, repo);
25740
- entry.lockHash = lockfileHash(wt.path);
25741
- entry.ready = true;
25742
- this.d.hub.emit("worktreesChanged");
25743
- } catch (e) {
25744
- log.warn(repoId, "spare warm-up failed; rolling back", e);
25745
- this.spares.delete(repoId);
25746
- if (entry.worktreeId && this.d.state.worktree(entry.worktreeId)) {
25747
- await this.d.discard(entry.worktreeId).catch((re) => log.warn(repoId, "spare rollback failed", re));
26451
+ return Promise.resolve();
26452
+ return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
26453
+ }
26454
+ async record(repoId, repo) {
26455
+ const { id, dir } = freeSlot(join22(this.d.paths.worktreesDir, repo.name));
26456
+ const wt = {
26457
+ id,
26458
+ repoId,
26459
+ path: join22(this.d.paths.worktreesDir, repo.name, dir),
26460
+ branch: repo.defaultBranch,
26461
+ kind: "spare",
26462
+ phase: "reserved",
26463
+ proxyPort: await allocateProxyPort(),
26464
+ title: dir,
26465
+ createdAt: Date.now()
26466
+ };
26467
+ this.d.state.addWorktree(wt);
26468
+ return wt;
26469
+ }
26470
+ warmUp(wt, repo, { after }) {
26471
+ const repoId = repo.id;
26472
+ let cut = () => {};
26473
+ const added = (async () => {
26474
+ if (existsSync25(wt.path))
26475
+ return;
26476
+ await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wt.path, repo.defaultBranch));
26477
+ })();
26478
+ const done = (async () => {
26479
+ try {
26480
+ await added;
26481
+ if (after) {
26482
+ await Promise.race([
26483
+ this.d.runtime.awaitPreview(after, STAGGER_MS),
26484
+ new Promise((r) => {
26485
+ cut = r;
26486
+ })
26487
+ ]);
26488
+ }
26489
+ if (!this.d.state.worktree(wt.id))
26490
+ return;
26491
+ if (wt.kind === "spare") {
26492
+ wt.phase = "warming";
26493
+ this.d.state.save();
26494
+ this.d.hub.emit("worktreesChanged");
26495
+ await withRepoLock(repo.path, () => git(wt.path, "reset", "--hard", repo.defaultBranch));
26496
+ }
26497
+ await this.d.setupAndStart(wt, repo);
26498
+ if (wt.kind !== "spare")
26499
+ return;
26500
+ wt.lockfile = lockfileHash(wt.path);
26501
+ wt.phase = "ready";
26502
+ this.d.state.save();
26503
+ this.d.ready?.(repoId);
26504
+ this.d.hub.emit("worktreesChanged");
26505
+ } catch (e) {
26506
+ await this.rollBack(wt, e);
26507
+ } finally {
26508
+ if (this.inflight.get(repoId)?.added === added)
26509
+ this.inflight.delete(repoId);
25748
26510
  }
26511
+ })();
26512
+ this.inflight.set(repoId, { added, done, cut: () => cut() });
26513
+ return done;
26514
+ }
26515
+ async rollBack(wt, e) {
26516
+ if (wt.kind !== "spare") {
26517
+ log.warn(wt.id, "setup failed after the spare was claimed", e);
26518
+ return;
26519
+ }
26520
+ log.warn(wt.repoId, "spare warm-up failed; rolling back", e);
26521
+ this.d.carryDraft?.(wt.id, wt.repoId);
26522
+ if (existsSync25(wt.path)) {
26523
+ await this.d.discard(wt.id).catch((re) => log.warn(wt.repoId, "spare rollback failed", re));
26524
+ return;
25749
26525
  }
26526
+ this.d.state.removeWorktree(wt.id);
26527
+ releasePort(wt.proxyPort);
26528
+ this.d.state.save();
26529
+ this.d.hub.emit("worktreesChanged");
25750
26530
  }
25751
26531
  async refresh(repoId) {
25752
- const entry = this.spares.get(repoId);
25753
- if (!entry?.ready || entry.refreshing)
26532
+ const wt = this.spareOf(repoId);
26533
+ if (wt?.phase !== "ready" || this.refreshing.has(repoId))
25754
26534
  return;
25755
26535
  const repo = this.d.state.requireRepo(repoId);
25756
- const wt = this.d.state.worktree(entry.worktreeId);
25757
- if (wt?.kind !== "spare")
25758
- return;
25759
- entry.refreshing = (async () => {
26536
+ const run = (async () => {
25760
26537
  await withRepoLock(repo.path, () => git(wt.path, "reset", "--hard", repo.defaultBranch));
25761
26538
  const h = lockfileHash(wt.path);
25762
- if (h !== entry.lockHash) {
25763
- entry.lockHash = h;
26539
+ if (h !== wt.lockfile) {
26540
+ wt.lockfile = h;
26541
+ this.d.state.save();
25764
26542
  for (const cmd of repo.config.setup ?? []) {
25765
26543
  const tail = [];
25766
26544
  const code = await runSetup(cmd, wt.path, (line) => {
@@ -25774,70 +26552,161 @@ class SparePool {
25774
26552
  }
25775
26553
  }
25776
26554
  })().finally(() => {
25777
- entry.refreshing = null;
26555
+ this.refreshing.delete(repoId);
25778
26556
  });
25779
- await entry.refreshing;
26557
+ this.refreshing.set(repoId, run);
26558
+ await run;
25780
26559
  }
25781
- async claim(repoId, branch, slug) {
25782
- const entry = this.spares.get(repoId);
25783
- if (!entry?.ready)
26560
+ async claim(repoId, title, worktreeId) {
26561
+ const wt = this.spareOf(repoId);
26562
+ if (!wt)
25784
26563
  return null;
25785
- this.spares.delete(repoId);
25786
- if (entry.refreshing)
25787
- await entry.refreshing;
25788
- const wt = this.d.state.worktree(entry.worktreeId);
25789
- if (wt?.kind !== "spare")
26564
+ if (worktreeId && wt.id !== worktreeId)
25790
26565
  return null;
25791
- const repo = this.d.state.requireRepo(repoId);
25792
- try {
25793
- await withRepoLock(repo.path, () => gitOrThrow(wt.path, "switch", "-c", branch));
25794
- } catch (e) {
25795
- this.spares.set(repoId, { ...entry, ready: true });
25796
- throw e;
26566
+ const wasReady = wt.phase === "ready";
26567
+ if (!wasReady) {
26568
+ if (!worktreeId)
26569
+ return null;
26570
+ const running = this.inflight.get(repoId);
26571
+ await running?.added.catch(() => {});
26572
+ running?.cut();
26573
+ if (wt.kind !== "spare" || !existsSync25(wt.path))
26574
+ return null;
25797
26575
  }
26576
+ await this.refreshing.get(repoId);
26577
+ const repo = this.d.state.requireRepo(repoId);
26578
+ const branch = `toyon/${basename7(wt.path)}`;
26579
+ await withRepoLock(repo.path, () => gitOrThrow(wt.path, "switch", "-c", branch));
25798
26580
  wt.kind = "worktree";
26581
+ delete wt.phase;
26582
+ delete wt.lockfile;
25799
26583
  wt.branch = branch;
25800
- wt.title = slug;
26584
+ wt.title = title;
26585
+ wt.unnamed = true;
25801
26586
  wt.createdAt = Date.now();
26587
+ if (wasReady)
26588
+ fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
26589
+ const next = repo.needsSetup ? null : await this.record(repoId, repo);
25802
26590
  this.d.state.save();
25803
- fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
25804
- fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
26591
+ if (next)
26592
+ fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
25805
26593
  return wt;
25806
26594
  }
25807
26595
  }
25808
26596
 
26597
+ // ../daemon/src/worktrees/trunk.ts
26598
+ var FETCH_EVERY_MS = 5 * 60000;
26599
+ var OPEN_FETCH_MIN_MS = 60000;
26600
+ function mainOf(state, repoId) {
26601
+ return state.worktrees.find((w) => w.repoId === repoId && w.kind === "main");
26602
+ }
26603
+
26604
+ class Trunk {
26605
+ d;
26606
+ lastFetch = new Map;
26607
+ stale = new Map;
26608
+ constructor(d) {
26609
+ this.d = d;
26610
+ }
26611
+ async behind(mainId, path) {
26612
+ const behind = await behindUpstream(path);
26613
+ if (behind === null)
26614
+ return {};
26615
+ const last = this.lastFetch.get(path) ?? 0;
26616
+ if (Date.now() - last > FETCH_EVERY_MS) {
26617
+ this.lastFetch.set(path, Date.now());
26618
+ fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then(async (r) => {
26619
+ if (!r.ok) {
26620
+ log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
26621
+ return;
26622
+ }
26623
+ this.d.invalidateCounts();
26624
+ this.d.hub.emit("worktreesChanged");
26625
+ if (await behindUpstream(path) ?? 0)
26626
+ await this.follow(mainId);
26627
+ }));
26628
+ }
26629
+ return { behind };
26630
+ }
26631
+ async sync(repoId) {
26632
+ const repo = this.d.state.repo(repoId);
26633
+ const main = mainOf(this.d.state, repoId);
26634
+ if (!repo || !main)
26635
+ return;
26636
+ const last = this.lastFetch.get(repo.path) ?? 0;
26637
+ if (Date.now() - last < OPEN_FETCH_MIN_MS)
26638
+ return;
26639
+ this.lastFetch.set(repo.path, Date.now());
26640
+ const f = await run(GIT, ["fetch", "--quiet"], repo.path, NO_PROMPT);
26641
+ if (!f.ok) {
26642
+ log.warn("fetch", `could not fetch ${repo.path}: ${f.err.slice(0, 200)}`);
26643
+ return;
26644
+ }
26645
+ this.d.invalidateCounts();
26646
+ await this.follow(main.id);
26647
+ }
26648
+ async follow(mainId) {
26649
+ const main = this.d.state.worktree(mainId);
26650
+ const repo = main && this.d.state.repo(main.repoId);
26651
+ if (!main || !repo)
26652
+ return;
26653
+ const ff = await withRepoLock(repo.path, () => fastForwardFetched(repo.path, repo.defaultBranch));
26654
+ const was = this.stale.get(repo.id);
26655
+ if (ff.stale)
26656
+ this.stale.set(repo.id, ff.stale);
26657
+ else
26658
+ this.stale.delete(repo.id);
26659
+ if (ff.moved) {
26660
+ this.d.invalidateCounts();
26661
+ this.d.hub.emit("worktreesChanged");
26662
+ } else if (was !== ff.stale)
26663
+ this.d.hub.emit("worktreesChanged");
26664
+ }
26665
+ async all(opts = {}) {
26666
+ const quick = opts.quick ? { missed: false } : null;
26667
+ const out = {};
26668
+ await Promise.all(this.d.state.repos.map(async (repo) => {
26669
+ const main = mainOf(this.d.state, repo.id);
26670
+ if (!main)
26671
+ return;
26672
+ const { behind, dirty } = await this.d.counts(main, repo.defaultBranch, quick);
26673
+ const stale = this.stale.get(repo.id);
26674
+ out[repo.id] = {
26675
+ id: main.id,
26676
+ ...behind !== undefined ? { behind } : {},
26677
+ dirty: dirty ?? 0,
26678
+ empty: main.empty === true,
26679
+ ...stale ? { stale } : {}
26680
+ };
26681
+ }));
26682
+ if (quick?.missed)
26683
+ setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
26684
+ return out;
26685
+ }
26686
+ }
26687
+
25809
26688
  // ../daemon/src/worktrees/turns.ts
25810
26689
  var TURN_HOLD = "turn";
25811
- var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
25812
26690
 
25813
26691
  class TurnService {
25814
26692
  d;
25815
26693
  lastStatus = new Map;
25816
26694
  stoppedByHand = new Set;
25817
- timers = new Map;
25818
- summarized = new Map;
25819
26695
  constructor(d) {
25820
26696
  this.d = d;
25821
26697
  this.boot();
25822
26698
  d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
25823
26699
  }
25824
26700
  boot() {
25825
- const now = Date.now();
25826
26701
  let changed = false;
25827
26702
  for (const wt of this.d.state.worktrees) {
25828
26703
  const turn = wt.lastTurn;
25829
- if (!turn)
26704
+ if (turn?.end !== "asking")
25830
26705
  continue;
25831
- if (turn.end === "asking") {
25832
- const { ask: _expired, ...facts } = turn.facts;
25833
- turn.end = "stopped";
25834
- turn.facts = facts;
25835
- changed = true;
25836
- }
25837
- if (!turn.recap && (wt.seenAt ?? 0) < turn.at) {
25838
- turn.recap = { at: now };
25839
- changed = true;
25840
- }
26706
+ const { ask: _expired, ...facts } = turn.facts;
26707
+ turn.end = "stopped";
26708
+ turn.facts = facts;
26709
+ changed = true;
25841
26710
  }
25842
26711
  if (changed)
25843
26712
  this.d.state.save();
@@ -25845,10 +26714,8 @@ class TurnService {
25845
26714
  onStatus(worktreeId, status) {
25846
26715
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
25847
26716
  this.lastStatus.set(worktreeId, status);
25848
- if (status === "working") {
25849
- this.disarm(worktreeId);
26717
+ if (status === "working")
25850
26718
  this.d.hold?.(worktreeId, TURN_HOLD);
25851
- }
25852
26719
  const edge = stopOf(prev, status);
25853
26720
  if (!edge)
25854
26721
  return;
@@ -25863,64 +26730,13 @@ class TurnService {
25863
26730
  wt.lastTurn = { at, end, facts: factsOf(turns, end, end === "asking" ? openAskOf(entries) : undefined) };
25864
26731
  if (this.stoppedByHand.delete(worktreeId) && end === "stopped")
25865
26732
  wt.seenAt = at;
25866
- this.disarm(worktreeId);
25867
- if ((wt.seenAt ?? 0) < at)
25868
- this.arm(worktreeId, at);
25869
26733
  this.d.state.save();
25870
26734
  this.d.hub.emit("turnSettled", worktreeId, wt.lastTurn);
25871
26735
  }
25872
- arm(worktreeId, at) {
25873
- const t = setTimeout(() => this.due(worktreeId, at), this.d.delayMs ?? RECAP_DELAY_MS);
25874
- t.unref?.();
25875
- this.timers.set(worktreeId, t);
25876
- }
25877
- disarm(worktreeId) {
25878
- const t = this.timers.get(worktreeId);
25879
- if (!t)
25880
- return;
25881
- clearTimeout(t);
25882
- this.timers.delete(worktreeId);
25883
- }
25884
- due(worktreeId, at) {
25885
- this.timers.delete(worktreeId);
25886
- const wt = this.d.state.worktree(worktreeId);
25887
- const turn = wt?.lastTurn;
25888
- if (!wt || !turn || turn.at !== at || (wt.seenAt ?? 0) >= at)
25889
- return;
25890
- turn.recap = { at: Date.now() };
25891
- this.d.state.save();
25892
- this.d.hub.emit("worktreesChanged");
25893
- const seenAt = wt.seenAt ?? 0;
25894
- const summarize = this.d.summarize;
25895
- if (!summarize || turn.facts.auth)
25896
- return;
25897
- if (this.summarized.get(worktreeId) === seenAt)
25898
- return;
25899
- this.summarized.set(worktreeId, seenAt);
25900
- const entries = this.d.transcript(worktreeId);
25901
- const prompt = recapPrompt({
25902
- title: wt.title,
25903
- firstAsk: firstAskOf(entries),
25904
- turns: turnsSince(entries, seenAt),
25905
- end: turn.end,
25906
- facts: turn.facts
25907
- });
25908
- fireAndForget(worktreeId, this.write(worktreeId, at, summarize(wt, prompt)), "recap summary");
25909
- }
25910
- async write(worktreeId, at, sentence) {
25911
- const text = await sentence;
25912
- const turn = this.d.state.worktree(worktreeId)?.lastTurn;
25913
- if (!text || turn?.at !== at || !turn.recap)
25914
- return;
25915
- turn.recap.text = text;
25916
- this.d.state.save();
25917
- this.d.hub.emit("worktreesChanged");
25918
- }
25919
26736
  stoppedByPerson(worktreeId) {
25920
26737
  this.stoppedByHand.add(worktreeId);
25921
26738
  }
25922
26739
  markSeen(worktreeId) {
25923
- this.disarm(worktreeId);
25924
26740
  const wt = this.d.state.worktree(worktreeId);
25925
26741
  if (!wt)
25926
26742
  return;
@@ -25953,6 +26769,7 @@ function stopOf(prev, next) {
25953
26769
 
25954
26770
  // ../daemon/src/worktrees/service.ts
25955
26771
  var LOCAL_CONFIG_FILES = [".env", ".env.local", ".env.development", ".env.development.local", ".dev.vars"];
26772
+ var BASE_IGNORE = "base .gitignore";
25956
26773
  function withoutAttachments(event) {
25957
26774
  if (event.type !== "user-message")
25958
26775
  return event;
@@ -25966,20 +26783,27 @@ function withCarry(context, c) {
25966
26783
  const line = c.moved ? `This worktree started with ${files} the person moved here from ${c.branch} by hand. They are part of the task, not something to clean up.` : `${c.branch} has ${files} the person chose to leave there. This worktree does not have them.`;
25967
26784
  return [...context ?? [], line];
25968
26785
  }
25969
- var FETCH_EVERY_MS = 5 * 60000;
25970
26786
 
25971
26787
  class WorktreeService {
25972
26788
  d;
25973
26789
  spare;
26790
+ trunk;
25974
26791
  archive;
25975
26792
  archiving = new Map;
26793
+ going = new Set;
25976
26794
  countsCache = new Map;
26795
+ handChecked = new Map;
25977
26796
  discoverCache = new Map;
25978
26797
  usage = new Map;
25979
- lastFetch = new Map;
25980
26798
  constructor(d) {
25981
26799
  this.d = d;
25982
26800
  this.archive = new WorktreeArchive(d.paths.archiveDir);
26801
+ this.trunk = new Trunk({
26802
+ state: d.state,
26803
+ hub: d.hub,
26804
+ counts: (main, defaultBranch, quick) => quick ? Promise.resolve(this.countsQuick(main.id, quick)) : this.counts(main.id, main.path, defaultBranch, false),
26805
+ invalidateCounts: () => this.invalidateCounts()
26806
+ });
25983
26807
  this.spare = new SparePool({
25984
26808
  state: d.state,
25985
26809
  hub: d.hub,
@@ -25988,10 +26812,28 @@ class WorktreeService {
25988
26812
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
25989
26813
  discard: async (id) => {
25990
26814
  await this.discardWorktree(id);
26815
+ },
26816
+ carryDraft: (fromId, repoId) => {
26817
+ const text = this.d.drafts?.text(fromId);
26818
+ const main = this.mainOf(repoId);
26819
+ if (text && main)
26820
+ this.d.drafts?.set(main.id, text);
26821
+ },
26822
+ ready: (repoId) => {
26823
+ const main = this.mainOf(repoId);
26824
+ if (!main)
26825
+ return;
26826
+ if (this.d.runtime.get(main.id)?.procs) {
26827
+ fireAndForget(main.id, this.d.runtime.stopProcs(main.id), "main stops for the spare");
26828
+ }
26829
+ const text = this.d.drafts?.text(main.id);
26830
+ const spare = this.leadSpareOf(repoId);
26831
+ if (text && spare && !this.d.drafts?.text(spare)) {
26832
+ this.d.drafts?.set(spare, text);
26833
+ this.d.drafts?.drop(main.id);
26834
+ }
25991
26835
  }
25992
26836
  });
25993
- for (const wt of d.state.worktrees)
25994
- this.refreshLink(wt);
25995
26837
  d.hub.on("agent", (worktreeId, _seq, event) => {
25996
26838
  if (event.type !== "usage")
25997
26839
  return;
@@ -26016,18 +26858,15 @@ class WorktreeService {
26016
26858
  const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
26017
26859
  const profile = this.checkProfile(repo, opts.profile);
26018
26860
  const task = taskText(prompt, attachments);
26019
- let slug = variant ? `${slugify2(task, false)}-v${variant.index}` : slugify2(task);
26020
- if (variant && (await git(repo.path, "show-ref", "--verify", `refs/heads/toyon/${slug}`)).ok) {
26021
- slug = `${slug}-${shortId().slice(0, 3)}`;
26022
- }
26023
- const branch = `toyon/${slug}`;
26861
+ const base = titleFrom(task);
26862
+ const title = variant ? `${base} v${variant.index}` : base;
26024
26863
  if (opts.carry && variant && variant.of > 1) {
26025
26864
  throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
26026
26865
  }
26027
26866
  const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
26028
26867
 
26029
26868
  `) : prompt;
26030
- const claimed = await this.spare.claim(repoId, branch, slug);
26869
+ const claimed = await this.spare.claim(repoId, title, opts.worktreeId);
26031
26870
  if (claimed) {
26032
26871
  if (variant)
26033
26872
  claimed.variant = variant;
@@ -26041,7 +26880,6 @@ class WorktreeService {
26041
26880
  claimed.model = opts.model;
26042
26881
  if (opts.effort)
26043
26882
  claimed.effort = opts.effort;
26044
- this.refreshLink(claimed);
26045
26883
  if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
26046
26884
  claimed.profile = profile;
26047
26885
  this.restartProcs(claimed, repo);
@@ -26049,22 +26887,31 @@ class WorktreeService {
26049
26887
  const carried = await this.carryMain(repo, claimed, !!opts.carry);
26050
26888
  this.d.state.save();
26051
26889
  this.d.hub.emit("worktreesChanged");
26052
- this.d.runtime.ensureAgent(claimed).agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
26890
+ const rt = this.d.runtime.ensureAgent(claimed);
26891
+ if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
26892
+ await rt.agent.restart();
26893
+ rt.agent.send(agentPrompt, { context: withCarry(context, carried), attachments });
26053
26894
  this.scheduleNaming(claimed, task, variant);
26054
26895
  if (carried.unmoved)
26055
26896
  throw new UserError(carried.unmoved);
26056
26897
  return claimed;
26057
26898
  }
26058
- const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
26899
+ if (!await isGitRepo(repo.path)) {
26900
+ throw new UserError(`git no longer reads ${repo.name} as a checkout, so there is nothing to start a worktree from. If \`git config core.bare\` says true there, \`git config --unset core.bare\` puts it back.`);
26901
+ }
26902
+ const { id, dir } = freeSlot(join23(this.d.paths.worktreesDir, repo.name));
26903
+ const wtPath = join23(this.d.paths.worktreesDir, repo.name, dir);
26904
+ const branch = `toyon/${dir}`;
26059
26905
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
26060
26906
  const wt = {
26061
- id: shortId(),
26907
+ id,
26062
26908
  repoId,
26063
26909
  path: wtPath,
26064
26910
  branch,
26065
26911
  kind: "worktree",
26066
26912
  proxyPort: await allocateProxyPort(),
26067
- title: slug,
26913
+ title,
26914
+ unnamed: true,
26068
26915
  createdAt: Date.now(),
26069
26916
  promptedAt: Date.now(),
26070
26917
  agent,
@@ -26084,9 +26931,20 @@ class WorktreeService {
26084
26931
  return wt;
26085
26932
  }
26086
26933
  async carryMain(repo, wt, move) {
26934
+ try {
26935
+ return await this.carryMainOrThrow(repo, wt, move);
26936
+ } catch (e) {
26937
+ log.warn(wt.id, `reading ${repo.defaultBranch}'s changes failed`, e);
26938
+ const unmoved = move ? `the changes on ${repo.defaultBranch} could not be read, so they stayed there` : undefined;
26939
+ return { branch: repo.defaultBranch, moved: false, count: 0, ...unmoved ? { unmoved } : {} };
26940
+ }
26941
+ }
26942
+ async carryMainOrThrow(repo, wt, move) {
26087
26943
  const main = repo.defaultBranch;
26088
- if (!move)
26089
- return { branch: main, moved: false, count: (await statusFiles(repo.path)).length };
26944
+ if (!move) {
26945
+ const count = this.countsCache.get(this.mainOf(repo.id)?.id ?? "")?.dirty ?? 0;
26946
+ return { branch: main, moved: false, count };
26947
+ }
26090
26948
  let count = 0;
26091
26949
  const unmoved = await withRepoLock(repo.path, async () => {
26092
26950
  count = (await statusFiles(repo.path)).length;
@@ -26106,6 +26964,9 @@ class WorktreeService {
26106
26964
  `).indexOf(sha);
26107
26965
  return i < 0 ? null : `stash@{${i}}`;
26108
26966
  };
26967
+ const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
26968
+ const restored = (carried.ok ? carried.out.split(`
26969
+ `).filter(Boolean) : []).filter((p) => !existsSync26(join23(wt.path, p)));
26109
26970
  const applied = await git(wt.path, "stash", "apply", sha);
26110
26971
  if (applied.ok) {
26111
26972
  const ref = await entry();
@@ -26115,12 +26976,14 @@ class WorktreeService {
26115
26976
  }
26116
26977
  log.warn(wt.id, `moving ${main}'s changes failed`, applied.err);
26117
26978
  await git(wt.path, "reset", "--hard");
26118
- await git(wt.path, "clean", "-fd");
26979
+ for (let i = 0;i < restored.length; i += 100) {
26980
+ await git(wt.path, "--literal-pathspecs", "clean", "-fd", "--", ...restored.slice(i, i + 100));
26981
+ }
26119
26982
  const ref = await entry();
26120
26983
  const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
26121
26984
  return back?.ok ? `the changes on ${main} did not apply in ${wt.title}, so they stayed on ${main}` : `the changes on ${main} did not apply in ${wt.title}; they are kept in git stash as "toyon: into ${wt.title}"`;
26122
26985
  });
26123
- const mainWt = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
26986
+ const mainWt = this.mainOf(repo.id);
26124
26987
  if (mainWt)
26125
26988
  this.countsCache.delete(mainWt.id);
26126
26989
  return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
@@ -26138,10 +27001,10 @@ class WorktreeService {
26138
27001
  throw new UserError("that is not a PR number");
26139
27002
  const branch = kind === "pr" ? `pr/${number}` : ref;
26140
27003
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
26141
- let slug = title;
26142
- if (existsSync23(join21(this.d.paths.worktreesDir, repo.name, slug)))
27004
+ let slug = branchSlug(title) || "branch";
27005
+ if (existsSync26(join23(this.d.paths.worktreesDir, repo.name, slug)))
26143
27006
  slug = `${slug}-${shortId().slice(0, 4)}`;
26144
- const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
27007
+ const wtPath = join23(this.d.paths.worktreesDir, repo.name, slug);
26145
27008
  await withRepoLock(repo.path, async () => {
26146
27009
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
26147
27010
  if (out)
@@ -26289,7 +27152,7 @@ class WorktreeService {
26289
27152
  if (!name)
26290
27153
  return;
26291
27154
  for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
26292
- await this.rename(sibling.id, `${name}-v${sibling.variant.index}`).catch((e) => {
27155
+ await this.rename(sibling.id, `${name} v${sibling.variant.index}`).catch((e) => {
26293
27156
  log.warn(sibling.id, "variant rename failed", e);
26294
27157
  });
26295
27158
  }
@@ -26317,6 +27180,14 @@ class WorktreeService {
26317
27180
  this.d.drafts?.drop(worktreeId);
26318
27181
  }
26319
27182
  async takeDown(wt, archive, reason) {
27183
+ this.going.add(wt.id);
27184
+ try {
27185
+ return await this.removeAndRecord(wt, archive, reason);
27186
+ } finally {
27187
+ this.going.delete(wt.id);
27188
+ }
27189
+ }
27190
+ async removeAndRecord(wt, archive, reason) {
26320
27191
  const worktreeId = wt.id;
26321
27192
  const repo = this.d.state.requireRepo(wt.repoId);
26322
27193
  await this.d.runtime.stop(worktreeId);
@@ -26324,7 +27195,14 @@ class WorktreeService {
26324
27195
  const k = archive ? await this.keep(repo, wt) : null;
26325
27196
  if (archive && !k)
26326
27197
  throw new UserError(`could not keep ${wt.title}'s work, so it was left in place`);
26327
- await gitOrThrow(repo.path, "worktree", "remove", "--force", wt.path);
27198
+ if (archive)
27199
+ this.keepPlans(wt);
27200
+ try {
27201
+ await gitOrThrow(repo.path, "worktree", "remove", "--force", wt.path);
27202
+ } catch (e) {
27203
+ rmSync5(this.plansScratch(wt.id), { recursive: true, force: true });
27204
+ throw e;
27205
+ }
26328
27206
  if (hasOwnBranch(wt)) {
26329
27207
  const r = await git(repo.path, "branch", "-D", wt.branch);
26330
27208
  if (!r.ok)
@@ -26332,7 +27210,6 @@ class WorktreeService {
26332
27210
  }
26333
27211
  return k;
26334
27212
  });
26335
- this.dropLink(wt);
26336
27213
  const sessionId = this.d.state.session(worktreeId);
26337
27214
  this.d.state.removeWorktree(worktreeId);
26338
27215
  releasePort(wt.proxyPort);
@@ -26348,7 +27225,7 @@ class WorktreeService {
26348
27225
  return archived;
26349
27226
  }
26350
27227
  async keep(repo, wt) {
26351
- const index = join21(this.d.paths.archiveDir, `${wt.id}.index`);
27228
+ const index = join23(this.d.paths.archiveDir, `${wt.id}.index`);
26352
27229
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index);
26353
27230
  if (!kept)
26354
27231
  log.warn(wt.id, "could not keep its git state");
@@ -26356,8 +27233,21 @@ class WorktreeService {
26356
27233
  log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
26357
27234
  return kept;
26358
27235
  }
27236
+ plansScratch(worktreeId) {
27237
+ return join23(this.d.paths.archiveDir, `${worktreeId}.plans`);
27238
+ }
27239
+ keepPlans(wt) {
27240
+ const from = join23(wt.path, PLANS_DIR);
27241
+ if (!existsSync26(from))
27242
+ return;
27243
+ try {
27244
+ cpSync(from, this.plansScratch(wt.id), { recursive: true });
27245
+ } catch (e) {
27246
+ log.warn(wt.id, "could not keep its plans", e);
27247
+ }
27248
+ }
26359
27249
  archiveChat(wt, repo, kept, sessionId, reason) {
26360
- const files = this.chatFiles(wt.id);
27250
+ const files = { ...this.chatFiles(wt.id), plans: this.plansScratch(wt.id) };
26361
27251
  const prompt = firstPrompt(files.transcript);
26362
27252
  const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
26363
27253
  const rec = {
@@ -26370,14 +27260,15 @@ class WorktreeService {
26370
27260
  ...kept ? { kept } : {},
26371
27261
  ...reason ? { auto: reason } : {}
26372
27262
  };
27263
+ let at;
26373
27264
  try {
26374
- this.archive.put(rec, files);
27265
+ at = this.archive.put(rec, files);
26375
27266
  } catch (e) {
26376
27267
  log.warn(wt.id, "could not archive its chat; the files stay where they were", e);
26377
27268
  return null;
26378
27269
  }
26379
27270
  this.d.hub.emit("archiveChanged", repo.id);
26380
- return summarize(rec, repo.id);
27271
+ return summarize(rec, repo.id, at.transcript);
26381
27272
  }
26382
27273
  chatFiles(worktreeId) {
26383
27274
  return {
@@ -26397,7 +27288,6 @@ class WorktreeService {
26397
27288
  async forgetGone(wt) {
26398
27289
  const repo = this.d.state.repo(wt.repoId);
26399
27290
  const sessionId = this.d.state.session(wt.id);
26400
- this.dropLink(wt);
26401
27291
  this.d.state.removeWorktree(wt.id);
26402
27292
  if (!repo || wt.kind !== "worktree") {
26403
27293
  this.deleteChat(wt.id);
@@ -26436,7 +27326,7 @@ class WorktreeService {
26436
27326
  }
26437
27327
  archivedAttachment(archiveId, file) {
26438
27328
  const files = this.archive.chatFiles(archiveId);
26439
- return files && isAttachmentFile(file) ? join21(files.attachments, file) : null;
27329
+ return files && isAttachmentFile(file) ? join23(files.attachments, file) : null;
26440
27330
  }
26441
27331
  archivedGit(archiveId) {
26442
27332
  const rec = this.archive.get(archiveId);
@@ -26457,6 +27347,9 @@ class WorktreeService {
26457
27347
  }
26458
27348
  }
26459
27349
  async archivedFile(archiveId, path, ref) {
27350
+ const plan = ref ? null : this.archive.planFile(archiveId, path);
27351
+ if (plan !== null)
27352
+ return { before: plan, after: plan };
26460
27353
  const kept = this.archivedGit(archiveId);
26461
27354
  return kept ? kept.file(path, ref) : null;
26462
27355
  }
@@ -26476,7 +27369,7 @@ class WorktreeService {
26476
27369
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
26477
27370
  if (rec.auto)
26478
27371
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
26479
- const path = existsSync23(old.path) ? join21(dirname8(old.path), `${basename6(old.path)}-${shortId().slice(0, 4)}`) : old.path;
27372
+ const path = existsSync26(old.path) ? join23(dirname9(old.path), freeSlot(dirname9(old.path)).dir) : old.path;
26480
27373
  const branch = await withRepoLock(repo.path, async () => {
26481
27374
  if (!await commitOf(repo.path, archiveRef(old.id))) {
26482
27375
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -26502,7 +27395,7 @@ class WorktreeService {
26502
27395
  }
26503
27396
  return name;
26504
27397
  });
26505
- const { variant: _variant, linkPath: _linkPath, ...rest } = old;
27398
+ const { variant: _variant, ...rest } = old;
26506
27399
  const wt = {
26507
27400
  ...rest,
26508
27401
  repoId: repo.id,
@@ -26513,7 +27406,7 @@ class WorktreeService {
26513
27406
  ...createdBy ? { createdBy } : {}
26514
27407
  };
26515
27408
  try {
26516
- this.archive.take(old.id, this.chatFiles(old.id));
27409
+ this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join23(path, PLANS_DIR) });
26517
27410
  } catch (e) {
26518
27411
  log.warn(old.id, "could not move its chat back out of the archive", e);
26519
27412
  }
@@ -26523,7 +27416,6 @@ class WorktreeService {
26523
27416
  if (!dropped.ok)
26524
27417
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
26525
27418
  this.countsCache.delete(wt.id);
26526
- this.refreshLink(wt);
26527
27419
  this.launch(wt, repo, repo.path);
26528
27420
  if (message)
26529
27421
  this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
@@ -26536,12 +27428,12 @@ class WorktreeService {
26536
27428
  return;
26537
27429
  const repo = this.repoOf(rec);
26538
27430
  const repoPath = repo?.path ?? rec.repoPath;
26539
- if (rec.kept && existsSync23(repoPath)) {
27431
+ if (rec.kept && existsSync26(repoPath)) {
26540
27432
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
26541
27433
  if (!r.ok)
26542
27434
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
26543
27435
  }
26544
- if (rec.worktree.lands?.length && existsSync23(repoPath))
27436
+ if (rec.worktree.lands?.length && existsSync26(repoPath))
26545
27437
  await dropLandRefs(repoPath, archiveId);
26546
27438
  this.archive.delete(archiveId);
26547
27439
  this.d.drafts?.drop(archiveId);
@@ -26558,55 +27450,24 @@ class WorktreeService {
26558
27450
  const clean = cleanTitle2(title);
26559
27451
  if (!clean)
26560
27452
  return;
27453
+ const slug = branchSlug(clean);
26561
27454
  await withRepoLock(repo.path, async () => {
26562
- let branch = `toyon/${clean}`;
26563
- if (branch !== wt.branch) {
27455
+ let branch = `toyon/${slug}`;
27456
+ if (slug && branch !== wt.branch) {
26564
27457
  let n = 2;
26565
27458
  while (!(await git(wt.path, "branch", "-m", wt.branch, branch)).ok) {
26566
27459
  if (n > 5)
26567
27460
  return;
26568
- branch = `toyon/${clean}-${n++}`;
27461
+ branch = `toyon/${slug}-${n++}`;
26569
27462
  }
26570
27463
  wt.branch = branch;
26571
27464
  }
26572
27465
  wt.title = clean;
27466
+ delete wt.unnamed;
26573
27467
  });
26574
- this.refreshLink(wt);
26575
27468
  this.d.state.save();
26576
27469
  this.d.hub.emit("worktreesChanged");
26577
27470
  }
26578
- refreshLink(wt) {
26579
- if (!hasOwnBranch(wt))
26580
- return;
26581
- const name = wt.branch.replace(/^toyon\//, "");
26582
- const desired = wt.kind === "worktree" ? join21(dirname8(wt.path), name) : wt.path;
26583
- if (wt.linkPath && wt.linkPath !== desired)
26584
- this.dropLink(wt);
26585
- if (desired === wt.path)
26586
- return;
26587
- try {
26588
- const st = lstatSync(desired, { throwIfNoEntry: false });
26589
- if (st && !(st.isSymbolicLink() && readlinkSync(desired) === wt.path))
26590
- return;
26591
- if (!st)
26592
- symlinkSync(wt.path, desired);
26593
- wt.linkPath = desired;
26594
- } catch (e) {
26595
- log.warn(wt.id, "could not link the title to the directory", e);
26596
- }
26597
- }
26598
- dropLink(wt) {
26599
- const p = wt.linkPath;
26600
- wt.linkPath = undefined;
26601
- if (!p)
26602
- return;
26603
- try {
26604
- if (lstatSync(p, { throwIfNoEntry: false })?.isSymbolicLink())
26605
- unlinkSync(p);
26606
- } catch (e) {
26607
- log.warn(wt.id, "could not remove the title link", e);
26608
- }
26609
- }
26610
27471
  async pickVariant(worktreeId) {
26611
27472
  const wt = this.d.state.worktree(worktreeId);
26612
27473
  if (!wt?.variant)
@@ -26671,10 +27532,19 @@ class WorktreeService {
26671
27532
  this.d.runtime.markSetup(wt.id, false);
26672
27533
  }
26673
27534
  }
27535
+ async mirrorBaseIgnore(repo) {
27536
+ const src = join23(repo.path, ".gitignore");
27537
+ const tracked = existsSync26(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
27538
+ const mirror = existsSync26(src) && !tracked;
27539
+ const lines = mirror ? readFileSync16(src, "utf8").split(`
27540
+ `).filter((l) => l.trim() !== "") : [];
27541
+ await excludeBlock(repo.path, BASE_IGNORE, lines);
27542
+ }
26674
27543
  async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
26675
- const srcNm = join21(depsSource, "node_modules");
26676
- const dstNm = join21(wt.path, "node_modules");
26677
- if (existsSync23(srcNm) && !existsSync23(dstNm)) {
27544
+ await this.mirrorBaseIgnore(repo);
27545
+ const srcNm = join23(depsSource, "node_modules");
27546
+ const dstNm = join23(wt.path, "node_modules");
27547
+ if (existsSync26(srcNm) && !existsSync26(dstNm)) {
26678
27548
  const started = Date.now();
26679
27549
  const attempts = [
26680
27550
  ["clonefile", ["-Rc", srcNm, dstNm]],
@@ -26690,10 +27560,10 @@ class WorktreeService {
26690
27560
  }
26691
27561
  }
26692
27562
  for (const f of LOCAL_CONFIG_FILES) {
26693
- const src = join21(depsSource, f);
26694
- if (!existsSync23(src) || existsSync23(join21(wt.path, f)))
27563
+ const src = join23(depsSource, f);
27564
+ if (!existsSync26(src) || existsSync26(join23(wt.path, f)))
26695
27565
  continue;
26696
- const r = await run("cp", [src, join21(wt.path, f)], wt.path);
27566
+ const r = await run("cp", [src, join23(wt.path, f)], wt.path);
26697
27567
  if (r.ok)
26698
27568
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
26699
27569
  else
@@ -26758,10 +27628,42 @@ ${wt.landing.body}` : s;
26758
27628
  async commitIfDirty(wt, typed) {
26759
27629
  if ((await statusFiles(wt.path)).length === 0)
26760
27630
  return null;
26761
- const result = await commitWorktree(wt.path, this.commitMessage(wt, typed));
26762
- if (result.ok)
27631
+ return this.commitRecorded(wt, this.commitMessage(wt, typed));
27632
+ }
27633
+ async commitRecorded(wt, message) {
27634
+ const result = await commitWorktree(wt.path, message);
27635
+ if (result.ok) {
27636
+ await this.verdictSurvives(wt, { dropMessage: true });
26763
27637
  this.headMoved(wt.id);
26764
- return result;
27638
+ return result;
27639
+ }
27640
+ if (!result.output?.trim() || !this.d.record)
27641
+ return result;
27642
+ const subject = message.split(`
27643
+ `, 1)[0] ?? "";
27644
+ try {
27645
+ this.d.record(wt.id, `git commit -m ${JSON.stringify(subject)}`, result.output, result.exit ?? null);
27646
+ } catch (e) {
27647
+ log.warn(wt.id, `could not put the refused commit on the transcript: ${e instanceof Error ? e.message : e}`);
27648
+ return result;
27649
+ }
27650
+ return { ...result, message: "commit refused: what git and its hooks printed is on the chat" };
27651
+ }
27652
+ async verdictSurvives(wt, opts) {
27653
+ const l = wt.landing;
27654
+ if (!l || l.check === "pending")
27655
+ return;
27656
+ const { subject, body, ...kept } = l;
27657
+ const message = opts.dropMessage ? {} : { ...subject ? { subject } : {}, ...body ? { body } : {} };
27658
+ this.setLanding(wt.id, { ...kept, ...message, fingerprint: await treeFingerprint(wt.path) });
27659
+ }
27660
+ async landedByHand(wt, mark) {
27661
+ const repo = this.d.state.requireRepo(wt.repoId);
27662
+ log.info(wt.id, `landed on ${repo.defaultBranch} outside toyon: ${mark.base.slice(0, 7)}..${mark.tip.slice(0, 7)}`);
27663
+ await this.noteLand(repo, wt, mark);
27664
+ await this.restartFromMain(wt, repo.defaultBranch);
27665
+ this.setLanding(wt.id, undefined);
27666
+ this.setLanded(wt.id, true);
26765
27667
  }
26766
27668
  async land(worktreeId, message) {
26767
27669
  const { wt, repo } = this.landable(worktreeId, "land");
@@ -26772,8 +27674,24 @@ ${wt.landing.body}` : s;
26772
27674
  ${wt.landing.body}` : wt.landing.subject : undefined;
26773
27675
  if (policy.land === "pr") {
26774
27676
  if (wt.pr?.state === "open") {
26775
- const result = await mergePr(wt.path, wt.pr.number, policy.merge);
26776
- return { result };
27677
+ const dirty = (await statusFiles(wt.path)).length > 0;
27678
+ const missing = dirty || (await aheadUpstream(wt.path) ?? 0) > 0;
27679
+ if (!missing)
27680
+ return { result: await mergePr(wt.path, wt.pr.number, policy.merge) };
27681
+ const committed = await this.commitIfDirty(wt, message);
27682
+ if (committed && !committed.ok)
27683
+ return { result: committed };
27684
+ const taken = await takeMainIn(wt.path, repo.defaultBranch, own);
27685
+ if (!taken.ok)
27686
+ return { result: taken };
27687
+ this.headMoved(wt.id);
27688
+ const pushed = await pushBranch(wt.path, wt.branch);
27689
+ if (!pushed.ok)
27690
+ return { result: pushed };
27691
+ this.headMoved(wt.id);
27692
+ this.setLanding(wt.id, undefined);
27693
+ const did = committed ? "committed and pushed" : "pushed";
27694
+ return { result: { ok: true, url: wt.pr.url, message: `${did}; PR #${wt.pr.number} has the new commits` } };
26777
27695
  }
26778
27696
  const committed = await this.commitIfDirty(wt, message);
26779
27697
  if (committed && !committed.ok)
@@ -26799,10 +27717,12 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26799
27717
  return { result };
26800
27718
  }
26801
27719
  let mergedHere = false;
27720
+ let committedHere = false;
26802
27721
  const result = await withRepoLock(repo.path, async () => {
26803
27722
  const committed = await this.commitIfDirty(wt, message);
26804
27723
  if (committed && !committed.ok)
26805
27724
  return committed;
27725
+ committedHere = committed !== null;
26806
27726
  if (policy.land === "push") {
26807
27727
  const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
26808
27728
  if (!pulled.ok && !/no upstream/.test(pulled.message))
@@ -26835,8 +27755,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26835
27755
  if (!result.ok)
26836
27756
  return { result };
26837
27757
  const archiveIds = siblingsOf(wt, this.d.state.worktrees).map((w) => w.id);
26838
- const where = policy.land === "push" ? `${repo.defaultBranch}, pushed` : repo.defaultBranch;
26839
- return { result: { ...result, message: `${wt.title} is on ${where}` }, archiveIds };
27758
+ const did = committedHere ? "committed and merged" : "merged";
27759
+ const then = policy.land === "push" ? " and pushed" : "";
27760
+ return { result: { ...result, message: `${did} into ${repo.defaultBranch}${then}` }, archiveIds };
26840
27761
  }
26841
27762
  async noteLand(repo, wt, mark) {
26842
27763
  if (!mark)
@@ -26882,6 +27803,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26882
27803
  throw new UserError("that worktree is gone");
26883
27804
  if (r.wt && isMain(r.wt))
26884
27805
  throw new UserError("sync from a worktree, not main");
27806
+ if (r.wt && isProvisional(r.wt))
27807
+ throw new UserError("the new worktree already sits on main; it follows it on its own");
26885
27808
  if (!r.branch)
26886
27809
  throw new UserError(`${r.name} is detached: check out a branch in it first`);
26887
27810
  if (r.locked)
@@ -26889,8 +27812,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26889
27812
  const repo = this.d.state.requireRepo(r.repoId);
26890
27813
  const own = r.wt ? hasOwnBranch(r.wt) : false;
26891
27814
  const result = await withRepoLock(repo.path, () => takeMainIn(r.path, repo.defaultBranch, own));
26892
- if (result.ok)
27815
+ if (result.ok) {
26893
27816
  this.headMoved(worktreeId);
27817
+ if (r.wt)
27818
+ await this.verdictSurvives(r.wt, { dropMessage: false });
27819
+ }
26894
27820
  return { result, defaultBranch: repo.defaultBranch };
26895
27821
  }
26896
27822
  async pull(worktreeId) {
@@ -26910,10 +27836,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26910
27836
  const m = message.trim();
26911
27837
  if (!m)
26912
27838
  throw new UserError("commit message required");
26913
- const result = await commitWorktree(wt.path, m);
26914
- if (result.ok)
26915
- this.headMoved(wt.id);
26916
- return result;
27839
+ return this.commitRecorded(wt, m);
26917
27840
  }
26918
27841
  setEmpty(wt, empty) {
26919
27842
  if (wt.empty === empty)
@@ -26922,23 +27845,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26922
27845
  this.d.state.save();
26923
27846
  this.d.hub.emit("worktreesChanged");
26924
27847
  }
26925
- async mainCounts(path) {
26926
- const behind = await behindUpstream(path);
26927
- if (behind === null)
26928
- return {};
26929
- const last = this.lastFetch.get(path) ?? 0;
26930
- if (Date.now() - last > FETCH_EVERY_MS) {
26931
- this.lastFetch.set(path, Date.now());
26932
- fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then((r) => {
26933
- if (!r.ok) {
26934
- log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
26935
- return;
26936
- }
26937
- this.invalidateCounts();
26938
- this.d.hub.emit("worktreesChanged");
26939
- }));
26940
- }
26941
- return { behind };
27848
+ syncTrunk(repoId) {
27849
+ return this.trunk.sync(repoId);
27850
+ }
27851
+ trunks(opts = {}) {
27852
+ return this.trunk.all(opts);
26942
27853
  }
26943
27854
  usageFor(worktreeId) {
26944
27855
  const known = this.usage.get(worktreeId);
@@ -27004,15 +27915,26 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27004
27915
  const cached = this.countsCache.get(id);
27005
27916
  if (cached && Date.now() - cached.at < 1e4)
27006
27917
  return cached;
27918
+ if (this.going.has(id))
27919
+ return cached ?? {};
27007
27920
  try {
27008
- const ab = countable ? await aheadBehind(path, defaultBranch) : await this.mainCounts(path);
27009
- const fresh = { ...ab, dirty: (await statusFiles(path)).length, at: Date.now() };
27921
+ const ab = countable ? await aheadBehind(path, defaultBranch) : await this.trunk.behind(id, path);
27922
+ const dirty = (await statusFiles(path)).length;
27923
+ const unpushed = await this.unpushed(id, path);
27924
+ if (this.going.has(id))
27925
+ return cached ?? {};
27926
+ const fresh = { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty, at: Date.now() };
27010
27927
  this.countsCache.set(id, fresh);
27011
27928
  return fresh;
27012
27929
  } catch {
27013
27930
  return cached ?? {};
27014
27931
  }
27015
27932
  }
27933
+ async unpushed(id, path) {
27934
+ if (this.d.state.worktree(id)?.pr?.state !== "open")
27935
+ return;
27936
+ return await aheadUpstream(path) ?? undefined;
27937
+ }
27016
27938
  async freshCounts(worktreeId) {
27017
27939
  const wt = this.d.state.worktree(worktreeId);
27018
27940
  const repo = wt && this.d.state.repo(wt.repoId);
@@ -27024,8 +27946,6 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27024
27946
  readable(id) {
27025
27947
  const wt = this.d.state.worktree(id);
27026
27948
  if (wt) {
27027
- if (wt.kind === "spare")
27028
- return null;
27029
27949
  const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
27030
27950
  return { id, repoId: wt.repoId, path: wt.path, name: wt.title, branch: wt.branch, defaultBranch, wt };
27031
27951
  }
@@ -27044,16 +27964,20 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27044
27964
  };
27045
27965
  }
27046
27966
  async gitStatus(worktreeId) {
27967
+ if (this.going.has(worktreeId))
27968
+ return null;
27047
27969
  const r = this.readable(worktreeId);
27048
27970
  if (!r)
27049
27971
  return this.archivedStatus(worktreeId);
27050
27972
  try {
27051
27973
  const isMain = r.wt?.kind === "main";
27052
- const [files, counts, head] = await Promise.all([
27974
+ const [files, ab, unpushed, head] = await Promise.all([
27053
27975
  statusFilesWithCounts(r.path),
27054
27976
  isMain ? Promise.resolve({}) : aheadBehind(r.path, r.defaultBranch),
27977
+ this.unpushed(worktreeId, r.path),
27055
27978
  git(r.path, "rev-parse", "HEAD")
27056
27979
  ]);
27980
+ const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
27057
27981
  const ahead = counts.ahead ?? 0;
27058
27982
  const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.defaultBranch) : undefined;
27059
27983
  if (r.wt?.landed && (files.length > 0 || ahead > 0)) {
@@ -27061,10 +27985,25 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27061
27985
  if (r.wt.pr)
27062
27986
  this.setPr(r.wt.id, undefined);
27063
27987
  }
27064
- if (r.wt?.landing && r.wt.landing.check !== "pending" && r.wt.landing.fingerprint !== await treeFingerprint(r.path))
27065
- this.setLanding(r.wt.id, undefined);
27988
+ if (r.wt && !isMain && !r.wt.landed && hasOwnBranch(r.wt) && files.length === 0 && ahead === 0 && head.ok && this.handChecked.get(r.wt.id) !== head.out) {
27989
+ this.handChecked.set(r.wt.id, head.out);
27990
+ const mark = await handLanding(r.path, r.wt.branch, r.defaultBranch, r.wt.lands?.at(-1)?.tip);
27991
+ if (mark) {
27992
+ await this.landedByHand(r.wt, mark);
27993
+ return this.gitStatus(worktreeId);
27994
+ }
27995
+ }
27996
+ if (r.wt?.landing && r.wt.landing.check !== "pending") {
27997
+ const moved = r.wt.landing.fingerprint !== await treeFingerprint(r.path);
27998
+ if (moved !== !!r.wt.landing.stale) {
27999
+ const { stale: _was, ...rest } = r.wt.landing;
28000
+ this.setLanding(r.wt.id, moved ? { ...rest, stale: true } : rest);
28001
+ }
28002
+ }
27066
28003
  if (r.wt && isMain)
27067
28004
  this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
28005
+ if (this.going.has(worktreeId))
28006
+ return null;
27068
28007
  this.noteCounts(worktreeId, isMain, files.length, counts);
27069
28008
  return { files, committed, head: head.ok ? head.out : undefined, ...counts };
27070
28009
  } catch (e) {
@@ -27078,8 +28017,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27078
28017
  return;
27079
28018
  const ahead = main ? prev?.ahead : ab.ahead;
27080
28019
  const behind = main ? prev?.behind : ab.behind;
27081
- this.countsCache.set(id, { ahead, behind, dirty, at: Date.now() });
27082
- if (prev?.dirty !== dirty || prev?.ahead !== ahead || prev?.behind !== behind)
28020
+ const unpushed = main ? prev?.unpushed : ab.unpushed;
28021
+ this.countsCache.set(id, { ahead, behind, unpushed, dirty, at: Date.now() });
28022
+ if (prev?.dirty !== dirty || prev?.ahead !== ahead || prev?.behind !== behind || prev?.unpushed !== unpushed)
27083
28023
  this.d.hub.emit("worktreesChanged");
27084
28024
  }
27085
28025
  async gitLog(worktreeId) {
@@ -27106,7 +28046,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27106
28046
  await this.restore(worktreeId, msg.clientId, { text: msg.text, attachments: msg.attachments });
27107
28047
  return;
27108
28048
  }
27109
- this.requireRun(worktreeId);
28049
+ const wt = this.requireRun(worktreeId);
28050
+ if (wt.kind === "main")
28051
+ throw new UserError("nothing runs on main: the plus starts a worktree for it");
27110
28052
  const agent = this.d.runtime.agentFor(worktreeId);
27111
28053
  if (!agent)
27112
28054
  throw new UserError("worktree still starting; try again in a moment");
@@ -27128,14 +28070,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27128
28070
  setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
27129
28071
  return [...owned, ...found];
27130
28072
  }
27131
- spares() {
27132
- return this.d.state.worktrees.filter((wt) => wt.kind === "spare").map((wt) => ({
27133
- repoId: wt.repoId,
27134
- id: wt.id,
27135
- path: wt.path,
27136
- proxyPort: wt.proxyPort,
27137
- ready: this.d.runtime.get(wt.id)?.proxy != null && this.spare.current(wt.repoId)?.worktreeId === wt.id
27138
- }));
28073
+ mainOf(repoId) {
28074
+ return mainOf(this.d.state, repoId);
28075
+ }
28076
+ leadSpareOf(repoId) {
28077
+ return this.spare.current(repoId)?.worktreeId ?? null;
27139
28078
  }
27140
28079
  countsQuick(id, quick) {
27141
28080
  const c = this.countsCache.get(id);
@@ -27144,11 +28083,17 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27144
28083
  quick.missed = true;
27145
28084
  return {};
27146
28085
  }
28086
+ isRow(wt) {
28087
+ if (wt.kind === "worktree")
28088
+ return true;
28089
+ const lead = this.leadSpareOf(wt.repoId);
28090
+ return wt.kind === "spare" ? lead === wt.id : lead === null;
28091
+ }
27147
28092
  async ownedRows(quick) {
27148
- return Promise.all(this.d.state.worktrees.filter((wt) => wt.kind !== "spare").map(async (wt) => {
28093
+ return Promise.all(this.d.state.worktrees.filter((wt) => this.isRow(wt)).map(async (wt) => {
27149
28094
  const rt = this.d.runtime.get(wt.id);
27150
28095
  const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
27151
- const { ahead, behind, dirty } = quick ? this.countsQuick(wt.id, quick) : await this.counts(wt.id, wt.path, defaultBranch, !isMain(wt));
28096
+ const { ahead, behind, unpushed, dirty } = quick ? this.countsQuick(wt.id, quick) : await this.counts(wt.id, wt.path, defaultBranch, !isMain(wt));
27152
28097
  return {
27153
28098
  id: wt.id,
27154
28099
  repoId: wt.repoId,
@@ -27161,10 +28106,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27161
28106
  login: !!rt?.login,
27162
28107
  ahead,
27163
28108
  behind,
28109
+ unpushed,
27164
28110
  dirty,
27165
28111
  queued: rt?.agent.queueLength || undefined,
27166
28112
  unseen: isUnseen(wt) || undefined,
27167
- usage: this.usageFor(wt.id)
28113
+ usage: this.usageFor(wt.id),
28114
+ transcript: transcriptPathFor(this.d.paths.transcriptsDir, wt.id),
28115
+ sessionId: this.d.state.session(wt.id)
27168
28116
  };
27169
28117
  }));
27170
28118
  }
@@ -27269,7 +28217,7 @@ class ArchiveSweep {
27269
28217
  // ../daemon/src/index.ts
27270
28218
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
27271
28219
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
27272
- var here = dirname9(fileURLToPath(import.meta.url));
28220
+ var here = dirname10(fileURLToPath(import.meta.url));
27273
28221
  var {
27274
28222
  shellDist: SHELL_DIST,
27275
28223
  bridgeJs: BRIDGE_JS,
@@ -27323,29 +28271,39 @@ var runtime = new RuntimeRegistry({
27323
28271
  bridgeScript: () => bridge.get(),
27324
28272
  remote,
27325
28273
  grant: previewGrant(token),
27326
- viewed: (id) => idle.isViewed(id)
28274
+ viewed: (id) => idle.isViewed(id),
28275
+ shown: (id) => idle.isShown(id),
28276
+ mainLeads: (repoId) => worktrees.spare.current(repoId) === null
27327
28277
  });
27328
28278
  var drafts = new DraftStore({ file: paths.draftsFile, hub });
27329
- var worktrees = new WorktreeService({ state, hub, runtime, paths, agents, drafts });
28279
+ var exec = new ExecService({ state, runtime });
28280
+ var worktrees = new WorktreeService({
28281
+ state,
28282
+ hub,
28283
+ runtime,
28284
+ paths,
28285
+ agents,
28286
+ drafts,
28287
+ record: (id, command, text, exit) => exec.record(id, command, text, exit)
28288
+ });
27330
28289
  var turns = new TurnService({
27331
28290
  state,
27332
28291
  hub,
27333
28292
  transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
27334
- summarize: makeRecapper(runtime, agents, state),
27335
28293
  hold: (id, tag) => runtime.hold(id, tag),
27336
28294
  release: (id, tag) => runtime.release(id, tag)
27337
28295
  });
27338
28296
  var files = new FileService(state, runtime, (id) => worktrees.readable(id));
27339
28297
  var design = new DesignService((id) => worktrees.readable(id));
27340
28298
  var routes = new RouteService({ state, hub, readable: (id) => worktrees.readable(id) });
27341
- var exec = new ExecService({ state, runtime });
27342
- new LandingService({
28299
+ var landing = new LandingService({
27343
28300
  state,
27344
28301
  hub,
27345
28302
  worktrees,
27346
28303
  transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
27347
- check: (id, command) => exec.exec(id, command, CHECK_TOOL),
27348
- judge: makeLander(runtime, agents, state)
28304
+ check: (id, command, opts) => exec.exec(id, command, CHECK_TOOL, opts),
28305
+ judge: makeLander(runtime, agents, state),
28306
+ recap: makeAnswerRecapper(runtime, agents, state)
27349
28307
  });
27350
28308
  var refs = new RefSearch({ state });
27351
28309
  var chats = new ChatSearch({
@@ -27357,7 +28315,7 @@ var chats = new ChatSearch({
27357
28315
  var prs = new PrService({ state, hub, worktrees, view: viewPr });
27358
28316
  var self = new SelfWatch(SOURCE_ROOT);
27359
28317
  await self.start();
27360
- var afterLand = new AfterLand({ state, hub, self });
28318
+ var afterLand = new AfterLand({ state, hub, self, settled: () => idle.settled() });
27361
28319
  var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
27362
28320
  var idle = new IdlePolicy({
27363
28321
  runtime,
@@ -27426,6 +28384,7 @@ var { branded, stop: stopServer } = startServer({
27426
28384
  chats,
27427
28385
  drafts,
27428
28386
  prs,
28387
+ landing,
27429
28388
  themes,
27430
28389
  agents,
27431
28390
  accounts,
@@ -27449,14 +28408,12 @@ bridge.setShellOrigins([
27449
28408
  ],
27450
28409
  ...remote ? [`https://${remote.host}`] : []
27451
28410
  ]);
27452
- writeFileSync9(paths.pidFile, `${process.pid}
28411
+ writeFileSync10(paths.pidFile, `${process.pid}
27453
28412
  `);
27454
28413
  var stopLagSampler = startLagSampler();
28414
+ await runtime.reclaimOrphans();
27455
28415
  await repos.boot();
27456
- drafts.prune((id) => {
27457
- const repoId = draftRepoOf(id);
27458
- return repoId ? !!state.repo(repoId) : !!state.worktree(id) || worktrees.hasArchived(id);
27459
- });
28416
+ drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
27460
28417
  idle.boot();
27461
28418
  fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
27462
28419
  var repoArg = process.argv[2];
@@ -27497,6 +28454,7 @@ async function shutdown(signal, opts = {}) {
27497
28454
  drafts.flush();
27498
28455
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
27499
28456
  await Promise.race([runtime.shutdown(), deadline]);
28457
+ state.flushGroups();
27500
28458
  rmSync6(paths.pidFile, { force: true });
27501
28459
  if (opts.respawn) {
27502
28460
  await Bun.sleep(250);