toyon 0.4.1 → 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 +1 -1
  2. package/dist/bridge.js +1 -1
  3. package/dist/cli.js +1 -1
  4. package/dist/daemon.js +1619 -761
  5. package/dist/shell/assets/{Editor-BV5mlMUd.js → Editor-DR-GTFmf.js} +3 -3
  6. package/dist/shell/assets/{XTerm-vYHVHYnx.js → XTerm-B3kT_naP.js} +12 -12
  7. package/dist/shell/assets/{cssMode-DmPPq0RM.js → cssMode-CB4CczRa.js} +1 -1
  8. package/dist/shell/assets/{freemarker2-D0jw6qr7.js → freemarker2-Wjq3dVyB.js} +1 -1
  9. package/dist/shell/assets/{grammar-SjzA6dB3.js → grammar-yCr7BdNP.js} +1 -1
  10. package/dist/shell/assets/{handlebars-DP51I8rr.js → handlebars-Bjvi6pTu.js} +1 -1
  11. package/dist/shell/assets/{html-lErft7TP.js → html-iB2mKeN2.js} +1 -1
  12. package/dist/shell/assets/{htmlMode-B8WYzHvO.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-2o8dwNxB.js → javascript-BpuIkItm.js} +1 -1
  16. package/dist/shell/assets/{jsonMode-BzyCh8f1.js → jsonMode-QCLe5ypz.js} +1 -1
  17. package/dist/shell/assets/{liquid-DwwOicEv.js → liquid-CQI6mzhA.js} +1 -1
  18. package/dist/shell/assets/{lspLanguageFeatures-BSPiRlDO.js → lspLanguageFeatures-DqXzGsHQ.js} +1 -1
  19. package/dist/shell/assets/{mdx-Dx7gR42C.js → mdx-D2Unh-By.js} +1 -1
  20. package/dist/shell/assets/{python-B09pxq08.js → python-LLSqiJ4D.js} +1 -1
  21. package/dist/shell/assets/{razor-DO8UQZDP.js → razor-CJ2qsTB8.js} +1 -1
  22. package/dist/shell/assets/{tsMode-1xhdljsc.js → tsMode-CwjjHUH5.js} +1 -1
  23. package/dist/shell/assets/{xml-Be6HmYJA.js → xml-BzqCyEOW.js} +1 -1
  24. package/dist/shell/assets/{yaml-D4xpEWFo.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-2d0IGh4f.js +0 -232
  28. package/dist/shell/assets/index-DZ08PVmu.css +0 -1
package/dist/daemon.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  // ../daemon/src/index.ts
3
3
  import { rmSync as rmSync6, writeFileSync as writeFileSync10 } from "fs";
4
- import { dirname as dirname9 } from "path";
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 = 60;
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.1",
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 });
@@ -13220,6 +13266,7 @@ var SYSTEM_APPEND = [
13220
13266
  "Never run `git push`, delete branches, or create pull requests; shipping is handled by the Toyon UI.",
13221
13267
  "Never run `git commit` unless the user explicitly asks you to; leave changes uncommitted for the user to review and commit themselves.",
13222
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.",
13223
13270
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists.",
13224
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.`,
13225
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.",
@@ -13251,7 +13298,7 @@ function previewContext(p) {
13251
13298
  line = `The preview is running${at}, and the user sees it live beside this chat.`;
13252
13299
  break;
13253
13300
  case "asleep":
13254
- 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.`;
13255
13302
  break;
13256
13303
  default:
13257
13304
  line = `The preview is ${p.status}${p.detail ? `: ${p.detail}` : ""}; nothing answers${at} until it is back.`;
@@ -13642,11 +13689,14 @@ function asRecord(v) {
13642
13689
  return v && typeof v === "object" && !Array.isArray(v) ? v : {};
13643
13690
  }
13644
13691
  var NETWORK_ASK = "SandboxNetworkAccess";
13692
+ function isNetworkAsk(update) {
13693
+ return update.name === NETWORK_ASK || asRecord(asRecord(update._meta).claudeCode).toolName === NETWORK_ASK;
13694
+ }
13645
13695
  function heading(update) {
13646
- const name = update.name ?? update.title;
13696
+ const name = typeof update.name === "string" ? update.name : "";
13647
13697
  const host = asRecord(update.rawInput).host;
13648
- if (name === NETWORK_ASK && typeof host === "string")
13649
- return { name: "network", title: host, kind: "fetch" };
13698
+ if (isNetworkAsk(update) && typeof host === "string")
13699
+ return { name: "", title: host, kind: "fetch" };
13650
13700
  return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
13651
13701
  }
13652
13702
  function endOfAsk(req, res, memos) {
@@ -13664,40 +13714,59 @@ function endOfAsk(req, res, memos) {
13664
13714
  isError: !allowed
13665
13715
  };
13666
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
+ }
13667
13727
  function mapUpdate(update, memos, tag) {
13668
13728
  switch (update.sessionUpdate) {
13669
13729
  case "agent_message_chunk": {
13730
+ const { parentToolId } = spawnOf(update._meta);
13731
+ const out = abandoned(memos, parentToolId);
13670
13732
  const text = textOf(update.content, tag, "message");
13671
13733
  if (text === null)
13672
- return [];
13673
- const { parentToolId } = spawnOf(update._meta);
13674
- 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;
13675
13737
  }
13676
13738
  case "agent_thought_chunk": {
13677
- if (spawnOf(update._meta).parentToolId)
13678
- return [];
13739
+ const { parentToolId } = spawnOf(update._meta);
13740
+ const out = abandoned(memos, parentToolId);
13741
+ if (parentToolId)
13742
+ return out;
13679
13743
  const text = textOf(update.content, tag, "thought");
13680
- return text === null ? [] : [{ type: "thinking-delta", text }];
13744
+ if (text !== null)
13745
+ out.push({ type: "thinking-delta", text });
13746
+ return out;
13681
13747
  }
13682
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 ?? [] };
13683
13752
  const memo = {
13684
13753
  ...heading(update),
13685
13754
  content: update.content ?? [],
13686
13755
  rawOutput: update.rawOutput,
13687
- ended: false
13756
+ ended: false,
13757
+ writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
13758
+ ...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
13688
13759
  };
13689
13760
  memos.set(update.toolCallId, memo);
13690
- const out = [
13691
- {
13692
- type: "tool-start",
13693
- toolId: update.toolCallId,
13694
- name: memo.name,
13695
- input: update.rawInput ?? { locations: update.locations ?? [] },
13696
- ...memo.kind ? { kind: memo.kind } : {},
13697
- title: memo.title,
13698
- ...spawnOf(update._meta)
13699
- }
13700
- ];
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
+ });
13701
13770
  if (update.status === "completed" || update.status === "failed")
13702
13771
  out.push(endOf(update.toolCallId, memo, update.status));
13703
13772
  return out;
@@ -13706,37 +13775,45 @@ function mapUpdate(update, memos, tag) {
13706
13775
  let memo = memos.get(update.toolCallId);
13707
13776
  const out = [];
13708
13777
  if (!memo) {
13778
+ const spawn = spawnOf(update._meta);
13779
+ const input = update.rawInput ?? { locations: update.locations ?? [] };
13709
13780
  memo = {
13710
- name: update.name ?? update.title ?? "tool",
13781
+ name: update.name ?? "",
13711
13782
  title: update.title ?? update.name ?? "tool",
13712
13783
  ...update.kind ? { kind: update.kind } : {},
13713
13784
  content: [],
13714
- ended: false
13785
+ ended: false,
13786
+ writing: isWrittenKind(update.kind ?? undefined) && emptyInput(input),
13787
+ ...spawn.parentToolId ? { parent: spawn.parentToolId } : {}
13715
13788
  };
13716
13789
  memos.set(update.toolCallId, memo);
13717
13790
  out.push({
13718
13791
  type: "tool-start",
13719
13792
  toolId: update.toolCallId,
13720
13793
  name: memo.name,
13721
- input: update.rawInput ?? { locations: update.locations ?? [] },
13794
+ input,
13722
13795
  ...memo.kind ? { kind: memo.kind } : {},
13723
13796
  title: memo.title,
13724
- ...spawnOf(update._meta)
13797
+ ...spawn
13725
13798
  });
13726
13799
  }
13800
+ if (update.rawInput !== undefined && update.content === undefined && update.status === undefined)
13801
+ return out;
13727
13802
  const refined = { type: "tool-update", toolId: update.toolCallId };
13728
13803
  if (update.title && update.title !== memo.title) {
13729
13804
  memo.title = update.title;
13730
13805
  refined.title = update.title;
13731
- if (!update.name && memo.name !== update.title)
13732
- refined.name = memo.name = update.title;
13733
13806
  }
13734
- if (update.name && update.name !== memo.name)
13807
+ if (update.name && update.name !== memo.name) {
13735
13808
  refined.name = memo.name = update.name;
13809
+ }
13736
13810
  if (update.kind && update.kind !== memo.kind)
13737
13811
  refined.kind = memo.kind = update.kind;
13738
- if (update.rawInput !== undefined)
13812
+ if (update.rawInput !== undefined) {
13739
13813
  refined.input = update.rawInput;
13814
+ if (!emptyInput(update.rawInput))
13815
+ memo.writing = false;
13816
+ }
13740
13817
  if (Object.keys(refined).length > 2 && !memo.ended && out.length === 0)
13741
13818
  out.push(refined);
13742
13819
  if (update.content)
@@ -13850,6 +13927,7 @@ function steerOutcome(value) {
13850
13927
 
13851
13928
  // ../daemon/src/agent/acp/session.ts
13852
13929
  var DEFAULT_IDLE_MS = Number(process.env.TOYON_AGENT_IDLE_MS) || 5 * 60000;
13930
+ var OWN_SETTLE_MS = 1e4;
13853
13931
  function recorded(reply) {
13854
13932
  if (reply?.kind === "choice")
13855
13933
  return { choiceId: reply.choiceId };
@@ -13868,8 +13946,8 @@ class AcpSession {
13868
13946
  conn = null;
13869
13947
  connecting = null;
13870
13948
  live = null;
13871
- loading = false;
13872
13949
  reaper = null;
13950
+ own = null;
13873
13951
  asking = 0;
13874
13952
  asks = new Map;
13875
13953
  log;
@@ -13897,13 +13975,38 @@ class AcpSession {
13897
13975
  async warmCommands() {
13898
13976
  if (this.commandList.length > 0 || this.stopped)
13899
13977
  return;
13978
+ await this.warm();
13979
+ }
13980
+ async warm() {
13981
+ if (this.stopped)
13982
+ return;
13900
13983
  try {
13901
- await this.ensureLive();
13984
+ await this.ensureLive("warm-up");
13902
13985
  this.maybeArmReaper();
13903
13986
  } catch (e) {
13904
- 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)}`);
13905
13988
  }
13906
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
+ }
13907
14010
  setCommands(next) {
13908
14011
  if (sameCommands(this.commandList, next))
13909
14012
  return;
@@ -13953,7 +14056,7 @@ class AcpSession {
13953
14056
  return log.warn(this.d.worktreeId, "send after close dropped");
13954
14057
  const { context, attachments } = opts;
13955
14058
  const item = { text, context, ...attachments?.length ? { attachments } : {} };
13956
- if (this.running && !this.interrupted && this.live?.conn.steering) {
14059
+ if ((this.running || this.own) && !this.interrupted && this.live?.conn.steering) {
13957
14060
  return fireAndForget(this.d.worktreeId, this.steer(item), "agent steer");
13958
14061
  }
13959
14062
  this.enqueue(item);
@@ -13967,7 +14070,7 @@ class AcpSession {
13967
14070
  async steer(item) {
13968
14071
  item.recorded ??= await this.record(item);
13969
14072
  const live = this.live;
13970
- if (!live || !this.running || this.interrupted)
14073
+ if (!live || !(this.running || this.own) || this.interrupted)
13971
14074
  return this.enqueue(item);
13972
14075
  this.steered.push(item);
13973
14076
  let outcome;
@@ -14005,8 +14108,14 @@ class AcpSession {
14005
14108
  }
14006
14109
  stop() {
14007
14110
  this.cancelAsks();
14008
- 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
+ }
14009
14117
  return;
14118
+ }
14010
14119
  this.interrupted = true;
14011
14120
  if (this.steered.length > 0) {
14012
14121
  this.queue.unshift(...this.steered);
@@ -14025,10 +14134,11 @@ class AcpSession {
14025
14134
  this.steered = [];
14026
14135
  this.queueChanged();
14027
14136
  this.stop();
14028
- await this.dropConn();
14137
+ await this.dropConn("closing");
14029
14138
  await this.log.flush();
14030
14139
  }
14031
14140
  async drain() {
14141
+ this.endOwn("end_turn");
14032
14142
  this.running = true;
14033
14143
  this.clearReaper();
14034
14144
  this.setStatus("working");
@@ -14066,7 +14176,7 @@ class AcpSession {
14066
14176
  } else {
14067
14177
  this.emit({ type: "agent-error", message: this.describe(e), ts: Date.now() });
14068
14178
  this.setStatus("error");
14069
- fireAndForget(this.d.worktreeId, this.dropConn(), "drop agent after error");
14179
+ fireAndForget(this.d.worktreeId, this.dropConn("error"), "drop agent after error");
14070
14180
  }
14071
14181
  } finally {
14072
14182
  this.interrupted = false;
@@ -14081,7 +14191,7 @@ class AcpSession {
14081
14191
  this.asking++;
14082
14192
  this.clearReaper();
14083
14193
  try {
14084
- const conn = await this.ensureConn();
14194
+ const conn = await this.ensureConn("an ask");
14085
14195
  return await askOnce(conn.ctx, {
14086
14196
  cwd: this.d.cwd,
14087
14197
  system,
@@ -14107,7 +14217,7 @@ class AcpSession {
14107
14217
  }
14108
14218
  }
14109
14219
  async authenticate(methodId, apiKey) {
14110
- const conn = await this.ensureConn();
14220
+ const conn = await this.ensureConn("login");
14111
14221
  const method = conn.authMethods.find((m) => m.id === methodId);
14112
14222
  if (!method)
14113
14223
  throw new UserError(`${conn.spec.name} offers no login method "${methodId}"`);
@@ -14183,7 +14293,7 @@ class AcpSession {
14183
14293
  async runTurn(item) {
14184
14294
  item.recorded ??= await this.record(item);
14185
14295
  this.emit({ type: "turn-start", ts: Date.now() });
14186
- const live = await this.ensureLive();
14296
+ const live = await this.ensureLive("a turn");
14187
14297
  await this.applyMode(live);
14188
14298
  await this.applyOption(live, "model");
14189
14299
  await this.applyOption(live, "thought_level");
@@ -14203,17 +14313,17 @@ class AcpSession {
14203
14313
  contextFor(item) {
14204
14314
  return ambientBlock([...item.context ?? [], this.d.preview?.()]);
14205
14315
  }
14206
- ensureConn() {
14316
+ ensureConn(why) {
14207
14317
  if (this.conn)
14208
14318
  return Promise.resolve(this.conn);
14209
14319
  if (this.connecting)
14210
14320
  return this.connecting;
14211
- this.connecting = this.openConn().finally(() => {
14321
+ this.connecting = this.openConn(why).finally(() => {
14212
14322
  this.connecting = null;
14213
14323
  });
14214
14324
  return this.connecting;
14215
14325
  }
14216
- async openConn() {
14326
+ async openConn(why) {
14217
14327
  const spec = this.d.spec();
14218
14328
  const prepared = await (this.d.prepare ?? prepareLaunch)(this.d.cwd, spec);
14219
14329
  const { bounds } = prepared;
@@ -14225,13 +14335,17 @@ class AcpSession {
14225
14335
  this.d.onAuth?.(spec.id, { status: c.params });
14226
14336
  });
14227
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"})`);
14228
14340
  const ctx = link.conn.agent;
14229
14341
  link.exited.then(() => {
14342
+ this.noteProcess(link, false);
14230
14343
  if (this.conn?.link === link) {
14231
14344
  log.warn(this.d.worktreeId, "agent process exited while idle");
14232
14345
  this.conn = null;
14233
14346
  this.live = null;
14234
14347
  this.cancelAsks();
14348
+ this.endOwn("interrupted");
14235
14349
  }
14236
14350
  });
14237
14351
  try {
@@ -14250,7 +14364,7 @@ class AcpSession {
14250
14364
  spec,
14251
14365
  bounds,
14252
14366
  authMethods: init.authMethods ?? [],
14253
- loadSession: !!init.agentCapabilities?.loadSession,
14367
+ resumeSession: !!init.agentCapabilities?.sessionCapabilities?.resume,
14254
14368
  closeSupported: !!init.agentCapabilities?.sessionCapabilities?.close,
14255
14369
  deleteSupported: !!init.agentCapabilities?.sessionCapabilities?.delete,
14256
14370
  acceptsImages: init.agentCapabilities?.promptCapabilities?.image === true,
@@ -14262,11 +14376,12 @@ class AcpSession {
14262
14376
  return this.conn;
14263
14377
  } catch (e) {
14264
14378
  fireAndForget(this.d.worktreeId, link.kill(), "kill agent after failed start");
14379
+ this.noteProcess(link, false);
14265
14380
  throw e;
14266
14381
  }
14267
14382
  }
14268
- async ensureLive() {
14269
- const conn = await this.ensureConn();
14383
+ async ensureLive(why) {
14384
+ const conn = await this.ensureConn(why);
14270
14385
  if (this.live?.conn === conn)
14271
14386
  return this.live;
14272
14387
  const additionalDirectories = conn.bounds.gitDir ? [conn.bounds.gitDir] : [];
@@ -14274,10 +14389,9 @@ class AcpSession {
14274
14389
  let modes;
14275
14390
  let configOptions;
14276
14391
  let resumed = false;
14277
- if (sessionId && conn.loadSession) {
14278
- this.loading = true;
14392
+ if (sessionId && conn.resumeSession) {
14279
14393
  try {
14280
- const r = await conn.ctx.request(methods.agent.session.load, {
14394
+ const r = await conn.ctx.request(methods.agent.session.resume, {
14281
14395
  sessionId,
14282
14396
  cwd: this.d.cwd,
14283
14397
  mcpServers: [],
@@ -14290,8 +14404,6 @@ class AcpSession {
14290
14404
  if (isAuthRequired(e))
14291
14405
  throw e;
14292
14406
  log.warn(this.d.worktreeId, `could not resume agent session ${sessionId}; starting a new one`, e);
14293
- } finally {
14294
- this.loading = false;
14295
14407
  }
14296
14408
  }
14297
14409
  if (!resumed) {
@@ -14388,8 +14500,6 @@ class AcpSession {
14388
14500
  this.setCommands(mapped);
14389
14501
  return;
14390
14502
  }
14391
- if (this.loading)
14392
- return;
14393
14503
  const listener = side.get(params.sessionId);
14394
14504
  if (listener) {
14395
14505
  const u = params.update;
@@ -14407,7 +14517,10 @@ class AcpSession {
14407
14517
  }
14408
14518
  if (params.update.sessionUpdate === "config_option_update")
14409
14519
  this.absorb(live, params.update.configOptions);
14410
- 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) {
14411
14524
  if (ev.type === "text-delta" || ev.type === "tool-start")
14412
14525
  this.steered = [];
14413
14526
  this.emit(ev.type === "session-info" ? { ...ev, sessionId: live.sessionId } : ev);
@@ -14469,7 +14582,7 @@ class AcpSession {
14469
14582
  });
14470
14583
  }
14471
14584
  async sayPlanEdited(proposed, path) {
14472
- if (!await this.d.planEdited?.(proposed))
14585
+ if (!await this.d.planEdited?.(path, proposed))
14473
14586
  return;
14474
14587
  this.send(`I edited the plan before approving it. Build what is in ${path}, not the plan you proposed.`);
14475
14588
  }
@@ -14530,21 +14643,56 @@ class AcpSession {
14530
14643
  syncStatus() {
14531
14644
  if (this.status === "error")
14532
14645
  return;
14533
- 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();
14534
14682
  }
14535
14683
  maybeArmReaper() {
14536
- 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)
14537
14685
  this.armReaper();
14538
14686
  }
14539
14687
  armReaper() {
14540
14688
  this.clearReaper();
14689
+ const idleMs = this.d.idleMs ?? DEFAULT_IDLE_MS;
14541
14690
  const t = setTimeout(() => {
14542
14691
  this.reaper = null;
14543
- 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)
14544
14693
  return;
14545
- log.debug(this.d.worktreeId, "agent idle; stopping its process");
14546
- fireAndForget(this.d.worktreeId, this.dropConn(), "reap agent");
14547
- }, this.d.idleMs ?? DEFAULT_IDLE_MS);
14694
+ fireAndForget(this.d.worktreeId, this.dropConn(`idle for ${Math.round(idleMs / 60000)} min`), "reap agent");
14695
+ }, idleMs);
14548
14696
  t.unref?.();
14549
14697
  this.reaper = t;
14550
14698
  }
@@ -14553,15 +14701,17 @@ class AcpSession {
14553
14701
  clearTimeout(this.reaper);
14554
14702
  this.reaper = null;
14555
14703
  }
14556
- async dropConn() {
14704
+ async dropConn(why) {
14557
14705
  this.cancelAsks();
14558
14706
  const conn = this.conn;
14559
14707
  this.conn = null;
14560
14708
  this.live = null;
14561
14709
  if (!conn)
14562
14710
  return;
14711
+ log.info(this.d.worktreeId, `agent ${conn.spec.id} stopped: ${why} (pid ${conn.link.pid ?? "unknown"})`);
14563
14712
  conn.link.conn.close();
14564
14713
  await conn.link.kill();
14714
+ this.noteProcess(conn.link, false);
14565
14715
  }
14566
14716
  }
14567
14717
  function isAuthRequired(e) {
@@ -14625,34 +14775,216 @@ function sameCommands(a, b) {
14625
14775
  }
14626
14776
 
14627
14777
  // ../daemon/src/agent/planDoc.ts
14628
- import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
14778
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
14629
14779
  import { join as join7 } from "path";
14630
- var PLAN_REL = join7(".toyon", "plan.md");
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
+ }
14631
14796
  async function writePlanDoc(cwd, markdown) {
14797
+ const dir = join7(cwd, PLANS_DIR);
14798
+ const rel = join7(PLANS_DIR, `${nextNumber(dir)}.md`);
14632
14799
  try {
14633
- mkdirSync5(join7(cwd, ".toyon"), { recursive: true });
14634
- writeFileSync4(join7(cwd, PLAN_REL), markdown.endsWith(`
14800
+ mkdirSync5(dir, { recursive: true });
14801
+ writeFileSync4(join7(cwd, rel), markdown.endsWith(`
14635
14802
  `) ? markdown : `${markdown}
14636
14803
  `);
14637
14804
  } catch (e) {
14638
- log.warn(cwd, `could not write ${PLAN_REL}; the plan stays on the card`, e);
14805
+ log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
14639
14806
  return null;
14640
14807
  }
14641
- await excludeFromGit(cwd, PLAN_REL);
14642
- return PLAN_REL;
14808
+ await excludeFromGit(cwd, `${PLANS_DIR}/`);
14809
+ return rel;
14643
14810
  }
14644
- function planEdited(cwd, proposed) {
14645
- const file = join7(cwd, PLAN_REL);
14811
+ function planEdited(cwd, rel, proposed) {
14812
+ const file = join7(cwd, rel);
14646
14813
  if (!existsSync8(file))
14647
14814
  return false;
14648
14815
  try {
14649
14816
  return readFileSync7(file, "utf8").trim() !== proposed.trim();
14650
14817
  } catch (e) {
14651
- log.warn(cwd, `could not read ${PLAN_REL}`, e);
14818
+ log.warn(cwd, `could not read ${rel}`, e);
14652
14819
  return false;
14653
14820
  }
14654
14821
  }
14655
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
+
14656
14988
  // ../daemon/src/runtime/ports.ts
14657
14989
  var allocated = new Set;
14658
14990
  var range = cloud.proxyPorts;
@@ -15441,7 +15773,7 @@ class WorktreeProcs {
15441
15773
  async killProc(mp) {
15442
15774
  await mp.pty?.kill();
15443
15775
  }
15444
- async sleep() {
15776
+ async sleep(why) {
15445
15777
  if (this.asleep || this.stopped)
15446
15778
  return;
15447
15779
  this.asleep = true;
@@ -15449,6 +15781,7 @@ class WorktreeProcs {
15449
15781
  for (const mp of this.procs.values()) {
15450
15782
  mp.state.status = "asleep";
15451
15783
  mp.state.pid = undefined;
15784
+ mp.state.detail = why;
15452
15785
  this.onProc({ ...mp.state });
15453
15786
  exits.push(this.killProc(mp));
15454
15787
  }
@@ -15463,13 +15796,16 @@ class WorktreeProcs {
15463
15796
  this.spawnProc(mp);
15464
15797
  }
15465
15798
  }
15466
- pgids() {
15799
+ groups() {
15467
15800
  const out = [];
15468
- for (const mp of this.procs.values())
15801
+ for (const [name, mp] of this.procs)
15469
15802
  if (mp.pty?.alive)
15470
- out.push(mp.pty.pid);
15803
+ out.push({ name, pgid: mp.pty.pid });
15471
15804
  return out;
15472
15805
  }
15806
+ pgids() {
15807
+ return this.groups().map((g) => g.pgid);
15808
+ }
15473
15809
  async stopAll() {
15474
15810
  this.stopped = true;
15475
15811
  const exits = [];
@@ -15538,11 +15874,12 @@ function terminalEnv(base, own, urls) {
15538
15874
  var DEFAULT_AGENT_ID = "claude";
15539
15875
  var PREVIEW_WAKE_MS = 8000;
15540
15876
  var PREVIEW_POLL_MS = 100;
15541
- function defaultAgent(wt, d, preview) {
15877
+ function defaultAgent(wt, d, preview, onProcess) {
15542
15878
  const agent = new AcpSession({
15543
15879
  worktreeId: wt.id,
15544
15880
  cwd: wt.path,
15545
15881
  preview: () => previewContext(preview()),
15882
+ onProcess,
15546
15883
  spec: () => {
15547
15884
  const w = d.state.requireWorktree(wt.id);
15548
15885
  if (!w.agent) {
@@ -15578,9 +15915,16 @@ function defaultAgent(wt, d, preview) {
15578
15915
  w.planned = true;
15579
15916
  d.state.save();
15580
15917
  }
15581
- return markdown.trim() ? writePlanDoc(wt.path, markdown) : null;
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;
15582
15926
  },
15583
- planEdited: async (proposed) => planEdited(wt.path, proposed),
15927
+ planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
15584
15928
  option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
15585
15929
  onOptionsLearned: (category, choices) => {
15586
15930
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
@@ -15630,8 +15974,10 @@ class RuntimeRegistry {
15630
15974
  holds = new Map;
15631
15975
  settingUp = new Set;
15632
15976
  starting = new Set;
15977
+ stopping = new Map;
15633
15978
  constructor(deps) {
15634
15979
  this.deps = deps;
15980
+ deps.hub.on("proc", (id) => this.recordGroups(id));
15635
15981
  }
15636
15982
  hold(id, tag) {
15637
15983
  let tags = this.holds.get(id);
@@ -15700,16 +16046,18 @@ class RuntimeRegistry {
15700
16046
  if (repo)
15701
16047
  await this.start(wt, repo);
15702
16048
  }
15703
- async sleep(id) {
16049
+ async sleep(id, why) {
15704
16050
  const rt = this.runtimes.get(id);
15705
16051
  if (!rt?.procs || rt.procs.asleep)
15706
16052
  return;
16053
+ log.info(id, `asleep: ${why}`);
15707
16054
  if (this.ports.ranged()) {
15708
16055
  rt.proxy?.stop();
15709
16056
  rt.proxy = null;
15710
16057
  this.returnLease(id);
15711
16058
  }
15712
- await rt.procs.sleep();
16059
+ await rt.procs.sleep(why);
16060
+ this.recordGroups(id);
15713
16061
  this.deps.hub.emit("worktreesChanged");
15714
16062
  }
15715
16063
  get ports() {
@@ -15766,11 +16114,15 @@ class RuntimeRegistry {
15766
16114
  args: ["-l"]
15767
16115
  };
15768
16116
  try {
15769
- 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
+ });
15770
16121
  } catch (e) {
15771
16122
  throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
15772
16123
  }
15773
16124
  this.looseShells.set(id, term);
16125
+ this.recordGroups(id);
15774
16126
  } else if (term.cols !== cols || term.rows !== rows) {
15775
16127
  term.resize(cols, rows);
15776
16128
  }
@@ -15785,6 +16137,7 @@ class RuntimeRegistry {
15785
16137
  continue;
15786
16138
  this.looseShells.delete(id);
15787
16139
  fireAndForget(id, Promise.resolve(term.kill()), "loose shell cleanup");
16140
+ this.recordGroups(id);
15788
16141
  }
15789
16142
  }
15790
16143
  get(id) {
@@ -15806,7 +16159,7 @@ class RuntimeRegistry {
15806
16159
  return existing;
15807
16160
  const rt = {
15808
16161
  info: wt,
15809
- 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)),
15810
16163
  procs: null,
15811
16164
  proxy: null,
15812
16165
  previewName: undefined,
@@ -15822,6 +16175,8 @@ class RuntimeRegistry {
15822
16175
  const rt = this.ensureAgent(wt);
15823
16176
  if (rt.procs)
15824
16177
  return;
16178
+ if (wt.kind === "main" && this.deps.mainLeads && !this.deps.mainLeads(wt.repoId))
16179
+ return;
15825
16180
  const live = this.deps.state.requireWorktree(wt.id);
15826
16181
  const port = this.leasePort(live);
15827
16182
  if (port === null)
@@ -15872,12 +16227,11 @@ class RuntimeRegistry {
15872
16227
  leased = new Map;
15873
16228
  leasePort(wt) {
15874
16229
  let port = this.ports.lease(wt.proxyPort);
15875
- if (port === null && wt.kind !== "spare") {
16230
+ if (port === null && (wt.kind !== "spare" || this.deps.shown?.(wt.id))) {
15876
16231
  const holders = [...this.leased.keys()].filter((id) => id !== wt.id && !this.starting.has(id) && !this.busy(id) && !this.deps.viewed?.(id));
15877
16232
  const victim = oldestViewed(holders, this.deps.state);
15878
16233
  if (victim) {
15879
- log.info(victim.id, `asleep: its preview port went to ${wt.title}`);
15880
- 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");
15881
16235
  port = this.ports.lease(wt.proxyPort);
15882
16236
  }
15883
16237
  if (port === null) {
@@ -15905,6 +16259,7 @@ class RuntimeRegistry {
15905
16259
  proxy?.stop();
15906
16260
  this.returnLease(id);
15907
16261
  await procs?.stopAll();
16262
+ this.recordGroups(id);
15908
16263
  }
15909
16264
  async stop(id) {
15910
16265
  this.holds.delete(id);
@@ -15913,11 +16268,18 @@ class RuntimeRegistry {
15913
16268
  if (!rt)
15914
16269
  return;
15915
16270
  this.runtimes.delete(id);
16271
+ this.stopping.set(id, rt);
15916
16272
  rt.proxy?.stop();
15917
16273
  this.returnLease(id);
15918
16274
  const login = rt.login;
15919
16275
  rt.login = null;
15920
- 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
+ }
15921
16283
  }
15922
16284
  openTerminal(id, stream, cols, rows) {
15923
16285
  if (stream === LOGIN_STREAM)
@@ -15925,12 +16287,10 @@ class RuntimeRegistry {
15925
16287
  if (stream !== SHELL_STREAM)
15926
16288
  return this.openProcStream(id, stream, cols, rows);
15927
16289
  const wt = this.deps.state.requireWorktree(id);
15928
- if (wt.kind === "spare")
15929
- throw new UserError("no terminal for a spare worktree");
15930
16290
  const rt = this.ensureAgent(wt);
15931
16291
  let term = rt.shell;
15932
16292
  if (!term?.alive) {
15933
- const cwd = wt.linkPath ?? wt.path;
16293
+ const cwd = wt.path;
15934
16294
  const opts = {
15935
16295
  cwd,
15936
16296
  env: { ...this.shellEnv(wt), PWD: cwd },
@@ -15940,11 +16300,15 @@ class RuntimeRegistry {
15940
16300
  args: ["-l"]
15941
16301
  };
15942
16302
  try {
15943
- 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
+ });
15944
16307
  } catch (e) {
15945
16308
  throw new UserError(`could not start a shell: ${e instanceof Error ? e.message : String(e)}`);
15946
16309
  }
15947
16310
  rt.shell = term;
16311
+ this.recordGroups(wt.id);
15948
16312
  } else if (term.cols !== cols || term.rows !== rows) {
15949
16313
  term.resize(cols, rows);
15950
16314
  }
@@ -15957,7 +16321,7 @@ class RuntimeRegistry {
15957
16321
  rt.login = null;
15958
16322
  if (prev)
15959
16323
  fireAndForget(id, Promise.resolve(prev.pty.kill()), "replace the agent login");
15960
- const cwd = wt.linkPath ?? wt.path;
16324
+ const cwd = wt.path;
15961
16325
  let pty;
15962
16326
  try {
15963
16327
  pty = (this.deps.makeTerminal ?? defaultTerminal)(wt, {
@@ -15969,6 +16333,7 @@ class RuntimeRegistry {
15969
16333
  args: run.args
15970
16334
  }, (data) => this.deps.hub.emit("termData", id, LOGIN_STREAM, data), (code) => {
15971
16335
  this.deps.hub.emit("termExit", id, LOGIN_STREAM, code);
16336
+ this.recordGroups(id);
15972
16337
  if (rt.login?.pty !== pty)
15973
16338
  return;
15974
16339
  if (code === 0) {
@@ -15981,6 +16346,7 @@ class RuntimeRegistry {
15981
16346
  throw new UserError(`could not start the login: ${e instanceof Error ? e.message : String(e)}`);
15982
16347
  }
15983
16348
  rt.login = { pty, run };
16349
+ this.recordGroups(id);
15984
16350
  this.deps.hub.emit("worktreesChanged");
15985
16351
  }
15986
16352
  openLoginStream(id, cols, rows) {
@@ -16056,12 +16422,93 @@ class RuntimeRegistry {
16056
16422
  const url = `http://${host.includes(":") ? `[${host}]` : host}:${st.boundPort ?? st.port}`;
16057
16423
  return { status: st.status, url, ...st.detail ? { detail: st.detail } : {} };
16058
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
+ }
16059
16468
  async shutdown() {
16060
16469
  this.pruneLooseShells(new Set);
16061
16470
  await Promise.all([...this.runtimes.keys()].map((id) => this.stop(id)));
16062
16471
  }
16063
16472
  }
16064
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
+
16065
16512
  // ../daemon/src/agent/recap.ts
16066
16513
  var REPLY_KEEP = 2000;
16067
16514
  var CLIP = 160;
@@ -16077,6 +16524,7 @@ function turnsSince(entries, seenAt) {
16077
16524
  let pending = [];
16078
16525
  let open = null;
16079
16526
  const writes = new Set;
16527
+ const refusals = new Map;
16080
16528
  const start = (ts) => {
16081
16529
  const t = slice(pending, ts);
16082
16530
  pending = [];
@@ -16097,16 +16545,35 @@ function turnsSince(entries, seenAt) {
16097
16545
  open = start(e.ts);
16098
16546
  break;
16099
16547
  case "text-delta":
16100
- if (open)
16548
+ if (open) {
16101
16549
  open.reply = (open.reply + e.text).slice(-REPLY_KEEP);
16550
+ open.refused = undefined;
16551
+ }
16102
16552
  break;
16103
16553
  case "tool-start":
16104
16554
  if (!open)
16105
16555
  break;
16106
16556
  if (!e.parentToolId)
16107
16557
  open.reply = "";
16558
+ open.refused = undefined;
16108
16559
  countWrite(open, writes, e.toolId, e);
16109
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
+ }
16110
16577
  case "tool-update":
16111
16578
  if (open && e.kind)
16112
16579
  countWrite(open, writes, e.toolId, { name: e.name ?? "", kind: e.kind });
@@ -16163,7 +16630,8 @@ function factsOf(turns, end, ask) {
16163
16630
  ...end === "failed" && last?.error ? { error: last.error } : {},
16164
16631
  ...end === "failed" && last?.auth ? { auth: true } : {},
16165
16632
  ...end === "done" && cut ? { cut } : {},
16166
- ...end === "asking" && ask ? { ask } : {}
16633
+ ...end === "asking" && ask ? { ask } : {},
16634
+ ...end === "stopped" && last?.refused ? { planBack: true } : {}
16167
16635
  };
16168
16636
  }
16169
16637
  function firstAskOf(entries) {
@@ -16172,59 +16640,6 @@ function firstAskOf(entries) {
16172
16640
  return e.text;
16173
16641
  return;
16174
16642
  }
16175
- var RECAP_SYSTEM = "You write one-line status recaps for a developer coming back to a coding task. Reply with only the recap.";
16176
- 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.";
16177
- var RECAP_TURNS = 6;
16178
- var RECAP_CHARS = 4000;
16179
- function recapPrompt(i) {
16180
- const head = [RECAP_ASK, "", `Task: ${clip(i.title, 200)}`];
16181
- if (i.firstAsk)
16182
- head.push(`First request: ${clip(i.firstAsk, 300)}`);
16183
- const now = `Now: ${standing(i.end, i.facts)}`;
16184
- let blocks = i.turns.slice(-RECAP_TURNS).map(turnBlock);
16185
- const size = () => [...head, "", ...blocks, "", now].join(`
16186
-
16187
- `).length;
16188
- while (blocks.length > 1 && size() > RECAP_CHARS)
16189
- blocks = blocks.slice(1);
16190
- return [head.join(`
16191
- `), blocks.join(`
16192
-
16193
- `), now].join(`
16194
-
16195
- `);
16196
- }
16197
- function turnBlock(t) {
16198
- const facts = [t.edits ? plural(t.edits, "edit") : "", t.toolErrors ? plural(t.toolErrors, "failed tool") : ""].filter(Boolean).join(", ");
16199
- const lines = [];
16200
- if (t.asks.length)
16201
- lines.push(`You asked: ${clip(t.asks.join(" / "), 400)}`);
16202
- if (t.reply.trim())
16203
- lines.push(`Agent ended with: ${tail(t.reply, 800)}`);
16204
- if (facts)
16205
- lines.push(`Facts: ${facts}`);
16206
- return lines.join(`
16207
- `);
16208
- }
16209
- function standing(end, f) {
16210
- switch (end) {
16211
- case "asking":
16212
- return f.ask ? `waiting for the user's answer to "${f.ask}"` : "waiting for the user";
16213
- case "failed":
16214
- return f.error ? `failed: ${f.error}` : "failed";
16215
- case "stopped":
16216
- return "stopped before it finished";
16217
- case "done":
16218
- return f.cut ? `finished early (${f.cut})` : "finished";
16219
- }
16220
- }
16221
- function plural(n, word) {
16222
- return `${n} ${word}${n === 1 ? "" : "s"}`;
16223
- }
16224
- function tail(text, max) {
16225
- const line = text.replace(/\s+/g, " ").trim();
16226
- return line.length > max ? `\u2026${line.slice(-(max - 1))}` : line;
16227
- }
16228
16643
  var RECAP_MAX = 160;
16229
16644
  function parseRecap(text) {
16230
16645
  if (!text)
@@ -16241,15 +16656,17 @@ function parseRecap(text) {
16241
16656
  }
16242
16657
 
16243
16658
  // ../daemon/src/agent/landing.ts
16244
- 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.";
16245
16660
  var LAND_ASK = [
16246
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.",
16247
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.",
16248
16663
  "Reply with exactly this shape and nothing else:",
16249
- "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>",
16250
16665
  "Line 2: blank",
16251
- "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.",
16252
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",
16253
16670
  "Then: a short body, 1 to 4 plain sentences on what changed and why. No markdown, no bullets, no dashes as punctuation."
16254
16671
  ].join(`
16255
16672
  `);
@@ -16265,7 +16682,7 @@ ${i.recentSubjects.map((s) => ` ${s}`).join(`
16265
16682
  `)}`);
16266
16683
  const diff = `Diff summary:
16267
16684
  ${i.diffStat.trim() || "(none)"}`;
16268
- let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock2);
16685
+ let blocks = i.turns.slice(-LAND_TURNS).map(turnBlock);
16269
16686
  const size = () => [...head, "", ...blocks, "", diff].join(`
16270
16687
 
16271
16688
  `).length;
@@ -16278,13 +16695,36 @@ ${i.diffStat.trim() || "(none)"}`;
16278
16695
 
16279
16696
  `);
16280
16697
  }
16281
- function turnBlock2(t) {
16698
+ function turnBlock(t) {
16282
16699
  const lines = [];
16283
16700
  if (t.asks.length)
16284
16701
  lines.push(`User asked: ${clip(t.asks.join(" / "), 400)}`);
16285
16702
  if (t.reply.trim())
16286
16703
  lines.push(`Agent ended with: ${clip(t.reply, 1000)}`);
16287
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
+
16288
16728
  `);
16289
16729
  }
16290
16730
  var SUBJECT_MAX = 72;
@@ -16301,13 +16741,17 @@ function parseLanding(text) {
16301
16741
  const rest = lines.slice(lines.indexOf(lines.find((l) => l.trim() === first) ?? "") + 1);
16302
16742
  const paragraphs = rest.join(`
16303
16743
  `).split(/\n\s*\n/).map((p) => p.trim()).filter(Boolean);
16304
- const subject = paragraphs[0]?.replace(/\s+/g, " ").replace(/^(subject|title)\s*:\s*/i, "").replace(/[.]+$/, "").slice(0, SUBJECT_MAX).trim();
16305
- 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(`
16306
16749
 
16307
16750
  `).replace(/^(body)\s*:\s*/i, "").trim();
16308
16751
  return {
16309
16752
  ready: verdict === "ready",
16310
16753
  ...why ? { why: clip(why, 120) } : {},
16754
+ ...recap ? { recap } : {},
16311
16755
  ...subject && subject.split(" ").length >= 2 ? { subject } : {},
16312
16756
  ...body ? { body: body.slice(0, 1500) } : {}
16313
16757
  };
@@ -16361,7 +16805,7 @@ async function askFreshAgent(agents, agentId, cwd, system, prompt, opts = {}) {
16361
16805
 
16362
16806
  // ../daemon/src/agent/tasks.ts
16363
16807
  var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
16364
- 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.
16365
16809
 
16366
16810
  Task: ${task.slice(0, 500)}`;
16367
16811
  function taskText(text, attachments = []) {
@@ -16383,13 +16827,13 @@ function parseName(text) {
16383
16827
  return null;
16384
16828
  const line = text.trim().split(`
16385
16829
  `).at(-1) ?? "";
16386
- const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
16387
- 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);
16388
16832
  if (words.length < 1 || words.length > 3 || name.length < 3 || name.length > 20)
16389
16833
  return null;
16390
16834
  if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
16391
16835
  return null;
16392
- return name;
16836
+ return sentenceCase(name);
16393
16837
  }
16394
16838
  var PLAN_SYSTEM = "You are a task-planning assistant. Reply with only the requested JSON.";
16395
16839
  var planPrompt = (request) => [
@@ -16419,15 +16863,6 @@ function parsePlan(text) {
16419
16863
  function makeNamer(runtime) {
16420
16864
  return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
16421
16865
  }
16422
- function makeRecapper(runtime, agents, state) {
16423
- return async (wt, prompt) => {
16424
- const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
16425
- if (!spec || lacksQuickModel(spec, state))
16426
- return null;
16427
- const agent = runtime.agentFor(wt.id);
16428
- return agent ? parseRecap(await agent.ask(RECAP_SYSTEM, prompt, { quick: "require" })) : null;
16429
- };
16430
- }
16431
16866
  function lacksQuickModel(spec, state) {
16432
16867
  const quick = spec.quickModel;
16433
16868
  if (!quick)
@@ -16437,43 +16872,51 @@ function lacksQuickModel(spec, state) {
16437
16872
  const offered = state.cachedOptions(spec.id, "model");
16438
16873
  return offered.length > 0 && !offered.some((m) => m.id === quick);
16439
16874
  }
16440
- function makeLander(runtime, agents, state) {
16441
- return async (wt, prompt) => {
16875
+ function makeQuickAsker(runtime, agents, state) {
16876
+ return async (wt, system, prompt) => {
16442
16877
  const spec = agents.get(wt.agent ?? state.defaultAgent ?? DEFAULT_AGENT_ID);
16443
16878
  if (!spec || lacksQuickModel(spec, state))
16444
16879
  return null;
16445
16880
  const agent = runtime.agentFor(wt.id);
16446
- return agent ? parseLanding(await agent.ask(LAND_SYSTEM, prompt, { quick: "require" })) : null;
16881
+ return agent ? agent.ask(system, prompt, { quick: "require" }) : null;
16447
16882
  };
16448
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
+ }
16449
16892
  function makePlanner(agents) {
16450
16893
  return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
16451
16894
  }
16452
16895
 
16453
16896
  // ../daemon/src/core/assets.ts
16454
- import { existsSync as existsSync10, readdirSync as readdirSync2, rmSync, statSync as statSync2 } from "fs";
16455
- import { join as join8, 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";
16456
16899
  function locateAssets(here, env = process.env) {
16457
- const packaged = existsSync10(join8(here, "shell", "index.html"));
16900
+ const packaged = existsSync11(join9(here, "shell", "index.html"));
16458
16901
  return {
16459
- shellDist: env.TOYON_SHELL_DIST ?? (packaged ? join8(here, "shell") : join8(here, "../../shell/dist")),
16460
- bridgeJs: env.TOYON_BRIDGE_JS ?? (packaged ? join8(here, "bridge.js") : join8(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")),
16461
16904
  sourceRoot: packaged ? null : resolve5(here, "../../.."),
16462
- packageJson: packaged ? join8(here, "..", "package.json") : null
16905
+ packageJson: packaged ? join9(here, "..", "package.json") : null
16463
16906
  };
16464
16907
  }
16465
16908
  var KEEP_MS = 3 * 24 * 60 * 60 * 1000;
16466
16909
  function pruneAssets(shellDist) {
16467
- const dir = join8(shellDist, "assets");
16910
+ const dir = join9(shellDist, "assets");
16468
16911
  let names;
16469
16912
  try {
16470
- names = readdirSync2(dir);
16913
+ names = readdirSync3(dir);
16471
16914
  } catch {
16472
16915
  return 0;
16473
16916
  }
16474
16917
  const found = [];
16475
16918
  for (const name of names) {
16476
- const path = join8(dir, name);
16919
+ const path = join9(dir, name);
16477
16920
  try {
16478
16921
  found.push({ path, mtime: statSync2(path).mtimeMs });
16479
16922
  } catch {}
@@ -16494,7 +16937,7 @@ function pruneAssets(shellDist) {
16494
16937
  }
16495
16938
 
16496
16939
  // ../daemon/src/core/dialog.ts
16497
- import { existsSync as existsSync11 } from "fs";
16940
+ import { existsSync as existsSync12 } from "fs";
16498
16941
  import { homedir as homedir4 } from "os";
16499
16942
  var PROMPTS = {
16500
16943
  location: "Choose where the new project goes",
@@ -16512,7 +16955,7 @@ function folderDialog() {
16512
16955
  let cancelled = false;
16513
16956
  const ask = async (start, purpose) => {
16514
16957
  const typed = start === "~" || start.startsWith("~/") ? `${homedir4()}${start.slice(1)}` : start;
16515
- const from = typed.startsWith("/") && existsSync11(typed) ? typed : homedir4();
16958
+ const from = typed.startsWith("/") && existsSync12(typed) ? typed : homedir4();
16516
16959
  cancelled = false;
16517
16960
  const p = Bun.spawn(["osascript", ...SCRIPT.flatMap((line) => ["-e", line]), PROMPTS[purpose], from], {
16518
16961
  stdout: "pipe",
@@ -16616,12 +17059,12 @@ class Restarter {
16616
17059
  d.hub.on("agentStatus", settle);
16617
17060
  d.hub.on("worktreesChanged", settle);
16618
17061
  }
16619
- request() {
17062
+ request(opts = {}) {
16620
17063
  const refused = this.d.refusal();
16621
17064
  if (refused)
16622
17065
  return refused;
16623
17066
  this.asked = true;
16624
- this.attempt();
17067
+ this.attempt(opts.now);
16625
17068
  return null;
16626
17069
  }
16627
17070
  waitingOn() {
@@ -16630,13 +17073,18 @@ class Restarter {
16630
17073
  return this.gone ? [] : this.working();
16631
17074
  }
16632
17075
  working() {
16633
- 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");
16634
17080
  }
16635
- 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) {
16636
17085
  if (this.gone)
16637
17086
  return;
16638
- const names = this.working();
16639
- if (names.length === 0)
17087
+ if (now || this.working().length === 0)
16640
17088
  this.gone = true;
16641
17089
  const key = JSON.stringify(this.waitingOn());
16642
17090
  if (key !== this.announced) {
@@ -16728,14 +17176,15 @@ class SelfWatch {
16728
17176
  }
16729
17177
 
16730
17178
  // ../daemon/src/core/state.ts
16731
- import { randomBytes } from "crypto";
16732
- import { existsSync as existsSync12, readFileSync as readFileSync9, renameSync as renameSync2, writeFileSync as writeFileSync5 } 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;
16733
17182
  var SEEN_WORKTREES = 50;
16734
17183
  var empty = { repos: [], worktrees: [], sessions: {} };
16735
17184
  function loadState(paths) {
16736
17185
  ensureDirs(paths);
16737
17186
  const STATE_FILE = paths.stateFile;
16738
- if (!existsSync12(STATE_FILE))
17187
+ if (!existsSync13(STATE_FILE))
16739
17188
  return structuredClone(empty);
16740
17189
  let raw;
16741
17190
  try {
@@ -16765,6 +17214,10 @@ function loadState(paths) {
16765
17214
  if (!ids.has(id) && !id.startsWith("disc-"))
16766
17215
  delete state.seen?.[id];
16767
17216
  }
17217
+ for (const id of Object.keys(state.groups ?? {})) {
17218
+ if (!ids.has(id) && !id.startsWith("disc-"))
17219
+ delete state.groups?.[id];
17220
+ }
16768
17221
  if (state.modelCache) {
16769
17222
  state.optionCache ??= {};
16770
17223
  for (const [agentId, models] of Object.entries(state.modelCache)) {
@@ -16783,9 +17236,9 @@ function saveState(paths, state) {
16783
17236
  function loadOrCreateToken(paths) {
16784
17237
  ensureDirs(paths);
16785
17238
  const TOKEN_FILE = paths.tokenFile;
16786
- if (existsSync12(TOKEN_FILE))
17239
+ if (existsSync13(TOKEN_FILE))
16787
17240
  return readFileSync9(TOKEN_FILE, "utf8").trim();
16788
- const token = randomBytes(32).toString("hex");
17241
+ const token = randomBytes2(32).toString("hex");
16789
17242
  writeFileSync5(TOKEN_FILE, token, { mode: 384 });
16790
17243
  return token;
16791
17244
  }
@@ -16793,6 +17246,7 @@ function loadOrCreateToken(paths) {
16793
17246
  class StateStore {
16794
17247
  paths;
16795
17248
  state;
17249
+ groupsTimer = null;
16796
17250
  constructor(paths, state) {
16797
17251
  this.paths = paths;
16798
17252
  this.state = state ?? loadState(paths);
@@ -16875,6 +17329,55 @@ class StateStore {
16875
17329
  delete this.state.sessions[id];
16876
17330
  if (this.state.seen)
16877
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;
16878
17381
  this.save();
16879
17382
  }
16880
17383
  session(worktreeId) {
@@ -16935,12 +17438,15 @@ class StateStore {
16935
17438
  this.save();
16936
17439
  }
16937
17440
  save() {
17441
+ if (this.groupsTimer)
17442
+ clearTimeout(this.groupsTimer);
17443
+ this.groupsTimer = null;
16938
17444
  saveState(this.paths, this.state);
16939
17445
  }
16940
17446
  }
16941
17447
 
16942
17448
  // ../daemon/src/design/service.ts
16943
- import { join as join9 } from "path";
17449
+ import { join as join10 } from "path";
16944
17450
 
16945
17451
  // ../daemon/src/design/scan.ts
16946
17452
  var COLOR = /^(#|rgba?\(|hsla?\(|oklch\(|oklab\(|lab\(|lch\(|color\(|color-mix\(|light-dark\()/i;
@@ -17390,7 +17896,7 @@ class DesignService {
17390
17896
  async function readAll(root, paths) {
17391
17897
  const out = [];
17392
17898
  for (const path of paths) {
17393
- const file = Bun.file(join9(root, path));
17899
+ const file = Bun.file(join10(root, path));
17394
17900
  if (file.size === 0 || file.size > MAX_FILE_BYTES)
17395
17901
  continue;
17396
17902
  try {
@@ -17505,14 +18011,14 @@ function typicalUse(classes) {
17505
18011
  var normal = (s) => s.replace(/[-_]/g, "").toLowerCase();
17506
18012
  async function loadProjectTypescript(root) {
17507
18013
  try {
17508
- return await import(join9(root, "node_modules", "typescript", "lib", "typescript.js"));
18014
+ return await import(join10(root, "node_modules", "typescript", "lib", "typescript.js"));
17509
18015
  } catch {
17510
18016
  return null;
17511
18017
  }
17512
18018
  }
17513
18019
 
17514
18020
  // ../daemon/src/drafts/store.ts
17515
- import { existsSync as existsSync13, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
18021
+ import { existsSync as existsSync14, readFileSync as readFileSync10, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
17516
18022
  var SAVE_DELAY_MS = 1000;
17517
18023
 
17518
18024
  class DraftStore {
@@ -17521,7 +18027,7 @@ class DraftStore {
17521
18027
  saveTimer = null;
17522
18028
  constructor(d) {
17523
18029
  this.d = d;
17524
- if (!existsSync13(d.file))
18030
+ if (!existsSync14(d.file))
17525
18031
  return;
17526
18032
  try {
17527
18033
  const raw = JSON.parse(readFileSync10(d.file, "utf8"));
@@ -17541,6 +18047,9 @@ class DraftStore {
17541
18047
  has(boxId) {
17542
18048
  return !!this.drafts[boxId]?.trim();
17543
18049
  }
18050
+ text(boxId) {
18051
+ return this.drafts[boxId] ?? "";
18052
+ }
17544
18053
  set(boxId, text, clientId) {
17545
18054
  if ((this.drafts[boxId] ?? "") === text)
17546
18055
  return;
@@ -17608,16 +18117,20 @@ class ExecService {
17608
18117
  run(worktreeId, command) {
17609
18118
  fireAndForget(worktreeId, this.exec(worktreeId, command), `exec ${command}`);
17610
18119
  }
17611
- exec(worktreeId, command, name = SHELL_TOOL) {
18120
+ exec(worktreeId, command, name = SHELL_TOOL, opts = {}) {
17612
18121
  const wt = this.deps.state.requireWorktree(worktreeId);
17613
18122
  if (wt.kind === "spare")
17614
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");
17615
18126
  const agent = this.deps.runtime.agentFor(worktreeId);
17616
18127
  if (!agent)
17617
18128
  throw new UserError("worktree still starting; try again in a moment");
17618
18129
  const toolId = `${name}-${Date.now().toString(36)}-${++this.n}`;
17619
- agent.note({ type: "tool-start", toolId, name, input: { command }, kind: "execute" });
17620
- 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;
17621
18134
  const env = { ...this.deps.runtime.shellEnv(wt), PWD: cwd, TERM: "dumb", NO_COLOR: "1" };
17622
18135
  let proc;
17623
18136
  try {
@@ -17630,13 +18143,28 @@ class ExecService {
17630
18143
  });
17631
18144
  } catch (e) {
17632
18145
  const reason = e instanceof Error ? e.message : String(e);
18146
+ if (opts.quiet)
18147
+ agent.note(start);
17633
18148
  agent.note({ type: "tool-end", toolId, output: `could not run: ${reason}`, isError: true });
17634
18149
  return Promise.resolve({ exit: reason, text: "" });
17635
18150
  }
17636
18151
  const timer = setTimeout(() => this.kill(worktreeId, toolId), TIMEOUT_MS);
17637
18152
  this.track(worktreeId, toolId, { proc, timer });
17638
18153
  this.deps.runtime.hold(worktreeId, `exec:${toolId}`);
17639
- 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 });
17640
18168
  }
17641
18169
  stop(worktreeId) {
17642
18170
  for (const toolId of this.running.get(worktreeId)?.keys() ?? [])
@@ -17668,7 +18196,7 @@ class ExecService {
17668
18196
  r.proc.kill("SIGKILL");
17669
18197
  }, KILL_GRACE_MS2);
17670
18198
  }
17671
- async collect(worktreeId, toolId, proc, agent) {
18199
+ async collect(worktreeId, toolId, proc, agent, heldStart) {
17672
18200
  let text = "";
17673
18201
  let truncated = false;
17674
18202
  const drain = async (stream) => {
@@ -17703,6 +18231,10 @@ class ExecService {
17703
18231
  clearTimeout(r.timer);
17704
18232
  this.deps.runtime.release(worktreeId, `exec:${toolId}`);
17705
18233
  }
18234
+ if (heldStart && exit === 0)
18235
+ return { exit, text };
18236
+ if (heldStart)
18237
+ agent.note(heldStart);
17706
18238
  agent.note({ type: "tool-end", toolId, output: formatOutput(text, exit, truncated), isError: exit !== 0 });
17707
18239
  return { exit, text };
17708
18240
  }
@@ -17749,7 +18281,7 @@ function withRepoLock(repoPath, fn) {
17749
18281
  var fileLockKey = (absPath) => `file:${absPath}`;
17750
18282
 
17751
18283
  // ../daemon/src/git/status.ts
17752
- import { join as join10 } from "path";
18284
+ import { join as join11 } from "path";
17753
18285
  async function treeFingerprint(worktreePath) {
17754
18286
  const [head, diff, untracked] = await Promise.all([
17755
18287
  git(worktreePath, "rev-parse", "HEAD"),
@@ -17759,7 +18291,7 @@ async function treeFingerprint(worktreePath) {
17759
18291
  const others = await Promise.all(untracked.out.split(`
17760
18292
  `).filter(Boolean).map(async (p) => {
17761
18293
  try {
17762
- const f = Bun.file(join10(worktreePath, p));
18294
+ const f = Bun.file(join11(worktreePath, p));
17763
18295
  const body = f.size > 2000000 ? String(f.size) : Bun.hash(await f.arrayBuffer()).toString(36);
17764
18296
  return `${p}:${body}`;
17765
18297
  } catch {
@@ -17810,7 +18342,7 @@ async function numstat(worktreePath, ...range) {
17810
18342
  }
17811
18343
  async function untrackedLines(worktreePath, file) {
17812
18344
  try {
17813
- const f = Bun.file(join10(worktreePath, file));
18345
+ const f = Bun.file(join11(worktreePath, file));
17814
18346
  if (f.size > 2000000)
17815
18347
  return {};
17816
18348
  const buf = new Uint8Array(await f.arrayBuffer());
@@ -17888,6 +18420,10 @@ async function behindUpstream(worktreePath) {
17888
18420
  const r = await git(worktreePath, "rev-list", "--count", "HEAD..@{upstream}");
17889
18421
  return r.ok ? Number(r.out) || 0 : null;
17890
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
+ }
17891
18427
  async function aheadBehind(worktreePath, defaultBr) {
17892
18428
  const [a, b] = await Promise.all([
17893
18429
  git(worktreePath, "rev-list", "--count", `${defaultBr}..HEAD`),
@@ -18106,7 +18642,7 @@ function rankElementSources(el, rows) {
18106
18642
  // ../daemon/src/files/listing.ts
18107
18643
  var SUBMODULE_MODE = "160000";
18108
18644
  var entries = (out) => out.split("\x00").filter(Boolean);
18109
- function parseListing(staged, untracked, deleted) {
18645
+ function parseListing(staged, untracked, deleted, ignored = "") {
18110
18646
  const gone = new Set(entries(deleted));
18111
18647
  const paths = new Set;
18112
18648
  const submodules = new Set;
@@ -18124,7 +18660,18 @@ function parseListing(staged, untracked, deleted) {
18124
18660
  }
18125
18661
  for (const path of entries(untracked))
18126
18662
  paths.add(path);
18127
- 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;
18128
18675
  }
18129
18676
 
18130
18677
  // ../daemon/src/files/vite-offset.ts
@@ -18300,12 +18847,13 @@ class FileService {
18300
18847
  }
18301
18848
  async list(worktreeId) {
18302
18849
  const cwd = this.require(worktreeId).path;
18303
- const [staged, untracked, deleted] = await Promise.all([
18850
+ const [staged, untracked, deleted, ignored] = await Promise.all([
18304
18851
  git(cwd, "ls-files", "-z", "-s"),
18305
18852
  git(cwd, "ls-files", "-z", "-o", "--exclude-standard"),
18306
- git(cwd, "ls-files", "-z", "-d")
18853
+ git(cwd, "ls-files", "-z", "-d"),
18854
+ git(cwd, "ls-files", "-z", "-o", "-i", "--exclude-standard", "--directory")
18307
18855
  ]);
18308
- return parseListing(staged.out, untracked.out, deleted.out);
18856
+ return parseListing(staged.out, untracked.out, deleted.out, ignored.out);
18309
18857
  }
18310
18858
  async search(worktreeId, query) {
18311
18859
  const { path: cwd } = this.require(worktreeId);
@@ -18528,6 +19076,7 @@ class AfterLand {
18528
19076
  fireAndForget(repo.id, done, "afterLand");
18529
19077
  }
18530
19078
  async exec(repo, commands) {
19079
+ await this.d.settled?.();
18531
19080
  if (this.d.self.building(true))
18532
19081
  this.d.hub.emit("selfChanged");
18533
19082
  let failure;
@@ -18568,41 +19117,18 @@ class AfterLand {
18568
19117
  }
18569
19118
 
18570
19119
  // ../daemon/src/repos/registry.ts
18571
- import { existsSync as existsSync18, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
19120
+ import { existsSync as existsSync19, mkdirSync as mkdirSync6, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "fs";
18572
19121
  import { homedir as homedir7 } from "os";
18573
- import { basename as basename3, dirname as dirname6, join as join15 } from "path";
18574
-
18575
- // ../daemon/src/worktrees/naming.ts
18576
- import { randomBytes as randomBytes2 } from "crypto";
18577
- function shortId() {
18578
- return randomBytes2(5).toString("hex");
18579
- }
18580
- function slugify2(prompt, withRandom = true) {
18581
- const words = prompt.toLowerCase().replace(/[^a-z0-9\s-]/g, "").split(/\s+/).filter(Boolean).slice(0, 3);
18582
- const base = words.join("-").slice(0, 20).replace(/-+$/g, "") || "task";
18583
- return withRandom ? `${base}-${randomBytes2(2).toString("hex")}` : base;
18584
- }
18585
- function cleanTitle2(title) {
18586
- return title.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
18587
- }
18588
- var VARIANT_LENSES = [
18589
- "Take the most direct approach: the change most people would expect, in the smallest diff.",
18590
- "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.",
18591
- "Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
18592
- ];
18593
- function variantLens(index) {
18594
- const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
18595
- 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})`;
18596
- }
19122
+ import { basename as basename3, dirname as dirname6, join as join16 } from "path";
18597
19123
 
18598
19124
  // ../daemon/src/repos/browse.ts
18599
- import { existsSync as existsSync14, statSync as statSync3 } from "fs";
19125
+ import { existsSync as existsSync15, statSync as statSync3 } from "fs";
18600
19126
  import { readdir } from "fs/promises";
18601
19127
  import { homedir as homedir5 } from "os";
18602
- import { dirname as dirname4, join as join11 } from "path";
19128
+ import { dirname as dirname4, join as join12 } from "path";
18603
19129
  var MAX_ENTRIES = 40;
18604
19130
  function expandTilde(raw) {
18605
- return raw === "~" || raw.startsWith("~/") ? join11(homedir5(), raw.slice(1)) : raw;
19131
+ return raw === "~" || raw.startsWith("~/") ? join12(homedir5(), raw.slice(1)) : raw;
18606
19132
  }
18607
19133
  function collapseTilde(abs) {
18608
19134
  const home = homedir5();
@@ -18613,7 +19139,7 @@ function holdsNothing(names) {
18613
19139
  }
18614
19140
  async function describeFolder(abs) {
18615
19141
  const path = collapseTilde(abs);
18616
- if (existsSync14(join11(abs, ".git")))
19142
+ if (existsSync15(join12(abs, ".git")))
18617
19143
  return { path, kind: "project" };
18618
19144
  try {
18619
19145
  return { path, kind: holdsNothing(await readdir(abs)) ? "empty" : "folder" };
@@ -18628,10 +19154,10 @@ function describe2(typed, parent) {
18628
19154
  isDir = statSync3(typed).isDirectory();
18629
19155
  } catch {}
18630
19156
  return {
18631
- exists: existsSync14(typed),
19157
+ exists: existsSync15(typed),
18632
19158
  isDir,
18633
- isRepo: isDir && existsSync14(join11(typed, ".git")),
18634
- parentExists: existsSync14(parent)
19159
+ isRepo: isDir && existsSync15(join12(typed, ".git")),
19160
+ parentExists: existsSync15(parent)
18635
19161
  };
18636
19162
  }
18637
19163
  async function browsePath(raw) {
@@ -18655,16 +19181,16 @@ async function browsePath(raw) {
18655
19181
  continue;
18656
19182
  if (!name.toLowerCase().startsWith(prefix))
18657
19183
  continue;
18658
- const abs = join11(dir, name);
18659
- entries.push({ path: collapseTilde(abs), name, isRepo: existsSync14(join11(abs, ".git")) });
19184
+ const abs = join12(dir, name);
19185
+ entries.push({ path: collapseTilde(abs), name, isRepo: existsSync15(join12(abs, ".git")) });
18660
19186
  }
18661
19187
  entries.sort((a, b) => a.isRepo !== b.isRepo ? a.isRepo ? -1 : 1 : a.name.localeCompare(b.name));
18662
19188
  return { entries: entries.slice(0, MAX_ENTRIES), target };
18663
19189
  }
18664
19190
 
18665
19191
  // ../daemon/src/repos/config.ts
18666
- import { closeSync, existsSync as existsSync15, openSync as openSync2, readdirSync as readdirSync3, readFileSync as readFileSync11, readSync } from "fs";
18667
- import { join as join12 } 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";
18668
19194
 
18669
19195
  // ../../node_modules/.bun/jsonc-parser@3.3.1/node_modules/jsonc-parser/lib/esm/impl/scanner.js
18670
19196
  function createScanner(text, ignoreTrivia = false) {
@@ -20028,7 +20554,7 @@ function applyEdits(text, edits) {
20028
20554
 
20029
20555
  // ../daemon/src/repos/config.ts
20030
20556
  var PLACES = [CONFIG_FILES.folder, CONFIG_FILES.root];
20031
- var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync15(join12(repoPath, rel)));
20557
+ var present = (repoPath, place) => [place.shared, place.local].filter((rel) => existsSync16(join13(repoPath, rel)));
20032
20558
  var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
20033
20559
  function parseSettings(text) {
20034
20560
  const errors = [];
@@ -20053,7 +20579,7 @@ function readConfigFile(repoPath) {
20053
20579
  for (const rel of files) {
20054
20580
  let raw;
20055
20581
  try {
20056
- raw = parseSettings(readFileSync11(join12(repoPath, rel), "utf8"));
20582
+ raw = parseSettings(readFileSync11(join13(repoPath, rel), "utf8"));
20057
20583
  } catch (e) {
20058
20584
  return { ok: false, reason: `${rel} is not valid JSON: ${e instanceof Error ? e.message : String(e)}` };
20059
20585
  }
@@ -20110,8 +20636,8 @@ function configTarget(repoPath, made) {
20110
20636
  }
20111
20637
  function configBody(repoPath, rel, config) {
20112
20638
  const place = PLACES.find((p) => p.local === rel);
20113
- const abs = place && join12(repoPath, place.shared);
20114
- if (!abs || !existsSync15(abs))
20639
+ const abs = place && join13(repoPath, place.shared);
20640
+ if (!abs || !existsSync16(abs))
20115
20641
  return config;
20116
20642
  let shared;
20117
20643
  try {
@@ -20128,7 +20654,7 @@ function configText(repoPath, rel, config) {
20128
20654
  let text;
20129
20655
  let was;
20130
20656
  try {
20131
- text = readFileSync11(join12(repoPath, rel), "utf8");
20657
+ text = readFileSync11(join13(repoPath, rel), "utf8");
20132
20658
  was = parseSettings(text);
20133
20659
  } catch {
20134
20660
  return fresh;
@@ -20158,8 +20684,8 @@ function detectConfig(repoPath) {
20158
20684
  return { config: explicit.config, needsSetup: false };
20159
20685
  if (explicit)
20160
20686
  log.warn("config", `${repoPath}: ${explicit.reason}; detecting instead`);
20161
- const pkgPath = join12(repoPath, "package.json");
20162
- if (existsSync15(pkgPath)) {
20687
+ const pkgPath = join13(repoPath, "package.json");
20688
+ if (existsSync16(pkgPath)) {
20163
20689
  const pkg = JSON.parse(readFileSync11(pkgPath, "utf8"));
20164
20690
  const scripts = pkg.scripts ?? {};
20165
20691
  const runner = detectRunner(repoPath, pkg.packageManager);
@@ -20181,7 +20707,7 @@ function detectConfig(repoPath) {
20181
20707
  };
20182
20708
  }
20183
20709
  }
20184
- if (existsSync15(join12(repoPath, "start.sh"))) {
20710
+ if (existsSync16(join13(repoPath, "start.sh"))) {
20185
20711
  return { config: { run: { app: "./start.sh" } }, needsSetup: true, from: "start.sh" };
20186
20712
  }
20187
20713
  const assumed = assumeNothingToRun(repoPath);
@@ -20224,24 +20750,24 @@ function assumeNothingToRun(repoPath) {
20224
20750
  return marker && !servesPage(repoPath) ? marker : undefined;
20225
20751
  }
20226
20752
  function markerIn(repoPath) {
20227
- const named = NO_PAGE_MARKERS.find((f) => existsSync15(join12(repoPath, f)));
20753
+ const named = NO_PAGE_MARKERS.find((f) => existsSync16(join13(repoPath, f)));
20228
20754
  if (named)
20229
20755
  return named;
20230
20756
  let entries;
20231
20757
  try {
20232
- entries = readdirSync3(repoPath);
20758
+ entries = readdirSync4(repoPath);
20233
20759
  } catch {
20234
20760
  return;
20235
20761
  }
20236
20762
  return entries.find((f) => NO_PAGE_EXTENSIONS.some((ext) => f.endsWith(ext)));
20237
20763
  }
20238
20764
  function servesPage(repoPath) {
20239
- if (WEB_FILES.some((f) => existsSync15(join12(repoPath, f))))
20765
+ if (WEB_FILES.some((f) => existsSync16(join13(repoPath, f))))
20240
20766
  return true;
20241
- if (/^web:/m.test(head(join12(repoPath, "Procfile"))))
20767
+ if (/^web:/m.test(head(join13(repoPath, "Procfile"))))
20242
20768
  return true;
20243
20769
  for (const [files, names] of WEB_DEPENDENCIES) {
20244
- const text = files.map((f) => head(join12(repoPath, f))).join(`
20770
+ const text = files.map((f) => head(join13(repoPath, f))).join(`
20245
20771
  `);
20246
20772
  if (text.trim() && names.some((n) => mentions(text, n)))
20247
20773
  return true;
@@ -20251,14 +20777,14 @@ function servesPage(repoPath) {
20251
20777
  function frontEndBeside(repoPath) {
20252
20778
  let dirs;
20253
20779
  try {
20254
- dirs = readdirSync3(repoPath, { withFileTypes: true });
20780
+ dirs = readdirSync4(repoPath, { withFileTypes: true });
20255
20781
  } catch {
20256
20782
  return false;
20257
20783
  }
20258
20784
  return dirs.some((d) => {
20259
20785
  if (!d.isDirectory() || d.name.startsWith(".") || NOT_A_PACKAGE.has(d.name))
20260
20786
  return false;
20261
- const pkg = head(join12(repoPath, d.name, "package.json"));
20787
+ const pkg = head(join13(repoPath, d.name, "package.json"));
20262
20788
  if (!pkg)
20263
20789
  return false;
20264
20790
  try {
@@ -20273,7 +20799,7 @@ function mentions(text, name) {
20273
20799
  return new RegExp(`(?<![\\w-])${escaped}(?![\\w-])`, "i").test(text);
20274
20800
  }
20275
20801
  function head(path) {
20276
- if (!existsSync15(path))
20802
+ if (!existsSync16(path))
20277
20803
  return "";
20278
20804
  try {
20279
20805
  const fd = openSync2(path, "r");
@@ -20301,9 +20827,9 @@ function procCommand(runner, script, body) {
20301
20827
  return `${base}${runner === "npm" ? " --" : ""} ${flag}`;
20302
20828
  }
20303
20829
  function detectPages(repoPath, scripts, runner) {
20304
- if (!scripts.build || !existsSync15(join12(repoPath, "functions")))
20830
+ if (!scripts.build || !existsSync16(join13(repoPath, "functions")))
20305
20831
  return null;
20306
- const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join12(repoPath, f)).find((f) => existsSync15(f));
20832
+ const cfg = ["wrangler.jsonc", "wrangler.json", "wrangler.toml"].map((f) => join13(repoPath, f)).find((f) => existsSync16(f));
20307
20833
  if (!cfg)
20308
20834
  return null;
20309
20835
  const out = readFileSync11(cfg, "utf8").match(/pages_build_output_dir"?\s*[:=]\s*"([^"]+)"/)?.[1];
@@ -20312,20 +20838,20 @@ function detectPages(repoPath, scripts, runner) {
20312
20838
  return { web: `${runner} run build && npx wrangler pages dev ${out} --ip 127.0.0.1 --port $PORT` };
20313
20839
  }
20314
20840
  function detectRunner(repoPath, packageManager) {
20315
- if (existsSync15(join12(repoPath, "bun.lock")) || existsSync15(join12(repoPath, "bun.lockb")))
20841
+ if (existsSync16(join13(repoPath, "bun.lock")) || existsSync16(join13(repoPath, "bun.lockb")))
20316
20842
  return "bun";
20317
- if (existsSync15(join12(repoPath, "pnpm-lock.yaml")))
20843
+ if (existsSync16(join13(repoPath, "pnpm-lock.yaml")))
20318
20844
  return "pnpm";
20319
- if (existsSync15(join12(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
20845
+ if (existsSync16(join13(repoPath, "yarn.lock")) || typeof packageManager === "string" && packageManager.startsWith("yarn"))
20320
20846
  return "yarn";
20321
20847
  return "npm";
20322
20848
  }
20323
20849
 
20324
20850
  // ../daemon/src/repos/create.ts
20325
- import { existsSync as existsSync16, 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";
20326
20852
  import { appendFile as appendFile2, mkdir as mkdir2, rm } from "fs/promises";
20327
20853
  import { homedir as homedir6 } from "os";
20328
- import { dirname as dirname5, join as join13, resolve as resolve7 } from "path";
20854
+ import { dirname as dirname5, join as join14, resolve as resolve7 } from "path";
20329
20855
  function isInside(dir, parent) {
20330
20856
  const p = parent.replace(/\/+$/, "");
20331
20857
  return dir === p || dir.startsWith(`${p}/`);
@@ -20336,29 +20862,29 @@ function planProject(opts, home = homedir6()) {
20336
20862
  throw new UserError(nameError);
20337
20863
  const first = firstProjectsFolder(opts.parent, home);
20338
20864
  const parent = first ?? resolveParent(opts.parent);
20339
- const dir = join13(parent, opts.name.trim());
20340
- if (existsSync16(dir))
20865
+ const dir = join14(parent, opts.name.trim());
20866
+ if (existsSync17(dir))
20341
20867
  throw new UserError(`${dir} already exists`);
20342
20868
  return { parent, dir, makeParent: first !== null };
20343
20869
  }
20344
20870
  function firstProjectsFolder(raw, home) {
20345
20871
  const typed = expandTilde(raw.trim()).replace(/\/+$/, "");
20346
- if (typed !== join13(home, PROJECTS_FOLDER) || existsSync16(typed) || !existsSync16(home))
20872
+ if (typed !== join14(home, PROJECTS_FOLDER) || existsSync17(typed) || !existsSync17(home))
20347
20873
  return null;
20348
- return join13(realpathSync2(home), PROJECTS_FOLDER);
20874
+ return join14(realpathSync2(home), PROJECTS_FOLDER);
20349
20875
  }
20350
20876
  function planInPlace(opts) {
20351
20877
  const name = opts.name.trim();
20352
20878
  if (!name || name === "." || name === ".." || name.includes("/")) {
20353
20879
  throw new UserError(`${opts.name} is not a folder name`);
20354
20880
  }
20355
- const typed = join13(resolveParent(opts.parent), name);
20356
- if (!existsSync16(typed) || !statSync4(typed).isDirectory())
20881
+ const typed = join14(resolveParent(opts.parent), name);
20882
+ if (!existsSync17(typed) || !statSync4(typed).isDirectory())
20357
20883
  throw new UserError(`${typed} is not a folder`);
20358
20884
  const dir = realpathSync2(typed);
20359
- if (existsSync16(join13(dir, ".git")))
20885
+ if (existsSync17(join14(dir, ".git")))
20360
20886
  throw new UserError(`${dir} is already a project; open it instead`);
20361
- if (!holdsNothing(readdirSync4(dir))) {
20887
+ if (!holdsNothing(readdirSync5(dir))) {
20362
20888
  throw new UserError(`${dir} is not empty; only an empty folder can become a project where it is`);
20363
20889
  }
20364
20890
  return { parent: dirname5(dir), dir, makeParent: false };
@@ -20367,7 +20893,7 @@ function resolveParent(raw) {
20367
20893
  const typed = expandTilde(raw.trim());
20368
20894
  if (!typed.startsWith("/"))
20369
20895
  throw new UserError(`${raw} is not a folder path`);
20370
- if (!existsSync16(typed))
20896
+ if (!existsSync17(typed))
20371
20897
  throw new UserError(`${raw} does not exist; make it first`);
20372
20898
  if (!statSync4(typed).isDirectory())
20373
20899
  throw new UserError(`${raw} is not a folder`);
@@ -20447,13 +20973,13 @@ async function undoCreate(dir) {
20447
20973
  }
20448
20974
  async function undoInit(dir) {
20449
20975
  try {
20450
- await rm(join13(dir, ".git"), { recursive: true, force: true });
20976
+ await rm(join14(dir, ".git"), { recursive: true, force: true });
20451
20977
  } catch (e) {
20452
20978
  log.warn(dir, "could not take back a half-made .git", e);
20453
20979
  }
20454
20980
  }
20455
20981
  async function unmakeProject(dir, made) {
20456
- const target = made === "folder" ? dir : join13(dir, ".git");
20982
+ const target = made === "folder" ? dir : join14(dir, ".git");
20457
20983
  try {
20458
20984
  await rm(target, { recursive: true, force: true });
20459
20985
  } catch (e) {
@@ -20483,8 +21009,8 @@ async function ok(p, what) {
20483
21009
  }
20484
21010
 
20485
21011
  // ../daemon/src/repos/watcher.ts
20486
- import { existsSync as existsSync17, watch } from "fs";
20487
- import { basename as basename2, join as join14 } from "path";
21012
+ import { existsSync as existsSync18, watch } from "fs";
21013
+ import { basename as basename2, join as join15 } from "path";
20488
21014
  function watchDefaultBranch(repoPath, branch, onMove) {
20489
21015
  let last = null;
20490
21016
  fireAndForget(repoPath, git(repoPath, "rev-parse", branch).then((r) => {
@@ -20514,8 +21040,8 @@ function watchDefaultBranch(repoPath, branch, onMove) {
20514
21040
  log.debug(repoPath, `not watching ${p}`, e);
20515
21041
  }
20516
21042
  };
20517
- tryWatch(join14(repoPath, ".git"));
20518
- tryWatch(join14(repoPath, ".git", "refs", "heads"));
21043
+ tryWatch(join15(repoPath, ".git"));
21044
+ tryWatch(join15(repoPath, ".git", "refs", "heads"));
20519
21045
  return () => {
20520
21046
  for (const w of watchers)
20521
21047
  w.close();
@@ -20537,7 +21063,7 @@ function watchWorktreeDir(repoPath, onChange) {
20537
21063
  }, 300);
20538
21064
  };
20539
21065
  const arm = (commonDir) => {
20540
- const dir = join14(commonDir, "worktrees");
21066
+ const dir = join15(commonDir, "worktrees");
20541
21067
  const armInner = () => {
20542
21068
  inner?.close();
20543
21069
  inner = null;
@@ -20580,7 +21106,7 @@ function watchWorktreeDir(repoPath, onChange) {
20580
21106
  function watchConfigFile(repoPath, onChange) {
20581
21107
  const atRoot = new Set([CONFIG_FILES.root.shared, CONFIG_FILES.root.local, CONFIG_DIR]);
20582
21108
  const inDir = new Set([CONFIG_FILES.folder.shared, CONFIG_FILES.folder.local].map((rel) => basename2(rel)));
20583
- const dir = join14(repoPath, CONFIG_DIR);
21109
+ const dir = join15(repoPath, CONFIG_DIR);
20584
21110
  let timer = null;
20585
21111
  let outer = null;
20586
21112
  let inner = null;
@@ -20595,7 +21121,7 @@ function watchConfigFile(repoPath, onChange) {
20595
21121
  const armInner = () => {
20596
21122
  inner?.close();
20597
21123
  inner = null;
20598
- if (!existsSync17(dir))
21124
+ if (!existsSync18(dir))
20599
21125
  return;
20600
21126
  try {
20601
21127
  inner = watch(dir, (_event, filename) => {
@@ -20649,7 +21175,7 @@ class RepoRegistry {
20649
21175
  }
20650
21176
  async boot() {
20651
21177
  const { state } = this.d;
20652
- const gone = state.worktrees.filter((wt) => !existsSync18(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));
20653
21179
  for (const wt of gone)
20654
21180
  await this.d.worktrees.forgetGone(wt);
20655
21181
  for (const repo of state.repos) {
@@ -20716,7 +21242,7 @@ class RepoRegistry {
20716
21242
  return `${repo.name} is set up now`;
20717
21243
  if (main.promptedAt || main.lastTurn)
20718
21244
  return `${repo.name} has a chat now`;
20719
- if (existsSync18(join15(repo.path, ".claude")))
21245
+ if (existsSync19(join16(repo.path, ".claude")))
20720
21246
  return `an agent has already run in ${repo.name}`;
20721
21247
  if ((await statusFiles(repo.path)).length > 0 || !await treeEmpty(repo.path)) {
20722
21248
  return `${repo.name} has files in it now`;
@@ -20793,7 +21319,7 @@ class RepoRegistry {
20793
21319
  }
20794
21320
  async register(rawPath, made) {
20795
21321
  const path = expandTilde(rawPath);
20796
- if (!existsSync18(path))
21322
+ if (!existsSync19(path))
20797
21323
  throw new UserError(`${path} does not exist`);
20798
21324
  if (!await isGitRepo(path))
20799
21325
  throw new UserError(`${path} is not a git repository`);
@@ -20831,7 +21357,6 @@ class RepoRegistry {
20831
21357
  };
20832
21358
  this.d.state.addWorktree(main);
20833
21359
  this.warmed.add(repo.id);
20834
- await this.d.runtime.start(main, repo);
20835
21360
  this.startWatcher(repo);
20836
21361
  fireAndForget(repo.id, this.d.worktrees.spare.ensure(repo.id), "spare warm-up");
20837
21362
  this.d.hub.emit("reposChanged");
@@ -20853,8 +21378,8 @@ class RepoRegistry {
20853
21378
  this.d.state.removeWorktree(wt.id);
20854
21379
  this.d.worktrees.deleteChat(wt.id);
20855
21380
  releasePort(wt.proxyPort);
21381
+ this.d.drafts?.drop(wt.id);
20856
21382
  }
20857
- this.d.drafts?.drop(draftKey(repoId));
20858
21383
  this.d.state.removeRepo(repoId);
20859
21384
  this.d.hub.emit("reposChanged");
20860
21385
  this.d.hub.emit("worktreesChanged");
@@ -20866,10 +21391,10 @@ class RepoRegistry {
20866
21391
  throw new UserError(current.reason);
20867
21392
  const was = configTarget(repo.path, !!repo.made);
20868
21393
  const rel = configSibling(was, kind);
20869
- const moved = rel !== was && existsSync18(join15(repo.path, was));
21394
+ const moved = rel !== was && existsSync19(join16(repo.path, was));
20870
21395
  if (moved && !(kind === "local" && await isTracked(repo.path, was))) {
20871
21396
  try {
20872
- rmSync2(join15(repo.path, was));
21397
+ rmSync2(join16(repo.path, was));
20873
21398
  } catch (e) {
20874
21399
  log.warn(repoId, `could not remove ${was}`, e);
20875
21400
  }
@@ -20878,7 +21403,7 @@ class RepoRegistry {
20878
21403
  }
20879
21404
  if (isLocalConfigFile(rel))
20880
21405
  await excludeFromGit(repo.path, rel);
20881
- const file = join15(repo.path, rel);
21406
+ const file = join16(repo.path, rel);
20882
21407
  try {
20883
21408
  mkdirSync6(dirname6(file), { recursive: true });
20884
21409
  writeFileSync7(file, configText(repo.path, rel, config));
@@ -20893,7 +21418,7 @@ class RepoRegistry {
20893
21418
  repo.assumed = undefined;
20894
21419
  this.d.state.save();
20895
21420
  this.warmed.add(repoId);
20896
- 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))) {
20897
21422
  fireAndForget(wt.id, this.d.runtime.stopProcs(wt.id).then(() => this.d.runtime.start(wt, repo)), "runtime restart");
20898
21423
  }
20899
21424
  fireAndForget(repoId, this.d.worktrees.spare.ensure(repoId), "spare warm-up");
@@ -20955,7 +21480,7 @@ class RepoRegistry {
20955
21480
  }
20956
21481
  placeConfig(repo) {
20957
21482
  const rel = configTarget(repo.path, !!repo.made);
20958
- if (isLocalConfigFile(rel) && existsSync18(join15(repo.path, rel))) {
21483
+ if (isLocalConfigFile(rel) && existsSync19(join16(repo.path, rel))) {
20959
21484
  fireAndForget(repo.id, excludeFromGit(repo.path, rel), "exclude local settings");
20960
21485
  }
20961
21486
  if (repo.configFile === rel)
@@ -21030,7 +21555,7 @@ class RepoRegistry {
21030
21555
  }
21031
21556
 
21032
21557
  // ../daemon/src/routes/service.ts
21033
- import { join as join16 } from "path";
21558
+ import { join as join17 } from "path";
21034
21559
 
21035
21560
  // ../daemon/src/routes/fileRouters.ts
21036
21561
  var SKIP = /(^|\/)(node_modules|dist|build|out|coverage|\.next|\.nuxt|\.output|\.svelte-kit|\.vercel|\.netlify)\//;
@@ -22230,7 +22755,7 @@ class RouteService {
22230
22755
  return mergeRoutes(fileRoutes(paths, roots), code);
22231
22756
  }
22232
22757
  async cachedRead(root, path, had, kept, decode) {
22233
- const abs = join16(root, path);
22758
+ const abs = join17(root, path);
22234
22759
  const stat = await this.fs.stat(abs);
22235
22760
  if (!stat)
22236
22761
  return null;
@@ -22255,7 +22780,7 @@ class RouteService {
22255
22780
  }
22256
22781
  }
22257
22782
  async hash(root, file) {
22258
- const abs = join16(root, file);
22783
+ const abs = join17(root, file);
22259
22784
  const stat = await this.fs.stat(abs);
22260
22785
  if (!stat)
22261
22786
  return null;
@@ -22299,7 +22824,7 @@ class RouteService {
22299
22824
  }
22300
22825
 
22301
22826
  // ../daemon/src/runtime/bridge-script.ts
22302
- import { existsSync as existsSync19, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
22827
+ import { existsSync as existsSync20, readFileSync as readFileSync12, statSync as statSync5 } from "fs";
22303
22828
  var LEARNED_MAX = 8;
22304
22829
 
22305
22830
  class BridgeScript {
@@ -22329,7 +22854,7 @@ class BridgeScript {
22329
22854
  this.cache = null;
22330
22855
  }
22331
22856
  get() {
22332
- if (!existsSync19(this.path))
22857
+ if (!existsSync20(this.path))
22333
22858
  return "// toyon bridge not built";
22334
22859
  const s = statSync5(this.path);
22335
22860
  const stamp = `${s.mtimeMs}:${s.size}`;
@@ -22355,70 +22880,6 @@ function servedLocally(origin) {
22355
22880
  return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]" || h.endsWith(".localhost");
22356
22881
  }
22357
22882
 
22358
- // ../daemon/src/runtime/memory.ts
22359
- var MAC_PRESSURE_CRITICAL = 4;
22360
- var AVAILABLE_FLOOR = 0.15;
22361
- var unsupportedLogged = false;
22362
- async function memoryTight() {
22363
- const signal = process.platform === "darwin" ? await darwin() : process.platform === "linux" ? await linux() : null;
22364
- if (signal === null && !unsupportedLogged) {
22365
- unsupportedLogged = true;
22366
- log.warn("memory", `no memory pressure reading on ${process.platform}; nothing sleeps for memory`);
22367
- }
22368
- return signal;
22369
- }
22370
- async function darwin() {
22371
- const r = await run("sysctl", ["-n", "kern.memorystatus_vm_pressure_level", "kern.memorystatus_level"], "/");
22372
- if (!r.ok)
22373
- return null;
22374
- const [level, available] = r.out.split(/\s+/).map(Number);
22375
- if (level === undefined || available === undefined || Number.isNaN(level) || Number.isNaN(available))
22376
- return null;
22377
- return darwinSignal(level, available);
22378
- }
22379
- function darwinSignal(level, available) {
22380
- const pressure = level === 1 ? "normal" : level === 2 ? "warn" : level === MAC_PRESSURE_CRITICAL ? "critical" : `level ${level}`;
22381
- return {
22382
- tight: level === MAC_PRESSURE_CRITICAL || available < AVAILABLE_FLOOR * 100,
22383
- why: `memory pressure ${pressure} with ${available}% available`
22384
- };
22385
- }
22386
- async function linux() {
22387
- let text;
22388
- try {
22389
- text = await Bun.file("/proc/meminfo").text();
22390
- } catch {
22391
- return null;
22392
- }
22393
- const field = (name) => {
22394
- const m = text.match(new RegExp(`^${name}:\\s+(\\d+)`, "m"));
22395
- return m ? Number(m[1]) : null;
22396
- };
22397
- const total = field("MemTotal");
22398
- const available = field("MemAvailable");
22399
- if (!total || available === null)
22400
- return null;
22401
- const percent = Math.round(available / total * 100);
22402
- return { tight: available / total < AVAILABLE_FLOOR, why: `${percent}% of memory available` };
22403
- }
22404
- async function sampleCosts(pgids) {
22405
- const costs = new Map;
22406
- if (pgids.length === 0)
22407
- return costs;
22408
- const wanted = new Set(pgids);
22409
- const r = await run("ps", ["-axo", "pgid=,rss="], "/");
22410
- if (!r.ok)
22411
- return costs;
22412
- for (const line of r.out.split(`
22413
- `)) {
22414
- const [pgid, rss] = line.trim().split(/\s+/).map(Number);
22415
- if (pgid === undefined || rss === undefined || !wanted.has(pgid))
22416
- continue;
22417
- costs.set(pgid, (costs.get(pgid) ?? 0) + rss);
22418
- }
22419
- return costs;
22420
- }
22421
-
22422
22883
  // ../daemon/src/runtime/idle.ts
22423
22884
  var LOCAL_SLEEP_MS = 2 * 60 * 60000;
22424
22885
  var CLOUD_SLEEP_MS = 5 * 60000;
@@ -22441,6 +22902,7 @@ class IdlePolicy {
22441
22902
  viewing = new Map;
22442
22903
  queue = [];
22443
22904
  draining = false;
22905
+ drained = [];
22444
22906
  sleepMs;
22445
22907
  now;
22446
22908
  constructor(d) {
@@ -22534,6 +22996,9 @@ class IdlePolicy {
22534
22996
  const wt = this.d.state.worktree(id);
22535
22997
  return !!wt && this.now() - (wt.viewedAt ?? 0) < (this.sleepMs ?? LOCAL_SLEEP_MS);
22536
22998
  }
22999
+ isShown(id) {
23000
+ return (this.entries.get(id)?.viewers.size ?? 0) > 0;
23001
+ }
22537
23002
  isViewed(id) {
22538
23003
  if ((this.entries.get(id)?.viewers.size ?? 0) > 0)
22539
23004
  return true;
@@ -22556,7 +23021,8 @@ class IdlePolicy {
22556
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];
22557
23022
  if (!pick)
22558
23023
  return;
22559
- 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");
22560
23026
  }
22561
23027
  async sampleCosts() {
22562
23028
  this.recordViewed();
@@ -22654,13 +23120,12 @@ class IdlePolicy {
22654
23120
  this.arm(id, this.sleepMs - idle);
22655
23121
  return;
22656
23122
  }
22657
- this.sleep(id, `idle ${humanMs(this.sleepMs)}`);
23123
+ this.sleep(id, `nobody looked for ${humanMs(this.sleepMs)}`);
22658
23124
  }
22659
23125
  sleep(id, why) {
22660
23126
  this.disarm(id);
22661
23127
  this.entry(id).costKb = undefined;
22662
- log.info(id, `asleep: ${why}`);
22663
- fireAndForget(id, this.d.runtime.sleep(id), "sleep");
23128
+ fireAndForget(id, this.d.runtime.sleep(id, why), "sleep");
22664
23129
  }
22665
23130
  enqueue(key, run) {
22666
23131
  if (this.queue.some((q) => q.key === key))
@@ -22676,15 +23141,22 @@ class IdlePolicy {
22676
23141
  await next.run();
22677
23142
  } finally {
22678
23143
  this.draining = false;
23144
+ for (const r of this.drained.splice(0))
23145
+ r();
22679
23146
  }
22680
23147
  }
23148
+ settled() {
23149
+ if (!this.draining)
23150
+ return Promise.resolve();
23151
+ return new Promise((r) => this.drained.push(r));
23152
+ }
22681
23153
  }
22682
23154
  function humanMs(ms) {
22683
23155
  if (ms >= 60 * 60000 && ms % (60 * 60000) === 0)
22684
- return `${ms / (60 * 60000)}h`;
23156
+ return `${ms / (60 * 60000)} h`;
22685
23157
  if (ms >= 60000)
22686
23158
  return `${Math.round(ms / 60000)} min`;
22687
- return `${Math.round(ms / 1000)}s`;
23159
+ return `${Math.round(ms / 1000)} s`;
22688
23160
  }
22689
23161
 
22690
23162
  // ../daemon/src/server/ws.ts
@@ -22711,47 +23183,47 @@ class PairCodes {
22711
23183
  }
22712
23184
 
22713
23185
  // ../daemon/src/agent/config.ts
22714
- import { existsSync as existsSync20, readFileSync as readFileSync13 } from "fs";
23186
+ import { existsSync as existsSync21, readFileSync as readFileSync13 } from "fs";
22715
23187
  import { homedir as homedir8 } from "os";
22716
- import { join as join17 } from "path";
23188
+ import { join as join18 } from "path";
22717
23189
  function agentConfigFiles(spec, repoPath, home = homedir8()) {
22718
23190
  const file = (id, label, path) => ({
22719
23191
  id,
22720
23192
  label,
22721
23193
  path,
22722
- exists: existsSync20(path)
23194
+ exists: existsSync21(path)
22723
23195
  });
22724
23196
  if (spec.id === "claude") {
22725
23197
  return [
22726
- file("user-config", "user config (MCP servers, projects)", join17(home, ".claude.json")),
22727
- file("user-settings", "user settings (permissions, hooks)", join17(home, ".claude", "settings.json")),
22728
- ...repoPath ? [file("project-mcp", "project MCP servers", join17(repoPath, ".mcp.json"))] : [],
22729
- ...repoPath ? [file("project-settings", "project settings", join17(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"))] : []
22730
23202
  ];
22731
23203
  }
22732
23204
  if (spec.id === "codex")
22733
- return [file("user-config", "config", join17(home, ".codex", "config.toml"))];
23205
+ return [file("user-config", "config", join18(home, ".codex", "config.toml"))];
22734
23206
  return [];
22735
23207
  }
22736
23208
  function describeAgentConfig(spec, repoPath, home = homedir8()) {
22737
23209
  const files = agentConfigFiles(spec, repoPath, home);
22738
23210
  const servers = [];
22739
23211
  if (spec.id === "claude") {
22740
- const user = readJson(join17(home, ".claude.json"));
23212
+ const user = readJson(join18(home, ".claude.json"));
22741
23213
  servers.push(...serversOf(user?.mcpServers, "user"));
22742
23214
  if (repoPath) {
22743
23215
  const projects = user?.projects;
22744
23216
  const mine = projects?.[repoPath];
22745
23217
  servers.push(...serversOf(mine?.mcpServers, "local"));
22746
- servers.push(...serversOf(readJson(join17(repoPath, ".mcp.json"))?.mcpServers, "project"));
23218
+ servers.push(...serversOf(readJson(join18(repoPath, ".mcp.json"))?.mcpServers, "project"));
22747
23219
  }
22748
23220
  } else if (spec.id === "codex") {
22749
- servers.push(...codexServers(join17(home, ".codex", "config.toml")));
23221
+ servers.push(...codexServers(join18(home, ".codex", "config.toml")));
22750
23222
  }
22751
23223
  return { agent: spec.id, files, servers };
22752
23224
  }
22753
23225
  function readJson(path) {
22754
- if (!existsSync20(path))
23226
+ if (!existsSync21(path))
22755
23227
  return null;
22756
23228
  try {
22757
23229
  const v = JSON.parse(readFileSync13(path, "utf8"));
@@ -22772,7 +23244,7 @@ function serversOf(raw, scope) {
22772
23244
  });
22773
23245
  }
22774
23246
  function codexServers(path) {
22775
- if (!existsSync20(path))
23247
+ if (!existsSync21(path))
22776
23248
  return [];
22777
23249
  const out = [];
22778
23250
  let current = null;
@@ -23368,8 +23840,11 @@ var handlers = {
23368
23840
  unsubscribe(msg, ctx) {
23369
23841
  ctx.unsubscribe(msg.worktreeId);
23370
23842
  },
23371
- view(msg, ctx) {
23843
+ view(msg, ctx, s) {
23372
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");
23373
23848
  },
23374
23849
  seen(msg, _ctx, s) {
23375
23850
  s.turns.markSeen(msg.worktreeId);
@@ -23379,6 +23854,7 @@ var handlers = {
23379
23854
  },
23380
23855
  "set-draft"(msg, _ctx, s) {
23381
23856
  s.drafts.set(msg.boxId, msg.text, msg.clientId);
23857
+ s.worktrees.spare.typed(msg.boxId, msg.text);
23382
23858
  },
23383
23859
  "refresh-git"(msg, _ctx, s) {
23384
23860
  s.worktrees.recount(msg.repoId);
@@ -23403,6 +23879,7 @@ var handlers = {
23403
23879
  async "create-worktree"(msg, _ctx, s) {
23404
23880
  await s.worktrees.create(msg.repoId, msg.prompt, {
23405
23881
  createdBy: msg.clientId,
23882
+ worktreeId: msg.worktreeId,
23406
23883
  variant: msg.variant,
23407
23884
  context: msg.context,
23408
23885
  attachments: msg.attachments,
@@ -23526,6 +24003,9 @@ var handlers = {
23526
24003
  "update-now"(_msg, _ctx, s) {
23527
24004
  return s.update.updateNow();
23528
24005
  },
24006
+ "check-update"(_msg, _ctx, s) {
24007
+ return s.update.checkNow();
24008
+ },
23529
24009
  async commit(msg, ctx, s) {
23530
24010
  const result = await s.worktrees.commit(msg.worktreeId, msg.message);
23531
24011
  await notify(s, ctx, msg.worktreeId, shipped(msg.worktreeId, result.ok, result.message));
@@ -23580,6 +24060,10 @@ var handlers = {
23580
24060
  async "discard-file"(msg, _ctx, s) {
23581
24061
  await s.files.discard(msg.worktreeId, msg.path);
23582
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);
23583
24067
  },
23584
24068
  async "write-file"(msg, ctx, s) {
23585
24069
  const head = { t: "file-written", worktreeId: msg.worktreeId, path: msg.path, seq: msg.seq };
@@ -23762,8 +24246,8 @@ async function dispatch(msg, ctx, s) {
23762
24246
  }
23763
24247
 
23764
24248
  // ../daemon/src/server/http.ts
23765
- import { existsSync as existsSync21 } from "fs";
23766
- import { join as join18 } from "path";
24249
+ import { existsSync as existsSync22 } from "fs";
24250
+ import { join as join19 } from "path";
23767
24251
  var IMMUTABLE = "private, max-age=31536000, immutable";
23768
24252
  var NO_STORE = "no-store";
23769
24253
  function createFetch(opts) {
@@ -23833,10 +24317,12 @@ function createFetch(opts) {
23833
24317
  headers["set-cookie"] = grantCookie(grant, remote.host);
23834
24318
  return Response.json(await opts.bootstrap(), { headers });
23835
24319
  }
23836
- if (url.pathname === "/restart" && req.method === "POST") {
24320
+ if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
23837
24321
  if (!sameSecret(url.searchParams.get("token"), opts.token))
23838
24322
  return new Response("unauthorized", { status: 401 });
23839
- 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));
23840
24326
  return refused ? new Response(refused, { status: 409 }) : new Response(null, { status: 202 });
23841
24327
  }
23842
24328
  if (url.pathname === "/pair" && req.method === "POST") {
@@ -23885,7 +24371,7 @@ function createFetch(opts) {
23885
24371
  return new Response("unauthorized", { status: 401 });
23886
24372
  const [worktreeId, file, extra] = url.pathname.slice("/attachments/".length).split("/");
23887
24373
  const places = worktreeId && file && !extra ? [opts.attachments.fileFor(worktreeId, file), opts.archivedAttachment(worktreeId, file)] : [];
23888
- const path = places.find((p) => !!p && existsSync21(p));
24374
+ const path = places.find((p) => !!p && existsSync22(p));
23889
24375
  if (!path)
23890
24376
  return new Response("not found", { status: 404 });
23891
24377
  return new Response(Bun.file(path), { headers: { "cache-control": IMMUTABLE } });
@@ -23906,15 +24392,15 @@ function createFetch(opts) {
23906
24392
  });
23907
24393
  }
23908
24394
  const rel = url.pathname === "/" ? "/index.html" : url.pathname;
23909
- const file = join18(opts.shellDist, rel.replaceAll("..", ""));
24395
+ const file = join19(opts.shellDist, rel.replaceAll("..", ""));
23910
24396
  const hashed = rel.startsWith("/assets/");
23911
- if (existsSync21(file) && Bun.file(file).size > 0) {
24397
+ if (existsSync22(file) && Bun.file(file).size > 0) {
23912
24398
  return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE } });
23913
24399
  }
23914
24400
  if (hashed)
23915
24401
  return new Response("not found", { status: 404 });
23916
- const index = join18(opts.shellDist, "index.html");
23917
- if (existsSync21(index))
24402
+ const index = join19(opts.shellDist, "index.html");
24403
+ if (existsSync22(index))
23918
24404
  return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE } });
23919
24405
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
23920
24406
  };
@@ -24026,7 +24512,7 @@ function startServer(opts) {
24026
24512
  fireAndForget("ws", (async () => {
24027
24513
  do {
24028
24514
  statusesDirty = false;
24029
- 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() });
24030
24516
  } while (statusesDirty);
24031
24517
  })().finally(() => {
24032
24518
  statusesInFlight = false;
@@ -24132,10 +24618,11 @@ function startServer(opts) {
24132
24618
  return {
24133
24619
  t: "hello",
24134
24620
  version,
24621
+ install: s.update.install(),
24135
24622
  protocol: PROTOCOL_VERSION,
24136
24623
  repos: s.state.repos,
24137
24624
  rows: await s.worktrees.rows({ quick: true }),
24138
- spares: s.worktrees.spares(),
24625
+ trunks: await s.worktrees.trunks({ quick: true }),
24139
24626
  themes: s.themes.themes,
24140
24627
  themePrefs: s.themes.prefs,
24141
24628
  agents: agentInfos(),
@@ -24170,7 +24657,8 @@ function startServer(opts) {
24170
24657
  remote: opts.remote,
24171
24658
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
24172
24659
  bootstrap: helloFrame,
24173
- restart: () => s.restarter.request(),
24660
+ restart: (now) => s.restarter.request({ now }),
24661
+ restartWait: () => ({ waiting: s.restarter.waitingOn(), asking: s.restarter.asking() }),
24174
24662
  pair: new PairCodes,
24175
24663
  onPaired: () => {
24176
24664
  s.state.notePaired();
@@ -24282,16 +24770,16 @@ function startServer(opts) {
24282
24770
  }
24283
24771
 
24284
24772
  // ../daemon/src/themes/store.ts
24285
- import { existsSync as existsSync22, readdirSync as readdirSync5, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync8 } from "fs";
24773
+ import { existsSync as existsSync23, readdirSync as readdirSync6, readFileSync as readFileSync14, statSync as statSync6, writeFileSync as writeFileSync8 } from "fs";
24286
24774
  import { homedir as homedir10 } from "os";
24287
- import { basename as basename4, dirname as dirname7, join as join19, resolve as resolve8 } from "path";
24775
+ import { basename as basename4, dirname as dirname7, join as join20, resolve as resolve8 } from "path";
24288
24776
  var home = homedir10();
24289
24777
  var defaultExtensionDirs = [
24290
- join19(home, ".vscode", "extensions"),
24291
- join19(home, ".vscode-insiders", "extensions"),
24292
- join19(home, ".vscode-oss", "extensions"),
24293
- join19(home, ".cursor", "extensions"),
24294
- join19(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"),
24295
24783
  "/Applications/Visual Studio Code.app/Contents/Resources/app/extensions",
24296
24784
  "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/extensions",
24297
24785
  "/Applications/Cursor.app/Contents/Resources/app/extensions",
@@ -24375,7 +24863,7 @@ class ThemeStore {
24375
24863
  name: json?.name ?? base,
24376
24864
  source: "file"
24377
24865
  });
24378
- writeFileSync8(join19(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
24866
+ writeFileSync8(join20(this.themesDir, `${base}.json`), `${JSON.stringify(theme, null, 2)}
24379
24867
  `);
24380
24868
  this.load();
24381
24869
  return theme;
@@ -24404,13 +24892,13 @@ function readVscodeTheme(file, depth = 0) {
24404
24892
  return t;
24405
24893
  }
24406
24894
  function loadDir(dir) {
24407
- if (!existsSync22(dir))
24895
+ if (!existsSync23(dir))
24408
24896
  return [];
24409
24897
  const out = [];
24410
- for (const f of readdirSync5(dir)) {
24898
+ for (const f of readdirSync6(dir)) {
24411
24899
  if (!/\.jsonc?$/i.test(f))
24412
24900
  continue;
24413
- const file = join19(dir, f);
24901
+ const file = join20(dir, f);
24414
24902
  const id = `file:${slug(f.replace(/\.(json|jsonc)$/i, ""))}`;
24415
24903
  try {
24416
24904
  const json = readJsonc(file);
@@ -24426,20 +24914,20 @@ function loadDir(dir) {
24426
24914
  return out;
24427
24915
  }
24428
24916
  function discover(dir) {
24429
- if (!existsSync22(dir))
24917
+ if (!existsSync23(dir))
24430
24918
  return [];
24431
24919
  const out = [];
24432
24920
  let entries;
24433
24921
  try {
24434
- entries = readdirSync5(dir);
24922
+ entries = readdirSync6(dir);
24435
24923
  } catch {
24436
24924
  return out;
24437
24925
  }
24438
24926
  for (const name of entries) {
24439
- const extDir = join19(dir, name);
24440
- const pkgFile = join19(extDir, "package.json");
24927
+ const extDir = join20(dir, name);
24928
+ const pkgFile = join20(extDir, "package.json");
24441
24929
  try {
24442
- if (!statSync6(extDir).isDirectory() || !existsSync22(pkgFile))
24930
+ if (!statSync6(extDir).isDirectory() || !existsSync23(pkgFile))
24443
24931
  continue;
24444
24932
  const pkg = JSON.parse(readFileSync14(pkgFile, "utf8"));
24445
24933
  const themes = pkg.contributes?.themes;
@@ -24447,8 +24935,8 @@ function discover(dir) {
24447
24935
  continue;
24448
24936
  const ext = `${pkg.publisher ?? "local"}.${pkg.name ?? name}`;
24449
24937
  let nls = {};
24450
- const nlsFile = join19(extDir, "package.nls.json");
24451
- if (existsSync22(nlsFile)) {
24938
+ const nlsFile = join20(extDir, "package.nls.json");
24939
+ if (existsSync23(nlsFile)) {
24452
24940
  try {
24453
24941
  nls = JSON.parse(readFileSync14(nlsFile, "utf8"));
24454
24942
  } catch {}
@@ -24589,6 +25077,21 @@ class UpdateService {
24589
25077
  this.wanted = true;
24590
25078
  await this.proceed();
24591
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
+ }
24592
25095
  get() {
24593
25096
  const restarting = this.d.restarter.waitingOn() ?? (this.wanted && !this.installing ? this.d.restarter.working() : null);
24594
25097
  if (this.installed === null && this.latest === null && !this.installing && this.failed === null && restarting === null) {
@@ -24809,14 +25312,22 @@ var DIFF_CHARS = 2000;
24809
25312
  class LandingService {
24810
25313
  d;
24811
25314
  judging = new Map;
25315
+ busy = new Set;
24812
25316
  constructor(d) {
24813
25317
  this.d = d;
24814
25318
  d.hub.on("turnSettled", (id, turn) => fireAndForget(id, this.settle(id, turn), "landing verdict"));
24815
25319
  d.hub.on("agentStatus", (id, status) => {
25320
+ this.note(id, status);
24816
25321
  if (status === "working")
24817
25322
  this.clear(id);
24818
25323
  });
24819
25324
  }
25325
+ note(worktreeId, status) {
25326
+ if (status === "working" || status === "waiting")
25327
+ this.busy.add(worktreeId);
25328
+ else
25329
+ this.busy.delete(worktreeId);
25330
+ }
24820
25331
  clear(worktreeId) {
24821
25332
  this.judging.delete(worktreeId);
24822
25333
  this.d.worktrees.setLanding(worktreeId, undefined);
@@ -24827,28 +25338,91 @@ class LandingService {
24827
25338
  return;
24828
25339
  if (turn.end !== "done")
24829
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) {
24830
25391
  const repo = this.d.state.requireRepo(wt.repoId);
24831
25392
  const files = await statusFiles(wt.path);
25393
+ if (files.length > 0)
25394
+ return true;
24832
25395
  const { ahead } = await aheadBehind(wt.path, repo.defaultBranch);
24833
- 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))
24834
25402
  return this.clear(worktreeId);
24835
- 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);
24836
25410
  const started = Date.now();
24837
- this.d.worktrees.setLanding(worktreeId, { at: turn.at, check: "pending", ready: false, fingerprint: "" });
24838
- 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);
24839
25413
  let check = "none";
24840
25414
  let checkTail;
24841
25415
  const command = repo.config.check?.trim();
24842
25416
  if (command) {
24843
- const r = await this.d.check(worktreeId, command);
25417
+ const r = await this.d.check(worktreeId, command, { quiet: opts.quiet });
24844
25418
  if (!live())
24845
25419
  return;
24846
25420
  check = r.exit === 0 ? "pass" : "fail";
24847
25421
  if (check === "fail")
24848
- checkTail = tail2(r.text) || `exit ${r.exit}`;
25422
+ checkTail = tail(r.text) || `exit ${r.exit}`;
24849
25423
  }
24850
25424
  let verdict = null;
24851
- if (check !== "fail" && this.d.judge) {
25425
+ if (opts.ask && check !== "fail" && this.d.judge) {
24852
25426
  const entries = this.d.transcript(worktreeId);
24853
25427
  const prompt = landPrompt({
24854
25428
  title: wt.title,
@@ -24866,10 +25440,11 @@ class LandingService {
24866
25440
  return;
24867
25441
  }
24868
25442
  const landing = {
24869
- at: turn.at,
25443
+ at: opts.at,
24870
25444
  check,
24871
25445
  ...checkTail ? { checkTail } : {},
24872
25446
  ready: check !== "fail",
25447
+ ...kept,
24873
25448
  ...verdict && !verdict.ready && verdict.why ? { why: verdict.why } : {},
24874
25449
  ...verdict?.subject ? { subject: verdict.subject } : {},
24875
25450
  ...verdict?.body ? { body: verdict.body } : {},
@@ -24878,11 +25453,14 @@ class LandingService {
24878
25453
  if (!live())
24879
25454
  return;
24880
25455
  this.judging.delete(worktreeId);
24881
- 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`);
24882
25460
  this.d.worktrees.setLanding(worktreeId, landing);
24883
25461
  }
24884
25462
  }
24885
- function tail2(text) {
25463
+ function tail(text) {
24886
25464
  const t = text.replace(/\s+$/, "");
24887
25465
  return t.length > TAIL_CHARS ? t.slice(-TAIL_CHARS) : t;
24888
25466
  }
@@ -25092,11 +25670,8 @@ function discoveredId(path) {
25092
25670
  }
25093
25671
  function knownPaths(known) {
25094
25672
  const paths = new Set;
25095
- for (const wt of known) {
25673
+ for (const wt of known)
25096
25674
  paths.add(canonical(wt.path));
25097
- if (wt.linkPath)
25098
- paths.add(canonical(wt.linkPath));
25099
- }
25100
25675
  return paths;
25101
25676
  }
25102
25677
  function subtractKnown(repoId, listed, known) {
@@ -25164,7 +25739,7 @@ class RefSearch {
25164
25739
  rowIdForPath = (path) => {
25165
25740
  const want = canonical(path);
25166
25741
  for (const wt of this.d.state.worktrees) {
25167
- if (canonical(wt.path) === want || wt.linkPath && canonical(wt.linkPath) === want)
25742
+ if (canonical(wt.path) === want)
25168
25743
  return wt.id;
25169
25744
  }
25170
25745
  return discoveredId(path);
@@ -25274,8 +25849,8 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
25274
25849
  }
25275
25850
 
25276
25851
  // ../daemon/src/worktrees/service.ts
25277
- import { existsSync as existsSync24, lstatSync, readFileSync as readFileSync16, readlinkSync, rmSync as rmSync5, symlinkSync, unlinkSync } from "fs";
25278
- import { basename as basename6, dirname as dirname8, join as join22 } 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";
25279
25854
 
25280
25855
  // ../daemon/src/git/archive.ts
25281
25856
  import { rmSync as rmSync3 } from "fs";
@@ -25345,6 +25920,32 @@ async function landingMark(worktreePath, defaultBr) {
25345
25920
  return null;
25346
25921
  return { base: base.out, tip: tip.out };
25347
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
+ }
25348
25949
  async function dropLandRefs(repoPath, worktreeId) {
25349
25950
  const r = await git(repoPath, "for-each-ref", "--format=%(refname)", `refs/toyon/lands/${worktreeId}/`);
25350
25951
  if (!r.ok || !r.out)
@@ -25360,8 +25961,11 @@ async function commitWorktree(worktreePath, message) {
25360
25961
  return { ok: false, message: "nothing to commit" };
25361
25962
  await git(worktreePath, "add", "-A");
25362
25963
  const c = await git(worktreePath, "commit", "-m", message);
25363
- if (!c.ok)
25364
- 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
+ }
25365
25969
  return { ok: true, message: `committed: ${message}` };
25366
25970
  }
25367
25971
  async function requireClean(worktreePath) {
@@ -25467,21 +26071,37 @@ async function fastForwardMain(repoPath, defaultBr) {
25467
26071
  const f = await run(GIT, ["fetch", "--quiet"], repoPath, NO_PROMPT);
25468
26072
  if (!f.ok)
25469
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
+ }
25470
26081
  const behind = await behindUpstream(repoPath);
25471
- if (behind === null)
25472
- 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
+ }
25473
26085
  if (behind === 0)
25474
- 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
+ }
25475
26094
  const m = await git(repoPath, "merge", "--ff-only", "@{upstream}");
25476
26095
  if (!m.ok) {
25477
26096
  const clobber = /overwritten by merge|would be overwritten/.test(`${m.out}
25478
26097
  ${m.err}`);
25479
- return {
26098
+ return clobber ? {
25480
26099
  ok: false,
25481
- 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`
25482
- };
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` };
25483
26103
  }
25484
- return { ok: true, message: `pulled ${behind} commit(s) from origin` };
26104
+ return { ok: true, moved: true, message: `pulled ${behind} commit(s) from origin` };
25485
26105
  }
25486
26106
  async function pushMain(repoPath, defaultBr) {
25487
26107
  const p = await run(GIT, ["push", "origin", defaultBr], repoPath, NO_PROMPT);
@@ -25503,6 +26123,12 @@ async function mergeFailure(cwd, m, conflictMessage) {
25503
26123
  }
25504
26124
  return { ok: false, message: `merge failed: ${said.slice(0, 300)}` };
25505
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
+ }
25506
26132
  async function openPr(o) {
25507
26133
  const cErr = await requireClean(o.worktreePath);
25508
26134
  if (cErr)
@@ -25513,9 +26139,9 @@ async function openPr(o) {
25513
26139
  const remote = await git(o.worktreePath, "remote", "get-url", "origin");
25514
26140
  if (!remote.ok)
25515
26141
  return { ok: false, message: "no 'origin' remote: add one, or land by merging here" };
25516
- 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);
25517
26143
  if (!push.ok)
25518
- return { ok: false, message: `push failed: ${push.err.slice(0, 300)}` };
26144
+ return push;
25519
26145
  const titled = o.subject ? ["--title", o.subject, "--body", o.body ?? ""] : ["--fill"];
25520
26146
  const created = await gh(["pr", "create", "--head", o.branch, "--base", o.defaultBr, ...titled], o.worktreePath);
25521
26147
  let url = null;
@@ -25565,17 +26191,17 @@ function compareUrl(remoteUrl, base, branch) {
25565
26191
  }
25566
26192
 
25567
26193
  // ../daemon/src/worktrees/archive.ts
25568
- import { existsSync as existsSync23, mkdirSync as mkdirSync7, readdirSync as readdirSync6, readFileSync as readFileSync15, renameSync as renameSync4, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
25569
- import { join as join20 } 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";
25570
26196
  class WorktreeArchive {
25571
26197
  dir;
25572
26198
  records = new Map;
25573
26199
  constructor(dir) {
25574
26200
  this.dir = dir;
25575
26201
  mkdirSync7(dir, { recursive: true });
25576
- for (const id of readdirSync6(dir)) {
26202
+ for (const id of readdirSync7(dir)) {
25577
26203
  const file = this.filesOf(id).record;
25578
- if (!existsSync23(file))
26204
+ if (!existsSync24(file))
25579
26205
  continue;
25580
26206
  try {
25581
26207
  this.records.set(id, JSON.parse(readFileSync15(file, "utf8")));
@@ -25592,8 +26218,10 @@ class WorktreeArchive {
25592
26218
  mkdirSync7(at.dir, { recursive: true });
25593
26219
  move(from.transcript, at.transcript);
25594
26220
  move(from.attachments, at.attachments);
26221
+ move(from.plans, at.plans);
25595
26222
  writeFileSync9(at.record, JSON.stringify(rec, null, 2));
25596
26223
  this.records.set(rec.worktree.id, rec);
26224
+ return { transcript: at.transcript, attachments: at.attachments, plans: at.plans };
25597
26225
  }
25598
26226
  take(id, to) {
25599
26227
  if (!this.records.has(id))
@@ -25601,6 +26229,7 @@ class WorktreeArchive {
25601
26229
  const at = this.filesOf(id);
25602
26230
  move(at.transcript, to.transcript);
25603
26231
  move(at.attachments, to.attachments);
26232
+ move(at.plans, to.plans);
25604
26233
  this.delete(id);
25605
26234
  }
25606
26235
  delete(id) {
@@ -25610,25 +26239,39 @@ class WorktreeArchive {
25610
26239
  this.records.delete(id);
25611
26240
  }
25612
26241
  list(repo) {
25613
- 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));
25614
26243
  }
25615
26244
  chatFiles(id) {
25616
26245
  if (!this.records.has(id))
25617
26246
  return;
25618
26247
  const at = this.filesOf(id);
25619
- 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
+ }
25620
26262
  }
25621
26263
  filesOf(id) {
25622
- const dir = join20(this.dir, id);
26264
+ const dir = join21(this.dir, id);
25623
26265
  return {
25624
26266
  dir,
25625
- record: join20(dir, "record.json"),
25626
- transcript: join20(dir, "transcript.jsonl"),
25627
- attachments: join20(dir, "attachments")
26267
+ record: join21(dir, "record.json"),
26268
+ transcript: join21(dir, "transcript.jsonl"),
26269
+ attachments: join21(dir, "attachments"),
26270
+ plans: join21(dir, "plans")
25628
26271
  };
25629
26272
  }
25630
26273
  }
25631
- function summarize(r, repoId) {
26274
+ function summarize(r, repoId, transcript) {
25632
26275
  return {
25633
26276
  id: r.worktree.id,
25634
26277
  repoId,
@@ -25639,6 +26282,8 @@ function summarize(r, repoId) {
25639
26282
  archivedAt: r.archivedAt,
25640
26283
  ...r.prompt ? { prompt: r.prompt } : {},
25641
26284
  restorable: !!r.kept,
26285
+ transcript,
26286
+ ...r.sessionId ? { sessionId: r.sessionId } : {},
25642
26287
  ...r.kept?.snapshot ? { uncommitted: true, ...r.kept.dirty ? { dirty: r.kept.dirty } : {} } : {},
25643
26288
  ...r.worktree.landed ? { landed: true } : {},
25644
26289
  ...r.cost !== undefined ? { cost: r.cost } : {},
@@ -25646,7 +26291,7 @@ function summarize(r, repoId) {
25646
26291
  };
25647
26292
  }
25648
26293
  function lastUsage(transcript) {
25649
- if (!existsSync23(transcript))
26294
+ if (!existsSync24(transcript))
25650
26295
  return null;
25651
26296
  const lines = readFileSync15(transcript, "utf8").split(`
25652
26297
  `);
@@ -25662,7 +26307,7 @@ function lastUsage(transcript) {
25662
26307
  return null;
25663
26308
  }
25664
26309
  function firstPrompt(transcript) {
25665
- if (!existsSync23(transcript))
26310
+ if (!existsSync24(transcript))
25666
26311
  return;
25667
26312
  for (const line of readFileSync15(transcript, "utf8").split(`
25668
26313
  `)) {
@@ -25677,8 +26322,9 @@ function firstPrompt(transcript) {
25677
26322
  return;
25678
26323
  }
25679
26324
  function move(from, to) {
25680
- if (!existsSync23(from))
26325
+ if (!existsSync24(from))
25681
26326
  return;
26327
+ mkdirSync7(dirname8(to), { recursive: true });
25682
26328
  rmSync4(to, { recursive: true, force: true });
25683
26329
  renameSync4(from, to);
25684
26330
  }
@@ -25741,16 +26387,31 @@ class ArchivedGit {
25741
26387
  }
25742
26388
 
25743
26389
  // ../daemon/src/worktrees/spare.ts
25744
- import { join as join21 } from "path";
26390
+ import { existsSync as existsSync25 } from "fs";
26391
+ import { basename as basename7, join as join22 } from "path";
25745
26392
  class SparePool {
25746
26393
  d;
25747
- spares = new Map;
26394
+ inflight = new Map;
26395
+ refreshing = new Map;
25748
26396
  constructor(d) {
25749
26397
  this.d = d;
25750
26398
  }
26399
+ spareOf(repoId) {
26400
+ return this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
26401
+ }
25751
26402
  current(repoId) {
25752
- const entry = this.spares.get(repoId);
25753
- 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");
25754
26415
  }
25755
26416
  adopt(repoId) {
25756
26417
  const persisted = this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "spare");
@@ -25759,76 +26420,125 @@ class SparePool {
25759
26420
  const spare = persisted[0];
25760
26421
  if (!spare)
25761
26422
  return;
25762
- this.spares.set(repoId, {
25763
- worktreeId: spare.id,
25764
- lockHash: lockfileHash(spare.path),
25765
- refreshing: null,
25766
- ready: true
25767
- });
26423
+ spare.phase ??= "ready";
26424
+ spare.lockfile ??= lockfileHash(spare.path);
25768
26425
  }
25769
26426
  warm(repoId) {
25770
- if (this.spares.has(repoId))
26427
+ const wt = this.spareOf(repoId);
26428
+ if (wt?.phase === "ready")
25771
26429
  fireAndForget(repoId, this.revive(repoId), "spare refresh");
25772
26430
  else
25773
26431
  fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
25774
26432
  }
25775
26433
  async revive(repoId) {
25776
26434
  await this.refresh(repoId);
25777
- const entry = this.spares.get(repoId);
25778
- const wt = entry?.ready ? this.d.state.worktree(entry.worktreeId) : undefined;
25779
- 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)
25780
26437
  return;
25781
26438
  await this.d.runtime.start(wt, this.d.state.requireRepo(repoId));
25782
26439
  }
25783
- async ensure(repoId) {
25784
- if (this.spares.has(repoId))
25785
- 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();
25786
26447
  const repo = this.d.state.requireRepo(repoId);
26448
+ if (have)
26449
+ return this.warmUp(have, repo, {});
25787
26450
  if (repo.needsSetup)
25788
- return;
25789
- const entry = { worktreeId: "", lockHash: "", refreshing: null, ready: false };
25790
- this.spares.set(repoId, entry);
25791
- try {
25792
- const slug = `wt-${shortId().slice(0, 4)}`;
25793
- const wtPath = join21(this.d.paths.worktreesDir, repo.name, slug);
25794
- await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--detach", wtPath, repo.defaultBranch));
25795
- const wt = {
25796
- id: shortId(),
25797
- repoId,
25798
- path: wtPath,
25799
- branch: repo.defaultBranch,
25800
- kind: "spare",
25801
- proxyPort: await allocateProxyPort(),
25802
- title: slug,
25803
- createdAt: Date.now()
25804
- };
25805
- entry.worktreeId = wt.id;
25806
- this.d.state.addWorktree(wt);
25807
- await this.d.setupAndStart(wt, repo);
25808
- entry.lockHash = lockfileHash(wt.path);
25809
- entry.ready = true;
25810
- this.d.hub.emit("worktreesChanged");
25811
- } catch (e) {
25812
- log.warn(repoId, "spare warm-up failed; rolling back", e);
25813
- this.spares.delete(repoId);
25814
- if (entry.worktreeId && this.d.state.worktree(entry.worktreeId)) {
25815
- 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);
25816
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;
25817
26525
  }
26526
+ this.d.state.removeWorktree(wt.id);
26527
+ releasePort(wt.proxyPort);
26528
+ this.d.state.save();
26529
+ this.d.hub.emit("worktreesChanged");
25818
26530
  }
25819
26531
  async refresh(repoId) {
25820
- const entry = this.spares.get(repoId);
25821
- if (!entry?.ready || entry.refreshing)
26532
+ const wt = this.spareOf(repoId);
26533
+ if (wt?.phase !== "ready" || this.refreshing.has(repoId))
25822
26534
  return;
25823
26535
  const repo = this.d.state.requireRepo(repoId);
25824
- const wt = this.d.state.worktree(entry.worktreeId);
25825
- if (wt?.kind !== "spare")
25826
- return;
25827
- entry.refreshing = (async () => {
26536
+ const run = (async () => {
25828
26537
  await withRepoLock(repo.path, () => git(wt.path, "reset", "--hard", repo.defaultBranch));
25829
26538
  const h = lockfileHash(wt.path);
25830
- if (h !== entry.lockHash) {
25831
- entry.lockHash = h;
26539
+ if (h !== wt.lockfile) {
26540
+ wt.lockfile = h;
26541
+ this.d.state.save();
25832
26542
  for (const cmd of repo.config.setup ?? []) {
25833
26543
  const tail = [];
25834
26544
  const code = await runSetup(cmd, wt.path, (line) => {
@@ -25842,70 +26552,161 @@ class SparePool {
25842
26552
  }
25843
26553
  }
25844
26554
  })().finally(() => {
25845
- entry.refreshing = null;
26555
+ this.refreshing.delete(repoId);
25846
26556
  });
25847
- await entry.refreshing;
26557
+ this.refreshing.set(repoId, run);
26558
+ await run;
25848
26559
  }
25849
- async claim(repoId, branch, slug) {
25850
- const entry = this.spares.get(repoId);
25851
- if (!entry?.ready)
26560
+ async claim(repoId, title, worktreeId) {
26561
+ const wt = this.spareOf(repoId);
26562
+ if (!wt)
25852
26563
  return null;
25853
- this.spares.delete(repoId);
25854
- if (entry.refreshing)
25855
- await entry.refreshing;
25856
- const wt = this.d.state.worktree(entry.worktreeId);
25857
- if (wt?.kind !== "spare")
26564
+ if (worktreeId && wt.id !== worktreeId)
25858
26565
  return null;
25859
- const repo = this.d.state.requireRepo(repoId);
25860
- try {
25861
- await withRepoLock(repo.path, () => gitOrThrow(wt.path, "switch", "-c", branch));
25862
- } catch (e) {
25863
- this.spares.set(repoId, { ...entry, ready: true });
25864
- 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;
25865
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));
25866
26580
  wt.kind = "worktree";
26581
+ delete wt.phase;
26582
+ delete wt.lockfile;
25867
26583
  wt.branch = branch;
25868
- wt.title = slug;
26584
+ wt.title = title;
26585
+ wt.unnamed = true;
25869
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);
25870
26590
  this.d.state.save();
25871
- fireAndForget(wt.id, this.d.runtime.wake(wt.id), "wake on claim");
25872
- fireAndForget(repoId, this.ensure(repoId), "spare warm-up");
26591
+ if (next)
26592
+ fireAndForget(repoId, this.warmUp(next, repo, { after: wt.id }), "spare reserve");
25873
26593
  return wt;
25874
26594
  }
25875
26595
  }
25876
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
+
25877
26688
  // ../daemon/src/worktrees/turns.ts
25878
26689
  var TURN_HOLD = "turn";
25879
- var RECAP_DELAY_MS = Number(process.env.TOYON_RECAP_DELAY_MS) || 2 * 60000;
25880
26690
 
25881
26691
  class TurnService {
25882
26692
  d;
25883
26693
  lastStatus = new Map;
25884
26694
  stoppedByHand = new Set;
25885
- timers = new Map;
25886
- summarized = new Map;
25887
26695
  constructor(d) {
25888
26696
  this.d = d;
25889
26697
  this.boot();
25890
26698
  d.hub.on("agentStatus", (worktreeId, status) => this.onStatus(worktreeId, status));
25891
26699
  }
25892
26700
  boot() {
25893
- const now = Date.now();
25894
26701
  let changed = false;
25895
26702
  for (const wt of this.d.state.worktrees) {
25896
26703
  const turn = wt.lastTurn;
25897
- if (!turn)
26704
+ if (turn?.end !== "asking")
25898
26705
  continue;
25899
- if (turn.end === "asking") {
25900
- const { ask: _expired, ...facts } = turn.facts;
25901
- turn.end = "stopped";
25902
- turn.facts = facts;
25903
- changed = true;
25904
- }
25905
- if (!turn.recap && (wt.seenAt ?? 0) < turn.at) {
25906
- turn.recap = { at: now };
25907
- changed = true;
25908
- }
26706
+ const { ask: _expired, ...facts } = turn.facts;
26707
+ turn.end = "stopped";
26708
+ turn.facts = facts;
26709
+ changed = true;
25909
26710
  }
25910
26711
  if (changed)
25911
26712
  this.d.state.save();
@@ -25913,10 +26714,8 @@ class TurnService {
25913
26714
  onStatus(worktreeId, status) {
25914
26715
  const prev = this.lastStatus.get(worktreeId) ?? "idle";
25915
26716
  this.lastStatus.set(worktreeId, status);
25916
- if (status === "working") {
25917
- this.disarm(worktreeId);
26717
+ if (status === "working")
25918
26718
  this.d.hold?.(worktreeId, TURN_HOLD);
25919
- }
25920
26719
  const edge = stopOf(prev, status);
25921
26720
  if (!edge)
25922
26721
  return;
@@ -25931,64 +26730,13 @@ class TurnService {
25931
26730
  wt.lastTurn = { at, end, facts: factsOf(turns, end, end === "asking" ? openAskOf(entries) : undefined) };
25932
26731
  if (this.stoppedByHand.delete(worktreeId) && end === "stopped")
25933
26732
  wt.seenAt = at;
25934
- this.disarm(worktreeId);
25935
- if ((wt.seenAt ?? 0) < at)
25936
- this.arm(worktreeId, at);
25937
26733
  this.d.state.save();
25938
26734
  this.d.hub.emit("turnSettled", worktreeId, wt.lastTurn);
25939
26735
  }
25940
- arm(worktreeId, at) {
25941
- const t = setTimeout(() => this.due(worktreeId, at), this.d.delayMs ?? RECAP_DELAY_MS);
25942
- t.unref?.();
25943
- this.timers.set(worktreeId, t);
25944
- }
25945
- disarm(worktreeId) {
25946
- const t = this.timers.get(worktreeId);
25947
- if (!t)
25948
- return;
25949
- clearTimeout(t);
25950
- this.timers.delete(worktreeId);
25951
- }
25952
- due(worktreeId, at) {
25953
- this.timers.delete(worktreeId);
25954
- const wt = this.d.state.worktree(worktreeId);
25955
- const turn = wt?.lastTurn;
25956
- if (!wt || !turn || turn.at !== at || (wt.seenAt ?? 0) >= at)
25957
- return;
25958
- turn.recap = { at: Date.now() };
25959
- this.d.state.save();
25960
- this.d.hub.emit("worktreesChanged");
25961
- const seenAt = wt.seenAt ?? 0;
25962
- const summarize = this.d.summarize;
25963
- if (!summarize || turn.facts.auth)
25964
- return;
25965
- if (this.summarized.get(worktreeId) === seenAt)
25966
- return;
25967
- this.summarized.set(worktreeId, seenAt);
25968
- const entries = this.d.transcript(worktreeId);
25969
- const prompt = recapPrompt({
25970
- title: wt.title,
25971
- firstAsk: firstAskOf(entries),
25972
- turns: turnsSince(entries, seenAt),
25973
- end: turn.end,
25974
- facts: turn.facts
25975
- });
25976
- fireAndForget(worktreeId, this.write(worktreeId, at, summarize(wt, prompt)), "recap summary");
25977
- }
25978
- async write(worktreeId, at, sentence) {
25979
- const text = await sentence;
25980
- const turn = this.d.state.worktree(worktreeId)?.lastTurn;
25981
- if (!text || turn?.at !== at || !turn.recap)
25982
- return;
25983
- turn.recap.text = text;
25984
- this.d.state.save();
25985
- this.d.hub.emit("worktreesChanged");
25986
- }
25987
26736
  stoppedByPerson(worktreeId) {
25988
26737
  this.stoppedByHand.add(worktreeId);
25989
26738
  }
25990
26739
  markSeen(worktreeId) {
25991
- this.disarm(worktreeId);
25992
26740
  const wt = this.d.state.worktree(worktreeId);
25993
26741
  if (!wt)
25994
26742
  return;
@@ -26035,21 +26783,27 @@ function withCarry(context, c) {
26035
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.`;
26036
26784
  return [...context ?? [], line];
26037
26785
  }
26038
- var FETCH_EVERY_MS = 5 * 60000;
26039
26786
 
26040
26787
  class WorktreeService {
26041
26788
  d;
26042
26789
  spare;
26790
+ trunk;
26043
26791
  archive;
26044
26792
  archiving = new Map;
26045
26793
  going = new Set;
26046
26794
  countsCache = new Map;
26795
+ handChecked = new Map;
26047
26796
  discoverCache = new Map;
26048
26797
  usage = new Map;
26049
- lastFetch = new Map;
26050
26798
  constructor(d) {
26051
26799
  this.d = d;
26052
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
+ });
26053
26807
  this.spare = new SparePool({
26054
26808
  state: d.state,
26055
26809
  hub: d.hub,
@@ -26058,10 +26812,28 @@ class WorktreeService {
26058
26812
  setupAndStart: (wt, repo) => this.setupAndStart(wt, repo),
26059
26813
  discard: async (id) => {
26060
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
+ }
26061
26835
  }
26062
26836
  });
26063
- for (const wt of d.state.worktrees)
26064
- this.refreshLink(wt);
26065
26837
  d.hub.on("agent", (worktreeId, _seq, event) => {
26066
26838
  if (event.type !== "usage")
26067
26839
  return;
@@ -26086,18 +26858,15 @@ class WorktreeService {
26086
26858
  const agent = this.d.agents.require(opts.agent ?? this.d.state.defaultAgent ?? DEFAULT_AGENT_ID).id;
26087
26859
  const profile = this.checkProfile(repo, opts.profile);
26088
26860
  const task = taskText(prompt, attachments);
26089
- let slug = variant ? `${slugify2(task, false)}-v${variant.index}` : slugify2(task);
26090
- if (variant && (await git(repo.path, "show-ref", "--verify", `refs/heads/toyon/${slug}`)).ok) {
26091
- slug = `${slug}-${shortId().slice(0, 3)}`;
26092
- }
26093
- const branch = `toyon/${slug}`;
26861
+ const base = titleFrom(task);
26862
+ const title = variant ? `${base} v${variant.index}` : base;
26094
26863
  if (opts.carry && variant && variant.of > 1) {
26095
26864
  throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
26096
26865
  }
26097
26866
  const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
26098
26867
 
26099
26868
  `) : prompt;
26100
- const claimed = await this.spare.claim(repoId, branch, slug);
26869
+ const claimed = await this.spare.claim(repoId, title, opts.worktreeId);
26101
26870
  if (claimed) {
26102
26871
  if (variant)
26103
26872
  claimed.variant = variant;
@@ -26111,7 +26880,6 @@ class WorktreeService {
26111
26880
  claimed.model = opts.model;
26112
26881
  if (opts.effort)
26113
26882
  claimed.effort = opts.effort;
26114
- this.refreshLink(claimed);
26115
26883
  if (profile !== undefined && profile !== resolveRun(repo, claimed).profile) {
26116
26884
  claimed.profile = profile;
26117
26885
  this.restartProcs(claimed, repo);
@@ -26119,22 +26887,31 @@ class WorktreeService {
26119
26887
  const carried = await this.carryMain(repo, claimed, !!opts.carry);
26120
26888
  this.d.state.save();
26121
26889
  this.d.hub.emit("worktreesChanged");
26122
- 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 });
26123
26894
  this.scheduleNaming(claimed, task, variant);
26124
26895
  if (carried.unmoved)
26125
26896
  throw new UserError(carried.unmoved);
26126
26897
  return claimed;
26127
26898
  }
26128
- const wtPath = join22(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}`;
26129
26905
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "-b", branch, wtPath, repo.defaultBranch));
26130
26906
  const wt = {
26131
- id: shortId(),
26907
+ id,
26132
26908
  repoId,
26133
26909
  path: wtPath,
26134
26910
  branch,
26135
26911
  kind: "worktree",
26136
26912
  proxyPort: await allocateProxyPort(),
26137
- title: slug,
26913
+ title,
26914
+ unnamed: true,
26138
26915
  createdAt: Date.now(),
26139
26916
  promptedAt: Date.now(),
26140
26917
  agent,
@@ -26154,9 +26931,20 @@ class WorktreeService {
26154
26931
  return wt;
26155
26932
  }
26156
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) {
26157
26943
  const main = repo.defaultBranch;
26158
- if (!move)
26159
- 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
+ }
26160
26948
  let count = 0;
26161
26949
  const unmoved = await withRepoLock(repo.path, async () => {
26162
26950
  count = (await statusFiles(repo.path)).length;
@@ -26178,7 +26966,7 @@ class WorktreeService {
26178
26966
  };
26179
26967
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
26180
26968
  const restored = (carried.ok ? carried.out.split(`
26181
- `).filter(Boolean) : []).filter((p) => !existsSync24(join22(wt.path, p)));
26969
+ `).filter(Boolean) : []).filter((p) => !existsSync26(join23(wt.path, p)));
26182
26970
  const applied = await git(wt.path, "stash", "apply", sha);
26183
26971
  if (applied.ok) {
26184
26972
  const ref = await entry();
@@ -26195,7 +26983,7 @@ class WorktreeService {
26195
26983
  const back = ref ? await git(repo.path, "stash", "pop", ref) : null;
26196
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}"`;
26197
26985
  });
26198
- const mainWt = this.d.state.worktrees.find((w) => w.repoId === repo.id && w.kind === "main");
26986
+ const mainWt = this.mainOf(repo.id);
26199
26987
  if (mainWt)
26200
26988
  this.countsCache.delete(mainWt.id);
26201
26989
  return { branch: main, moved: count > 0 && !unmoved, count, unmoved };
@@ -26213,10 +27001,10 @@ class WorktreeService {
26213
27001
  throw new UserError("that is not a PR number");
26214
27002
  const branch = kind === "pr" ? `pr/${number}` : ref;
26215
27003
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
26216
- let slug = title;
26217
- if (existsSync24(join22(this.d.paths.worktreesDir, repo.name, slug)))
27004
+ let slug = branchSlug(title) || "branch";
27005
+ if (existsSync26(join23(this.d.paths.worktreesDir, repo.name, slug)))
26218
27006
  slug = `${slug}-${shortId().slice(0, 4)}`;
26219
- const wtPath = join22(this.d.paths.worktreesDir, repo.name, slug);
27007
+ const wtPath = join23(this.d.paths.worktreesDir, repo.name, slug);
26220
27008
  await withRepoLock(repo.path, async () => {
26221
27009
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
26222
27010
  if (out)
@@ -26364,7 +27152,7 @@ class WorktreeService {
26364
27152
  if (!name)
26365
27153
  return;
26366
27154
  for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
26367
- 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) => {
26368
27156
  log.warn(sibling.id, "variant rename failed", e);
26369
27157
  });
26370
27158
  }
@@ -26407,7 +27195,14 @@ class WorktreeService {
26407
27195
  const k = archive ? await this.keep(repo, wt) : null;
26408
27196
  if (archive && !k)
26409
27197
  throw new UserError(`could not keep ${wt.title}'s work, so it was left in place`);
26410
- 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
+ }
26411
27206
  if (hasOwnBranch(wt)) {
26412
27207
  const r = await git(repo.path, "branch", "-D", wt.branch);
26413
27208
  if (!r.ok)
@@ -26415,7 +27210,6 @@ class WorktreeService {
26415
27210
  }
26416
27211
  return k;
26417
27212
  });
26418
- this.dropLink(wt);
26419
27213
  const sessionId = this.d.state.session(worktreeId);
26420
27214
  this.d.state.removeWorktree(worktreeId);
26421
27215
  releasePort(wt.proxyPort);
@@ -26431,7 +27225,7 @@ class WorktreeService {
26431
27225
  return archived;
26432
27226
  }
26433
27227
  async keep(repo, wt) {
26434
- const index = join22(this.d.paths.archiveDir, `${wt.id}.index`);
27228
+ const index = join23(this.d.paths.archiveDir, `${wt.id}.index`);
26435
27229
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index);
26436
27230
  if (!kept)
26437
27231
  log.warn(wt.id, "could not keep its git state");
@@ -26439,8 +27233,21 @@ class WorktreeService {
26439
27233
  log.warn(wt.id, "could not keep its uncommitted changes: only its commits are archived");
26440
27234
  return kept;
26441
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
+ }
26442
27249
  archiveChat(wt, repo, kept, sessionId, reason) {
26443
- const files = this.chatFiles(wt.id);
27250
+ const files = { ...this.chatFiles(wt.id), plans: this.plansScratch(wt.id) };
26444
27251
  const prompt = firstPrompt(files.transcript);
26445
27252
  const cost = (this.usage.get(wt.id) ?? lastUsage(files.transcript))?.cost;
26446
27253
  const rec = {
@@ -26453,14 +27260,15 @@ class WorktreeService {
26453
27260
  ...kept ? { kept } : {},
26454
27261
  ...reason ? { auto: reason } : {}
26455
27262
  };
27263
+ let at;
26456
27264
  try {
26457
- this.archive.put(rec, files);
27265
+ at = this.archive.put(rec, files);
26458
27266
  } catch (e) {
26459
27267
  log.warn(wt.id, "could not archive its chat; the files stay where they were", e);
26460
27268
  return null;
26461
27269
  }
26462
27270
  this.d.hub.emit("archiveChanged", repo.id);
26463
- return summarize(rec, repo.id);
27271
+ return summarize(rec, repo.id, at.transcript);
26464
27272
  }
26465
27273
  chatFiles(worktreeId) {
26466
27274
  return {
@@ -26480,7 +27288,6 @@ class WorktreeService {
26480
27288
  async forgetGone(wt) {
26481
27289
  const repo = this.d.state.repo(wt.repoId);
26482
27290
  const sessionId = this.d.state.session(wt.id);
26483
- this.dropLink(wt);
26484
27291
  this.d.state.removeWorktree(wt.id);
26485
27292
  if (!repo || wt.kind !== "worktree") {
26486
27293
  this.deleteChat(wt.id);
@@ -26519,7 +27326,7 @@ class WorktreeService {
26519
27326
  }
26520
27327
  archivedAttachment(archiveId, file) {
26521
27328
  const files = this.archive.chatFiles(archiveId);
26522
- return files && isAttachmentFile(file) ? join22(files.attachments, file) : null;
27329
+ return files && isAttachmentFile(file) ? join23(files.attachments, file) : null;
26523
27330
  }
26524
27331
  archivedGit(archiveId) {
26525
27332
  const rec = this.archive.get(archiveId);
@@ -26540,6 +27347,9 @@ class WorktreeService {
26540
27347
  }
26541
27348
  }
26542
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 };
26543
27353
  const kept = this.archivedGit(archiveId);
26544
27354
  return kept ? kept.file(path, ref) : null;
26545
27355
  }
@@ -26559,7 +27369,7 @@ class WorktreeService {
26559
27369
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
26560
27370
  if (rec.auto)
26561
27371
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
26562
- const path = existsSync24(old.path) ? join22(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;
26563
27373
  const branch = await withRepoLock(repo.path, async () => {
26564
27374
  if (!await commitOf(repo.path, archiveRef(old.id))) {
26565
27375
  throw new UserError(`${old.title}'s kept commits are gone from git`);
@@ -26585,7 +27395,7 @@ class WorktreeService {
26585
27395
  }
26586
27396
  return name;
26587
27397
  });
26588
- const { variant: _variant, linkPath: _linkPath, ...rest } = old;
27398
+ const { variant: _variant, ...rest } = old;
26589
27399
  const wt = {
26590
27400
  ...rest,
26591
27401
  repoId: repo.id,
@@ -26596,7 +27406,7 @@ class WorktreeService {
26596
27406
  ...createdBy ? { createdBy } : {}
26597
27407
  };
26598
27408
  try {
26599
- this.archive.take(old.id, this.chatFiles(old.id));
27409
+ this.archive.take(old.id, { ...this.chatFiles(old.id), plans: join23(path, PLANS_DIR) });
26600
27410
  } catch (e) {
26601
27411
  log.warn(old.id, "could not move its chat back out of the archive", e);
26602
27412
  }
@@ -26606,7 +27416,6 @@ class WorktreeService {
26606
27416
  if (!dropped.ok)
26607
27417
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
26608
27418
  this.countsCache.delete(wt.id);
26609
- this.refreshLink(wt);
26610
27419
  this.launch(wt, repo, repo.path);
26611
27420
  if (message)
26612
27421
  this.d.runtime.ensureAgent(wt).agent.send(message.text, { attachments: message.attachments });
@@ -26619,12 +27428,12 @@ class WorktreeService {
26619
27428
  return;
26620
27429
  const repo = this.repoOf(rec);
26621
27430
  const repoPath = repo?.path ?? rec.repoPath;
26622
- if (rec.kept && existsSync24(repoPath)) {
27431
+ if (rec.kept && existsSync26(repoPath)) {
26623
27432
  const r = await git(repoPath, "update-ref", "-d", archiveRef(archiveId));
26624
27433
  if (!r.ok)
26625
27434
  log.warn(archiveId, `could not drop its archive ref: ${r.err}`);
26626
27435
  }
26627
- if (rec.worktree.lands?.length && existsSync24(repoPath))
27436
+ if (rec.worktree.lands?.length && existsSync26(repoPath))
26628
27437
  await dropLandRefs(repoPath, archiveId);
26629
27438
  this.archive.delete(archiveId);
26630
27439
  this.d.drafts?.drop(archiveId);
@@ -26641,55 +27450,24 @@ class WorktreeService {
26641
27450
  const clean = cleanTitle2(title);
26642
27451
  if (!clean)
26643
27452
  return;
27453
+ const slug = branchSlug(clean);
26644
27454
  await withRepoLock(repo.path, async () => {
26645
- let branch = `toyon/${clean}`;
26646
- if (branch !== wt.branch) {
27455
+ let branch = `toyon/${slug}`;
27456
+ if (slug && branch !== wt.branch) {
26647
27457
  let n = 2;
26648
27458
  while (!(await git(wt.path, "branch", "-m", wt.branch, branch)).ok) {
26649
27459
  if (n > 5)
26650
27460
  return;
26651
- branch = `toyon/${clean}-${n++}`;
27461
+ branch = `toyon/${slug}-${n++}`;
26652
27462
  }
26653
27463
  wt.branch = branch;
26654
27464
  }
26655
27465
  wt.title = clean;
27466
+ delete wt.unnamed;
26656
27467
  });
26657
- this.refreshLink(wt);
26658
27468
  this.d.state.save();
26659
27469
  this.d.hub.emit("worktreesChanged");
26660
27470
  }
26661
- refreshLink(wt) {
26662
- if (!hasOwnBranch(wt))
26663
- return;
26664
- const name = wt.branch.replace(/^toyon\//, "");
26665
- const desired = wt.kind === "worktree" ? join22(dirname8(wt.path), name) : wt.path;
26666
- if (wt.linkPath && wt.linkPath !== desired)
26667
- this.dropLink(wt);
26668
- if (desired === wt.path)
26669
- return;
26670
- try {
26671
- const st = lstatSync(desired, { throwIfNoEntry: false });
26672
- if (st && !(st.isSymbolicLink() && readlinkSync(desired) === wt.path))
26673
- return;
26674
- if (!st)
26675
- symlinkSync(wt.path, desired);
26676
- wt.linkPath = desired;
26677
- } catch (e) {
26678
- log.warn(wt.id, "could not link the title to the directory", e);
26679
- }
26680
- }
26681
- dropLink(wt) {
26682
- const p = wt.linkPath;
26683
- wt.linkPath = undefined;
26684
- if (!p)
26685
- return;
26686
- try {
26687
- if (lstatSync(p, { throwIfNoEntry: false })?.isSymbolicLink())
26688
- unlinkSync(p);
26689
- } catch (e) {
26690
- log.warn(wt.id, "could not remove the title link", e);
26691
- }
26692
- }
26693
27471
  async pickVariant(worktreeId) {
26694
27472
  const wt = this.d.state.worktree(worktreeId);
26695
27473
  if (!wt?.variant)
@@ -26754,18 +27532,19 @@ class WorktreeService {
26754
27532
  this.d.runtime.markSetup(wt.id, false);
26755
27533
  }
26756
27534
  }
26757
- async mirrorBaseIgnore(wt, repo, depsSource) {
26758
- const src = join22(depsSource, ".gitignore");
26759
- const mirror = existsSync24(src) && !existsSync24(join22(wt.path, ".gitignore"));
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;
26760
27539
  const lines = mirror ? readFileSync16(src, "utf8").split(`
26761
27540
  `).filter((l) => l.trim() !== "") : [];
26762
27541
  await excludeBlock(repo.path, BASE_IGNORE, lines);
26763
27542
  }
26764
27543
  async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
26765
- await this.mirrorBaseIgnore(wt, repo, depsSource);
26766
- const srcNm = join22(depsSource, "node_modules");
26767
- const dstNm = join22(wt.path, "node_modules");
26768
- if (existsSync24(srcNm) && !existsSync24(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)) {
26769
27548
  const started = Date.now();
26770
27549
  const attempts = [
26771
27550
  ["clonefile", ["-Rc", srcNm, dstNm]],
@@ -26781,10 +27560,10 @@ class WorktreeService {
26781
27560
  }
26782
27561
  }
26783
27562
  for (const f of LOCAL_CONFIG_FILES) {
26784
- const src = join22(depsSource, f);
26785
- if (!existsSync24(src) || existsSync24(join22(wt.path, f)))
27563
+ const src = join23(depsSource, f);
27564
+ if (!existsSync26(src) || existsSync26(join23(wt.path, f)))
26786
27565
  continue;
26787
- const r = await run("cp", [src, join22(wt.path, f)], wt.path);
27566
+ const r = await run("cp", [src, join23(wt.path, f)], wt.path);
26788
27567
  if (r.ok)
26789
27568
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
26790
27569
  else
@@ -26849,10 +27628,42 @@ ${wt.landing.body}` : s;
26849
27628
  async commitIfDirty(wt, typed) {
26850
27629
  if ((await statusFiles(wt.path)).length === 0)
26851
27630
  return null;
26852
- const result = await commitWorktree(wt.path, this.commitMessage(wt, typed));
26853
- 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 });
26854
27637
  this.headMoved(wt.id);
26855
- 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);
26856
27667
  }
26857
27668
  async land(worktreeId, message) {
26858
27669
  const { wt, repo } = this.landable(worktreeId, "land");
@@ -26863,8 +27674,24 @@ ${wt.landing.body}` : s;
26863
27674
  ${wt.landing.body}` : wt.landing.subject : undefined;
26864
27675
  if (policy.land === "pr") {
26865
27676
  if (wt.pr?.state === "open") {
26866
- const result = await mergePr(wt.path, wt.pr.number, policy.merge);
26867
- 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` } };
26868
27695
  }
26869
27696
  const committed = await this.commitIfDirty(wt, message);
26870
27697
  if (committed && !committed.ok)
@@ -26890,10 +27717,12 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26890
27717
  return { result };
26891
27718
  }
26892
27719
  let mergedHere = false;
27720
+ let committedHere = false;
26893
27721
  const result = await withRepoLock(repo.path, async () => {
26894
27722
  const committed = await this.commitIfDirty(wt, message);
26895
27723
  if (committed && !committed.ok)
26896
27724
  return committed;
27725
+ committedHere = committed !== null;
26897
27726
  if (policy.land === "push") {
26898
27727
  const pulled = await fastForwardMain(repo.path, repo.defaultBranch);
26899
27728
  if (!pulled.ok && !/no upstream/.test(pulled.message))
@@ -26926,8 +27755,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26926
27755
  if (!result.ok)
26927
27756
  return { result };
26928
27757
  const archiveIds = siblingsOf(wt, this.d.state.worktrees).map((w) => w.id);
26929
- const where = policy.land === "push" ? `${repo.defaultBranch}, pushed` : repo.defaultBranch;
26930
- 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 };
26931
27761
  }
26932
27762
  async noteLand(repo, wt, mark) {
26933
27763
  if (!mark)
@@ -26973,6 +27803,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26973
27803
  throw new UserError("that worktree is gone");
26974
27804
  if (r.wt && isMain(r.wt))
26975
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");
26976
27808
  if (!r.branch)
26977
27809
  throw new UserError(`${r.name} is detached: check out a branch in it first`);
26978
27810
  if (r.locked)
@@ -26980,8 +27812,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
26980
27812
  const repo = this.d.state.requireRepo(r.repoId);
26981
27813
  const own = r.wt ? hasOwnBranch(r.wt) : false;
26982
27814
  const result = await withRepoLock(repo.path, () => takeMainIn(r.path, repo.defaultBranch, own));
26983
- if (result.ok)
27815
+ if (result.ok) {
26984
27816
  this.headMoved(worktreeId);
27817
+ if (r.wt)
27818
+ await this.verdictSurvives(r.wt, { dropMessage: false });
27819
+ }
26985
27820
  return { result, defaultBranch: repo.defaultBranch };
26986
27821
  }
26987
27822
  async pull(worktreeId) {
@@ -27001,10 +27836,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27001
27836
  const m = message.trim();
27002
27837
  if (!m)
27003
27838
  throw new UserError("commit message required");
27004
- const result = await commitWorktree(wt.path, m);
27005
- if (result.ok)
27006
- this.headMoved(wt.id);
27007
- return result;
27839
+ return this.commitRecorded(wt, m);
27008
27840
  }
27009
27841
  setEmpty(wt, empty) {
27010
27842
  if (wt.empty === empty)
@@ -27013,23 +27845,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27013
27845
  this.d.state.save();
27014
27846
  this.d.hub.emit("worktreesChanged");
27015
27847
  }
27016
- async mainCounts(path) {
27017
- const behind = await behindUpstream(path);
27018
- if (behind === null)
27019
- return {};
27020
- const last = this.lastFetch.get(path) ?? 0;
27021
- if (Date.now() - last > FETCH_EVERY_MS) {
27022
- this.lastFetch.set(path, Date.now());
27023
- fireAndForget("fetch", run(GIT, ["fetch", "--quiet"], path, NO_PROMPT).then((r) => {
27024
- if (!r.ok) {
27025
- log.warn("fetch", `could not fetch ${path}: ${r.err.slice(0, 200)}`);
27026
- return;
27027
- }
27028
- this.invalidateCounts();
27029
- this.d.hub.emit("worktreesChanged");
27030
- }));
27031
- }
27032
- return { behind };
27848
+ syncTrunk(repoId) {
27849
+ return this.trunk.sync(repoId);
27850
+ }
27851
+ trunks(opts = {}) {
27852
+ return this.trunk.all(opts);
27033
27853
  }
27034
27854
  usageFor(worktreeId) {
27035
27855
  const known = this.usage.get(worktreeId);
@@ -27098,17 +27918,23 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27098
27918
  if (this.going.has(id))
27099
27919
  return cached ?? {};
27100
27920
  try {
27101
- const ab = countable ? await aheadBehind(path, defaultBranch) : await this.mainCounts(path);
27921
+ const ab = countable ? await aheadBehind(path, defaultBranch) : await this.trunk.behind(id, path);
27102
27922
  const dirty = (await statusFiles(path)).length;
27923
+ const unpushed = await this.unpushed(id, path);
27103
27924
  if (this.going.has(id))
27104
27925
  return cached ?? {};
27105
- const fresh = { ...ab, dirty, at: Date.now() };
27926
+ const fresh = { ...ab, ...unpushed === undefined ? {} : { unpushed }, dirty, at: Date.now() };
27106
27927
  this.countsCache.set(id, fresh);
27107
27928
  return fresh;
27108
27929
  } catch {
27109
27930
  return cached ?? {};
27110
27931
  }
27111
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
+ }
27112
27938
  async freshCounts(worktreeId) {
27113
27939
  const wt = this.d.state.worktree(worktreeId);
27114
27940
  const repo = wt && this.d.state.repo(wt.repoId);
@@ -27120,8 +27946,6 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27120
27946
  readable(id) {
27121
27947
  const wt = this.d.state.worktree(id);
27122
27948
  if (wt) {
27123
- if (wt.kind === "spare")
27124
- return null;
27125
27949
  const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
27126
27950
  return { id, repoId: wt.repoId, path: wt.path, name: wt.title, branch: wt.branch, defaultBranch, wt };
27127
27951
  }
@@ -27147,11 +27971,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27147
27971
  return this.archivedStatus(worktreeId);
27148
27972
  try {
27149
27973
  const isMain = r.wt?.kind === "main";
27150
- const [files, counts, head] = await Promise.all([
27974
+ const [files, ab, unpushed, head] = await Promise.all([
27151
27975
  statusFilesWithCounts(r.path),
27152
27976
  isMain ? Promise.resolve({}) : aheadBehind(r.path, r.defaultBranch),
27977
+ this.unpushed(worktreeId, r.path),
27153
27978
  git(r.path, "rev-parse", "HEAD")
27154
27979
  ]);
27980
+ const counts = { ...ab, ...unpushed === undefined ? {} : { unpushed } };
27155
27981
  const ahead = counts.ahead ?? 0;
27156
27982
  const committed = !isMain && ahead > 0 ? await committedFiles(r.path, r.defaultBranch) : undefined;
27157
27983
  if (r.wt?.landed && (files.length > 0 || ahead > 0)) {
@@ -27159,8 +27985,21 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27159
27985
  if (r.wt.pr)
27160
27986
  this.setPr(r.wt.id, undefined);
27161
27987
  }
27162
- if (r.wt?.landing && r.wt.landing.check !== "pending" && r.wt.landing.fingerprint !== await treeFingerprint(r.path))
27163
- 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
+ }
27164
28003
  if (r.wt && isMain)
27165
28004
  this.setEmpty(r.wt, files.length === 0 ? await treeEmpty(r.path) : false);
27166
28005
  if (this.going.has(worktreeId))
@@ -27178,8 +28017,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27178
28017
  return;
27179
28018
  const ahead = main ? prev?.ahead : ab.ahead;
27180
28019
  const behind = main ? prev?.behind : ab.behind;
27181
- this.countsCache.set(id, { ahead, behind, dirty, at: Date.now() });
27182
- 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)
27183
28023
  this.d.hub.emit("worktreesChanged");
27184
28024
  }
27185
28025
  async gitLog(worktreeId) {
@@ -27206,7 +28046,9 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27206
28046
  await this.restore(worktreeId, msg.clientId, { text: msg.text, attachments: msg.attachments });
27207
28047
  return;
27208
28048
  }
27209
- 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");
27210
28052
  const agent = this.d.runtime.agentFor(worktreeId);
27211
28053
  if (!agent)
27212
28054
  throw new UserError("worktree still starting; try again in a moment");
@@ -27228,14 +28070,11 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27228
28070
  setTimeout(() => this.d.hub.emit("worktreesChanged"), 0);
27229
28071
  return [...owned, ...found];
27230
28072
  }
27231
- spares() {
27232
- return this.d.state.worktrees.filter((wt) => wt.kind === "spare").map((wt) => ({
27233
- repoId: wt.repoId,
27234
- id: wt.id,
27235
- path: wt.path,
27236
- proxyPort: wt.proxyPort,
27237
- ready: this.d.runtime.get(wt.id)?.proxy != null && this.spare.current(wt.repoId)?.worktreeId === wt.id
27238
- }));
28073
+ mainOf(repoId) {
28074
+ return mainOf(this.d.state, repoId);
28075
+ }
28076
+ leadSpareOf(repoId) {
28077
+ return this.spare.current(repoId)?.worktreeId ?? null;
27239
28078
  }
27240
28079
  countsQuick(id, quick) {
27241
28080
  const c = this.countsCache.get(id);
@@ -27244,11 +28083,17 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27244
28083
  quick.missed = true;
27245
28084
  return {};
27246
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
+ }
27247
28092
  async ownedRows(quick) {
27248
- 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) => {
27249
28094
  const rt = this.d.runtime.get(wt.id);
27250
28095
  const { defaultBranch } = this.d.state.requireRepo(wt.repoId);
27251
- 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));
27252
28097
  return {
27253
28098
  id: wt.id,
27254
28099
  repoId: wt.repoId,
@@ -27261,10 +28106,13 @@ ${wt.landing.body}` : wt.landing.subject : undefined;
27261
28106
  login: !!rt?.login,
27262
28107
  ahead,
27263
28108
  behind,
28109
+ unpushed,
27264
28110
  dirty,
27265
28111
  queued: rt?.agent.queueLength || undefined,
27266
28112
  unseen: isUnseen(wt) || undefined,
27267
- 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)
27268
28116
  };
27269
28117
  }));
27270
28118
  }
@@ -27369,7 +28217,7 @@ class ArchiveSweep {
27369
28217
  // ../daemon/src/index.ts
27370
28218
  process.on("unhandledRejection", (e) => log.error("daemon", "unhandled rejection", e));
27371
28219
  process.on("uncaughtException", (e) => log.error("daemon", "uncaught exception", e));
27372
- var here = dirname9(fileURLToPath(import.meta.url));
28220
+ var here = dirname10(fileURLToPath(import.meta.url));
27373
28221
  var {
27374
28222
  shellDist: SHELL_DIST,
27375
28223
  bridgeJs: BRIDGE_JS,
@@ -27423,29 +28271,39 @@ var runtime = new RuntimeRegistry({
27423
28271
  bridgeScript: () => bridge.get(),
27424
28272
  remote,
27425
28273
  grant: previewGrant(token),
27426
- 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
27427
28277
  });
27428
28278
  var drafts = new DraftStore({ file: paths.draftsFile, hub });
27429
- 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
+ });
27430
28289
  var turns = new TurnService({
27431
28290
  state,
27432
28291
  hub,
27433
28292
  transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
27434
- summarize: makeRecapper(runtime, agents, state),
27435
28293
  hold: (id, tag) => runtime.hold(id, tag),
27436
28294
  release: (id, tag) => runtime.release(id, tag)
27437
28295
  });
27438
28296
  var files = new FileService(state, runtime, (id) => worktrees.readable(id));
27439
28297
  var design = new DesignService((id) => worktrees.readable(id));
27440
28298
  var routes = new RouteService({ state, hub, readable: (id) => worktrees.readable(id) });
27441
- var exec = new ExecService({ state, runtime });
27442
- new LandingService({
28299
+ var landing = new LandingService({
27443
28300
  state,
27444
28301
  hub,
27445
28302
  worktrees,
27446
28303
  transcript: (id) => runtime.agentFor(id)?.transcript() ?? [],
27447
- check: (id, command) => exec.exec(id, command, CHECK_TOOL),
27448
- 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)
27449
28307
  });
27450
28308
  var refs = new RefSearch({ state });
27451
28309
  var chats = new ChatSearch({
@@ -27457,7 +28315,7 @@ var chats = new ChatSearch({
27457
28315
  var prs = new PrService({ state, hub, worktrees, view: viewPr });
27458
28316
  var self = new SelfWatch(SOURCE_ROOT);
27459
28317
  await self.start();
27460
- var afterLand = new AfterLand({ state, hub, self });
28318
+ var afterLand = new AfterLand({ state, hub, self, settled: () => idle.settled() });
27461
28319
  var repos = new RepoRegistry({ state, hub, runtime, worktrees, afterLand, self, drafts });
27462
28320
  var idle = new IdlePolicy({
27463
28321
  runtime,
@@ -27526,6 +28384,7 @@ var { branded, stop: stopServer } = startServer({
27526
28384
  chats,
27527
28385
  drafts,
27528
28386
  prs,
28387
+ landing,
27529
28388
  themes,
27530
28389
  agents,
27531
28390
  accounts,
@@ -27552,11 +28411,9 @@ bridge.setShellOrigins([
27552
28411
  writeFileSync10(paths.pidFile, `${process.pid}
27553
28412
  `);
27554
28413
  var stopLagSampler = startLagSampler();
28414
+ await runtime.reclaimOrphans();
27555
28415
  await repos.boot();
27556
- drafts.prune((id) => {
27557
- const repoId = draftRepoOf(id);
27558
- return repoId ? !!state.repo(repoId) : !!state.worktree(id) || worktrees.hasArchived(id);
27559
- });
28416
+ drafts.prune((id) => !!state.worktree(id) || worktrees.hasArchived(id));
27560
28417
  idle.boot();
27561
28418
  fireAndForget("agents", agents.installMissing().then(() => probe.missing()), "agent adapter install");
27562
28419
  var repoArg = process.argv[2];
@@ -27597,6 +28454,7 @@ async function shutdown(signal, opts = {}) {
27597
28454
  drafts.flush();
27598
28455
  const deadline = new Promise((resolve) => setTimeout(resolve, 5000));
27599
28456
  await Promise.race([runtime.shutdown(), deadline]);
28457
+ state.flushGroups();
27600
28458
  rmSync6(paths.pidFile, { force: true });
27601
28459
  if (opts.respawn) {
27602
28460
  await Bun.sleep(250);