toyon 0.19.0 → 0.20.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 (102) hide show
  1. package/dist/CHANGELOG.md +33 -0
  2. package/dist/cli.js +1 -1
  3. package/dist/daemon.js +604 -91
  4. package/dist/shell/assets/{Editor-B-1lrUVF.js → Editor--Z8ktewC.js} +3 -3
  5. package/dist/shell/assets/Editor--Z8ktewC.js.br +0 -0
  6. package/dist/shell/assets/Editor--Z8ktewC.js.gz +0 -0
  7. package/dist/shell/assets/{XTerm-CZl-vWEO.js → XTerm-DsUQ0Xew.js} +1 -1
  8. package/dist/shell/assets/XTerm-DsUQ0Xew.js.br +0 -0
  9. package/dist/shell/assets/XTerm-DsUQ0Xew.js.gz +0 -0
  10. package/dist/shell/assets/{cssMode-BpigOakf.js → cssMode-BjAkCPrp.js} +1 -1
  11. package/dist/shell/assets/cssMode-BjAkCPrp.js.br +0 -0
  12. package/dist/shell/assets/cssMode-BjAkCPrp.js.gz +0 -0
  13. package/dist/shell/assets/{freemarker2-vvh2KLdJ.js → freemarker2-BZCFxXwq.js} +1 -1
  14. package/dist/shell/assets/freemarker2-BZCFxXwq.js.br +0 -0
  15. package/dist/shell/assets/freemarker2-BZCFxXwq.js.gz +0 -0
  16. package/dist/shell/assets/{grammar-DBMSnNcB.js → grammar-CQ1uaD6_.js} +1 -1
  17. package/dist/shell/assets/{grammar-DBMSnNcB.js.br → grammar-CQ1uaD6_.js.br} +0 -0
  18. package/dist/shell/assets/grammar-CQ1uaD6_.js.gz +0 -0
  19. package/dist/shell/assets/{handlebars-ExFAR1bz.js → handlebars-G1zJeDLP.js} +1 -1
  20. package/dist/shell/assets/handlebars-G1zJeDLP.js.br +0 -0
  21. package/dist/shell/assets/handlebars-G1zJeDLP.js.gz +0 -0
  22. package/dist/shell/assets/{html-DpIWoe_L.js → html-CUHSCFMy.js} +1 -1
  23. package/dist/shell/assets/html-CUHSCFMy.js.br +0 -0
  24. package/dist/shell/assets/html-CUHSCFMy.js.gz +0 -0
  25. package/dist/shell/assets/{htmlMode-vCEOKCM-.js → htmlMode-gpMedtd9.js} +1 -1
  26. package/dist/shell/assets/htmlMode-gpMedtd9.js.br +0 -0
  27. package/dist/shell/assets/htmlMode-gpMedtd9.js.gz +0 -0
  28. package/dist/shell/assets/index-B3Alj0WH.css +1 -0
  29. package/dist/shell/assets/index-B3Alj0WH.css.br +0 -0
  30. package/dist/shell/assets/index-B3Alj0WH.css.gz +0 -0
  31. package/dist/shell/assets/index-VVd_p5tJ.js +278 -0
  32. package/dist/shell/assets/index-VVd_p5tJ.js.br +0 -0
  33. package/dist/shell/assets/index-VVd_p5tJ.js.gz +0 -0
  34. package/dist/shell/assets/{javascript-CF6f9y1O.js → javascript-BHWH8gQN.js} +1 -1
  35. package/dist/shell/assets/{jsonMode-CZzpDv9U.js → jsonMode-DyV5nvVT.js} +1 -1
  36. package/dist/shell/assets/jsonMode-DyV5nvVT.js.br +0 -0
  37. package/dist/shell/assets/jsonMode-DyV5nvVT.js.gz +0 -0
  38. package/dist/shell/assets/{liquid-5T0ZgJhW.js → liquid-Dy3tUgRC.js} +1 -1
  39. package/dist/shell/assets/liquid-Dy3tUgRC.js.br +0 -0
  40. package/dist/shell/assets/liquid-Dy3tUgRC.js.gz +0 -0
  41. package/dist/shell/assets/{lspLanguageFeatures-DrkNrp6u.js → lspLanguageFeatures-CB_Ow-w5.js} +1 -1
  42. package/dist/shell/assets/lspLanguageFeatures-CB_Ow-w5.js.br +0 -0
  43. package/dist/shell/assets/lspLanguageFeatures-CB_Ow-w5.js.gz +0 -0
  44. package/dist/shell/assets/{mdx-DnLcCYKb.js → mdx-DD5jgco_.js} +1 -1
  45. package/dist/shell/assets/mdx-DD5jgco_.js.br +0 -0
  46. package/dist/shell/assets/mdx-DD5jgco_.js.gz +0 -0
  47. package/dist/shell/assets/{python-Di4WuyJe.js → python-BvLKUXll.js} +1 -1
  48. package/dist/shell/assets/python-BvLKUXll.js.br +0 -0
  49. package/dist/shell/assets/python-BvLKUXll.js.gz +0 -0
  50. package/dist/shell/assets/{razor-NpEB1IHs.js → razor-B2tUlbzR.js} +1 -1
  51. package/dist/shell/assets/razor-B2tUlbzR.js.br +0 -0
  52. package/dist/shell/assets/razor-B2tUlbzR.js.gz +0 -0
  53. package/dist/shell/assets/{tsMode-BylSuxTF.js → tsMode-DMuNnqM7.js} +1 -1
  54. package/dist/shell/assets/tsMode-DMuNnqM7.js.br +0 -0
  55. package/dist/shell/assets/tsMode-DMuNnqM7.js.gz +0 -0
  56. package/dist/shell/assets/{xml-BYRl5HAh.js → xml-DdYgb5a2.js} +1 -1
  57. package/dist/shell/assets/xml-DdYgb5a2.js.br +0 -0
  58. package/dist/shell/assets/xml-DdYgb5a2.js.gz +0 -0
  59. package/dist/shell/assets/{yaml-DeSD0TUR.js → yaml-BDujJdoL.js} +1 -1
  60. package/dist/shell/assets/yaml-BDujJdoL.js.br +0 -0
  61. package/dist/shell/assets/yaml-BDujJdoL.js.gz +0 -0
  62. package/dist/shell/index.html +2 -2
  63. package/package.json +1 -1
  64. package/dist/shell/assets/Editor-B-1lrUVF.js.br +0 -0
  65. package/dist/shell/assets/Editor-B-1lrUVF.js.gz +0 -0
  66. package/dist/shell/assets/XTerm-CZl-vWEO.js.br +0 -0
  67. package/dist/shell/assets/XTerm-CZl-vWEO.js.gz +0 -0
  68. package/dist/shell/assets/cssMode-BpigOakf.js.br +0 -0
  69. package/dist/shell/assets/cssMode-BpigOakf.js.gz +0 -0
  70. package/dist/shell/assets/freemarker2-vvh2KLdJ.js.br +0 -0
  71. package/dist/shell/assets/freemarker2-vvh2KLdJ.js.gz +0 -0
  72. package/dist/shell/assets/grammar-DBMSnNcB.js.gz +0 -0
  73. package/dist/shell/assets/handlebars-ExFAR1bz.js.br +0 -0
  74. package/dist/shell/assets/handlebars-ExFAR1bz.js.gz +0 -0
  75. package/dist/shell/assets/html-DpIWoe_L.js.br +0 -0
  76. package/dist/shell/assets/html-DpIWoe_L.js.gz +0 -0
  77. package/dist/shell/assets/htmlMode-vCEOKCM-.js.br +0 -0
  78. package/dist/shell/assets/htmlMode-vCEOKCM-.js.gz +0 -0
  79. package/dist/shell/assets/index-CHbNBHbD.css +0 -1
  80. package/dist/shell/assets/index-CHbNBHbD.css.br +0 -0
  81. package/dist/shell/assets/index-CHbNBHbD.css.gz +0 -0
  82. package/dist/shell/assets/index-D5GN5OOv.js +0 -276
  83. package/dist/shell/assets/index-D5GN5OOv.js.br +0 -0
  84. package/dist/shell/assets/index-D5GN5OOv.js.gz +0 -0
  85. package/dist/shell/assets/jsonMode-CZzpDv9U.js.br +0 -0
  86. package/dist/shell/assets/jsonMode-CZzpDv9U.js.gz +0 -0
  87. package/dist/shell/assets/liquid-5T0ZgJhW.js.br +0 -0
  88. package/dist/shell/assets/liquid-5T0ZgJhW.js.gz +0 -0
  89. package/dist/shell/assets/lspLanguageFeatures-DrkNrp6u.js.br +0 -0
  90. package/dist/shell/assets/lspLanguageFeatures-DrkNrp6u.js.gz +0 -0
  91. package/dist/shell/assets/mdx-DnLcCYKb.js.br +0 -0
  92. package/dist/shell/assets/mdx-DnLcCYKb.js.gz +0 -0
  93. package/dist/shell/assets/python-Di4WuyJe.js.br +0 -0
  94. package/dist/shell/assets/python-Di4WuyJe.js.gz +0 -0
  95. package/dist/shell/assets/razor-NpEB1IHs.js.br +0 -0
  96. package/dist/shell/assets/razor-NpEB1IHs.js.gz +0 -0
  97. package/dist/shell/assets/tsMode-BylSuxTF.js.br +0 -0
  98. package/dist/shell/assets/tsMode-BylSuxTF.js.gz +0 -0
  99. package/dist/shell/assets/xml-BYRl5HAh.js.br +0 -0
  100. package/dist/shell/assets/xml-BYRl5HAh.js.gz +0 -0
  101. package/dist/shell/assets/yaml-DeSD0TUR.js.br +0 -0
  102. package/dist/shell/assets/yaml-DeSD0TUR.js.gz +0 -0
package/dist/daemon.js CHANGED
@@ -12,6 +12,7 @@ var TOOL_SEARCH = "ToolSearch";
12
12
  var ASK_TOOL = "AskUserQuestion";
13
13
  var TOYON_MCP_SERVER = "toyon";
14
14
  var SPLIT_TOOL = "split";
15
+ var WORKTREES_TOOL = "worktrees";
15
16
  function coalesce(entries) {
16
17
  const out = [];
17
18
  for (const entry of entries) {
@@ -45,6 +46,15 @@ function isWrittenKind(kind) {
45
46
  function writingCall(call, input) {
46
47
  return (isWrittenKind(call.kind) || call.name === ASK_TOOL || call.name === TOOL_SEARCH) && emptyInput(input);
47
48
  }
49
+ var MCP_NAME = /^(?:mcp__(.+?)__(.+)|([A-Za-z0-9_-]+)\/([A-Za-z0-9_-]+))$/;
50
+ function mcpTool(name) {
51
+ const m = MCP_NAME.exec(name);
52
+ if (!m)
53
+ return null;
54
+ const server = m[1] ?? m[3] ?? "";
55
+ const tool = m[2] ?? m[4] ?? "";
56
+ return { server, tool: tool.replace(/[_-]+/g, " ").trim() };
57
+ }
48
58
  function emptyInput(input) {
49
59
  if (!input || typeof input !== "object")
50
60
  return true;
@@ -117,6 +127,35 @@ function railOrder(rows) {
117
127
 
118
128
  // ../shared/src/archiveRule.ts
119
129
  var ARCHIVE_KEEP_RECENT = 5;
130
+ function heldBy(w, f) {
131
+ const c = f.counts(w.id);
132
+ if (c.dirty === undefined || c.ahead === undefined)
133
+ return "its changes are not counted yet";
134
+ if (c.dirty > 0)
135
+ return "it has uncommitted changes";
136
+ if (c.ahead > 0)
137
+ return "it has commits not on main";
138
+ if (f.drafted(w.id))
139
+ return "unsent text waits in its composer";
140
+ if (f.pending(w.id))
141
+ return "its agent is still busy";
142
+ const end = w.lastTurn?.end;
143
+ if (!end)
144
+ return "its agent has not run yet";
145
+ if (end === "stopped")
146
+ return "its last turn was stopped";
147
+ if (end === "failed")
148
+ return "its last turn failed";
149
+ if (end === "asking")
150
+ return "its agent is waiting on an answer";
151
+ if (isUnseen(w))
152
+ return "its last reply is unread";
153
+ if (w.planned)
154
+ return "it showed a plan";
155
+ if (w.pr?.state === "open")
156
+ return "its pull request is still open";
157
+ return null;
158
+ }
120
159
  function archiveReason(wt, f) {
121
160
  if (!nothingToKeep(wt, f))
122
161
  return null;
@@ -129,16 +168,11 @@ var UNOPENED = "not opened in ";
129
168
  function nothingToKeep(w, f) {
130
169
  if (w.kind !== "worktree" || !hasOwnBranch(w))
131
170
  return false;
132
- if (w.lastTurn?.end !== "done" || isUnseen(w))
133
- return false;
134
- if (w.planned || w.pr?.state === "open")
135
- return false;
136
- if (f.viewed(w.id) || f.pending(w.id) || f.drafted(w.id))
171
+ if (f.viewed(w.id))
137
172
  return false;
138
173
  if (f.now - (w.viewedAt ?? w.createdAt) < f.afterMs)
139
174
  return false;
140
- const c = f.counts(w.id);
141
- return c.dirty === 0 && c.ahead === 0;
175
+ return heldBy(w, f) === null;
142
176
  }
143
177
  function rowsAhead(wt, rows) {
144
178
  const tasks = railOrder(rows.filter((w) => w.kind === "worktree").map((worktree) => ({ worktree })));
@@ -6586,7 +6620,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6586
6620
  }
6587
6621
 
6588
6622
  // ../shared/src/protocol/limits.ts
6589
- var PROTOCOL_VERSION = 129;
6623
+ var PROTOCOL_VERSION = 133;
6590
6624
  var PAGE_ERROR_MAX_CHARS = 2000;
6591
6625
  var PAGE_TITLE_MAX_CHARS = 1000;
6592
6626
  var FILE_MAX_CHARS = 5000000;
@@ -6948,6 +6982,7 @@ var clientMsgSchema = discriminatedUnion("t", [
6948
6982
  object({ t: literal("reveal"), worktreeId: id, path: relPath.optional() }),
6949
6983
  object({ t: literal("reveal-path"), path: relPath }),
6950
6984
  object({ t: literal("reveal-attachment"), worktreeId: id, file: string2().min(1).max(120) }),
6985
+ object({ t: literal("reveal-transcript"), worktreeId: id }),
6951
6986
  object({ t: literal("stop-agent"), worktreeId: id }),
6952
6987
  object({ t: literal("seen"), worktreeId: id }),
6953
6988
  object({ t: literal("mark-unread"), worktreeId: id }),
@@ -8518,7 +8553,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8518
8553
  // ../daemon/package.json
8519
8554
  var package_default = {
8520
8555
  name: "@toyon/daemon",
8521
- version: "0.19.0",
8556
+ version: "0.20.0",
8522
8557
  private: true,
8523
8558
  type: "module",
8524
8559
  main: "src/index.ts",
@@ -14372,7 +14407,13 @@ var DENIED_COMMANDS = [
14372
14407
  "git branch -D",
14373
14408
  "git branch -d",
14374
14409
  "git branch --delete",
14375
- "git worktree",
14410
+ "git worktree add",
14411
+ "git worktree remove",
14412
+ "git worktree move",
14413
+ "git worktree prune",
14414
+ "git worktree lock",
14415
+ "git worktree unlock",
14416
+ "git worktree repair",
14376
14417
  "gh pr create",
14377
14418
  "gh pr merge"
14378
14419
  ];
@@ -14422,6 +14463,17 @@ function opencodeEnv() {
14422
14463
  };
14423
14464
  }
14424
14465
 
14466
+ // ../daemon/src/agent/quick.ts
14467
+ function quickModelFor(spec, offered, current) {
14468
+ const quick = spec.quickModel;
14469
+ if (typeof quick === "function")
14470
+ return quick(offered, current);
14471
+ return quick && offered.includes(quick) ? quick : undefined;
14472
+ }
14473
+ function quickModelNamed(name) {
14474
+ return (offered) => offered.includes(name) ? name : offered.find((id) => id.includes(name));
14475
+ }
14476
+
14425
14477
  // ../daemon/src/agent/sandbox.ts
14426
14478
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync7, renameSync, writeFileSync as writeFileSync4 } from "fs";
14427
14479
  import { homedir as homedir2, tmpdir } from "os";
@@ -14754,7 +14806,7 @@ var BUILTIN_AGENTS = [
14754
14806
  systemPrompt: "meta-append",
14755
14807
  modes: { plan: "plan", build: "default" },
14756
14808
  sideMeta: { claudeCode: { options: { tools: [], persistSession: false } } },
14757
- quickModel: "haiku",
14809
+ quickModel: quickModelNamed("haiku"),
14758
14810
  loginHint: "Claude is not logged in"
14759
14811
  },
14760
14812
  {
@@ -15571,6 +15623,65 @@ function unifiedDiff(oldText, newText, context = 3) {
15571
15623
  `);
15572
15624
  }
15573
15625
 
15626
+ // ../daemon/src/agent/acp/wakeup.ts
15627
+ var WAKEUP_TOOL = "ScheduleWakeup";
15628
+ var MIN_DELAY_S = 60;
15629
+ var MAX_DELAY_S = 3600;
15630
+ var GRACE_MS2 = 60000;
15631
+ function wakeupAt(name, input, now = Date.now()) {
15632
+ if (name !== WAKEUP_TOOL)
15633
+ return null;
15634
+ const rec = input && typeof input === "object" ? input : null;
15635
+ if (!rec || rec.stop === true)
15636
+ return null;
15637
+ const delay = rec.delaySeconds;
15638
+ if (typeof delay !== "number" || !Number.isFinite(delay))
15639
+ return null;
15640
+ return now + Math.min(MAX_DELAY_S, Math.max(MIN_DELAY_S, delay)) * 1000;
15641
+ }
15642
+
15643
+ class Wakeups {
15644
+ deps;
15645
+ held = new Map;
15646
+ constructor(deps) {
15647
+ this.deps = deps;
15648
+ }
15649
+ get size() {
15650
+ return this.held.size;
15651
+ }
15652
+ get next() {
15653
+ let next = null;
15654
+ for (const w of this.held.values())
15655
+ if (next === null || w.until < next)
15656
+ next = w.until;
15657
+ return next;
15658
+ }
15659
+ start(toolId, until) {
15660
+ const wait = Math.max(0, until - Date.now()) + (this.deps.graceMs ?? GRACE_MS2);
15661
+ const timer = setTimeout(() => this.finish(toolId, "no turn came; the row stops waiting", false), wait);
15662
+ timer.unref?.();
15663
+ this.held.set(toolId, { until, timer });
15664
+ this.deps.onChange?.();
15665
+ }
15666
+ wake(how) {
15667
+ for (const toolId of [...this.held.keys()])
15668
+ this.finish(toolId, how, false);
15669
+ }
15670
+ endAll(reason) {
15671
+ for (const toolId of [...this.held.keys()])
15672
+ this.finish(toolId, reason, true);
15673
+ }
15674
+ finish(toolId, output, isError) {
15675
+ const w = this.held.get(toolId);
15676
+ if (!w)
15677
+ return;
15678
+ clearTimeout(w.timer);
15679
+ this.held.delete(toolId);
15680
+ this.deps.note({ type: "tool-end", toolId, output, isError });
15681
+ this.deps.onChange?.();
15682
+ }
15683
+ }
15684
+
15574
15685
  // ../daemon/src/agent/acp/map.ts
15575
15686
  function spawnOf(meta) {
15576
15687
  const claude = asRecord2(asRecord2(meta).claudeCode);
@@ -15598,7 +15709,10 @@ function toolSearchTitle(rawInput) {
15598
15709
  const names = /^select:(.*)$/.exec(query.trim());
15599
15710
  if (!names)
15600
15711
  return `looked for a tool: ${query.trim()}`;
15601
- const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).join(", ");
15712
+ const loaded = (names[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean).map((s) => {
15713
+ const mcp = mcpTool(s);
15714
+ return mcp ? `${mcp.server} ${mcp.tool}` : s;
15715
+ }).join(", ");
15602
15716
  return loaded ? `loaded ${loaded}` : "";
15603
15717
  }
15604
15718
  var TOYON_TOOL = new RegExp(`^(?:mcp__${TOYON_MCP_SERVER}__|${TOYON_MCP_SERVER}/)([A-Za-z0-9_-]+)$`);
@@ -15676,7 +15790,7 @@ function abandoned(memos, parent, opening = false) {
15676
15790
  }
15677
15791
  return out;
15678
15792
  }
15679
- function mapUpdate(update, memos, tag, sink, background) {
15793
+ function mapUpdate(update, memos, tag, sink, background, wakeup) {
15680
15794
  switch (update.sessionUpdate) {
15681
15795
  case "agent_message_chunk": {
15682
15796
  const { parentToolId } = spawnOf(update._meta);
@@ -15723,7 +15837,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15723
15837
  ...spawn
15724
15838
  });
15725
15839
  if (update.status === "completed" || update.status === "failed")
15726
- out.push(endOf(update.toolCallId, memo, update.status, sink, background));
15840
+ out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup));
15727
15841
  return out;
15728
15842
  }
15729
15843
  case "tool_call_update": {
@@ -15780,7 +15894,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15780
15894
  if (update.rawOutput !== undefined)
15781
15895
  memo.rawOutput = update.rawOutput;
15782
15896
  if ((update.status === "completed" || update.status === "failed") && !memo.ended) {
15783
- out.push(endOf(update.toolCallId, memo, update.status, sink, background));
15897
+ out.push(endOf(update.toolCallId, memo, update.status, sink, background, wakeup));
15784
15898
  }
15785
15899
  return out;
15786
15900
  }
@@ -15811,7 +15925,7 @@ function textOf(content, tag, what) {
15811
15925
  log.debug(tag, `acp: dropping non-text ${what} block (${content.type})`);
15812
15926
  return null;
15813
15927
  }
15814
- function endOf(toolId, memo, status, sink, background) {
15928
+ function endOf(toolId, memo, status, sink, background, wakeup) {
15815
15929
  memo.ended = true;
15816
15930
  const output = summarizeToolOutput(memo.content, memo.rawOutput);
15817
15931
  if (status === "completed" && memo.kind === "execute" && background) {
@@ -15819,6 +15933,13 @@ function endOf(toolId, memo, status, sink, background) {
15819
15933
  if (start && background(toolId, start, memo.input))
15820
15934
  return { type: "tool-update", toolId, background: true };
15821
15935
  }
15936
+ if (status === "completed" && wakeup) {
15937
+ const until = wakeupAt(memo.name || memo.title, memo.input);
15938
+ if (until !== null) {
15939
+ wakeup(toolId, until);
15940
+ return { type: "tool-update", toolId, background: true, until };
15941
+ }
15942
+ }
15822
15943
  const images = sink ? toolImages(memo.content, sink) : [];
15823
15944
  const detached = status === "completed" && !!memo.spawns && spawnDetached(wholeText(memo.content, memo.rawOutput));
15824
15945
  return {
@@ -16012,10 +16133,10 @@ var SYSTEM_APPEND = [
16012
16133
  "You are working inside a dedicated git worktree managed by Toyon.",
16013
16134
  "Make every change inside the current working directory and never modify files outside it. Reading files outside it is fine when the user points you there.",
16014
16135
  "When a change belongs in another project Toyon has open rather than in this worktree, do not edit outside it and do not stop at saying so: propose it with the `split` tool when you have one, as one piece with `where` set to `worktree` and `project` naming that project, its `plan` in markdown that project's agent builds from, with what is needed and why, how to reproduce it, and the paths here that depend on it; the user decides on a card, and once it is proposed you finish your turn.",
16015
- "When the work you are planning splits into pieces that could go on in parallel, hand Toyon the list with the `split` tool when you have one, right before you ask for plan approval (or, outside plan mode, as part of your reply): for each piece a title, a plan in markdown standing on its own, which its agent builds from as an approved plan, the attachments from this chat it needs by their labels (`Image 2`, `Paste 1`), and where it goes: `here` for what you will do yourself in this worktree, `worktree` for a new worktree of this project or, with `project`, of another open one (with `after` naming the branch it must wait for, another piece of the list by its title, or `this` for your own), or `send` for an open worktree already working on that part, named by its branch or title. The user reads the list beside your plan and may move any piece; Toyon acts on their answer, so never start a sibling's piece yourself, and do only the pieces marked `here`.",
16016
- "The same tool, with one piece, is how you offer a separate thing you come across mid-task: a feature you would call its own, a follow-up you would otherwise park in a sentence, work the person asks to have done in a new worktree. Offer it once, as `worktree`, with a plan its agent builds from; a sentence saying it could be its own branch is not an offer, and a card the person can decline in one press is.",
16136
+ "When the work you are planning splits into pieces that could go on in parallel, hand Toyon the list with the `split` tool when you have one, right before you ask for plan approval, or, outside plan mode, before you start on your own pieces, never at the end of the turn: a sibling's agent then works while you do, and the user sees the card at once. For each piece, a title, a plan in markdown standing on its own, which its agent builds from as an approved plan, the attachments from this chat it needs by their labels (`Image 2`, `Paste 1`), and where it goes: `here` for what you will do yourself in this worktree (with `after` naming a branch or a piece of the list your own work must wait for, which Toyon then shows under the box until it lands), `worktree` for a new worktree of this project or, with `project`, of another open one (with `after` naming the branch it must wait for, another piece of the list by its title, or `this` for your own), or `send` for an open worktree already working on that part, named by its branch or title. The user reads the list beside your plan and may move any piece; Toyon acts on their answer, so never start a sibling's piece yourself, and do only the pieces marked `here`.",
16137
+ "The same tool, with one piece, is how you offer a separate thing you come across mid-task: a feature you would call its own, a follow-up you would otherwise park in a sentence, work the person asks to have done in a new worktree. Offer it once, as `worktree`, with a plan its agent builds from, as soon as you see it rather than when your own work is done; a sentence saying it could be its own branch is not an offer, and a card the person can decline in one press is.",
16017
16138
  "A write Toyon refuses (outside the worktree, or to an agent's settings, hooks or plugin code such as .claude/settings.json, .claude/hooks or opencode.json) was refused by Toyon and not by the user, who was never asked. Report it as the boundary that stopped it, never as the user declining.",
16018
- "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
16139
+ "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI. Never add, remove, move or prune worktrees; Toyon owns them. Listing them with `git worktree list` is fine.",
16019
16140
  "Never run `git commit` unless the user explicitly asks you to, and never offer to, through the question tool or otherwise; leave changes uncommitted for the user to review and commit themselves.",
16020
16141
  "Keep the scope tight: do the asked task well, then stop. A follow-up the user might want later is a sentence in your reply, not a question.",
16021
16142
  "When the work is done and the only thing left is whether to do something you recommend, do not end your turn on a prose offer waiting for a yes. Ask it with your question tool when you have one: one question, two options with the recommended one first, each saying what it changes, and typed text allowed so the answer can carry instructions. The card is the first thing the user reads, before any prose of yours, so the question itself states what you propose and why in a sentence or two; a question that is only the yes-or-no leaves them guessing what it is. Ask this way only where you would otherwise have stopped and waited; never to confirm work already asked for.",
@@ -16023,6 +16144,7 @@ var SYSTEM_APPEND = [
16023
16144
  "When referencing a file in chat, use a Markdown link whose target is its absolute path, with an optional `:line`; Toyon opens it in the built-in editor, whether it is in this worktree or anywhere else on this machine. A folder, a URL route or a command is not a file: name it in backticks, never as a link.",
16024
16145
  "`@some/path` in a message means read that file or directory first; `@changes` means this worktree's uncommitted files, which `git status` lists; `@<branch>` names another worktree of this project, and the context attached to the message says where its chat is on disk.",
16025
16146
  "Toyon knows every worktree by a title and a branch; its directory, `wt-xxxx`, is an id that outlives both. When you name this worktree or another in prose, use its title in quotes or its branch, never the directory: the context attached to each message gives this worktree's title and branch, and a sibling's when the message names it.",
16147
+ "To find which worktree of this project worked on a topic, a ticket or a feature, or to see what is open and what was archived, call the `worktrees` tool when you have one: it lists them by title and branch with where each chat is on disk, and with a `query` it keeps only those whose title, branch or chat mention it. `git worktree list` knows neither titles nor chats, so it cannot answer that.",
16026
16148
  '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, teardown (optional) runs once when a worktree is removed, 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; timeouts (optional) gives setup, check and commit each a ceiling as a duration like "30m" (ten minutes each when unset), for a suite that runs longer. 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.',
16027
16149
  "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.",
16028
16150
  "When you scaffold a project, write its .gitignore (dependencies, build output, local env files) before installing anything, so an install never leaves thousands of untracked files for the user to wade through or commit.",
@@ -16166,7 +16288,7 @@ function piecesActedPrompt(pieces, outcomes) {
16166
16288
  const started = outcomes.filter((o) => o.fate === "worktree" && !o.after && !o.error).map((o) => titled(o.pieceId));
16167
16289
  const queued = outcomes.filter((o) => o.fate === "worktree" && o.after && !o.error);
16168
16290
  const sent = outcomes.filter((o) => o.fate === "send" && !o.error);
16169
- const here = outcomes.filter((o) => o.fate === "here").map((o) => titled(o.pieceId));
16291
+ const here = outcomes.filter((o) => o.fate === "here").map((o) => o.after ? `${titled(o.pieceId)} (once ${o.after} lands)` : titled(o.pieceId));
16170
16292
  const skipped = outcomes.filter((o) => o.fate === "skip").map((o) => titled(o.pieceId));
16171
16293
  const failed = outcomes.filter((o) => o.error).map((o) => `${titled(o.pieceId)} (${o.error})`);
16172
16294
  const parts = [];
@@ -16203,8 +16325,11 @@ function pieceAskPrompt(text, project) {
16203
16325
  const where = project ? `in a new worktree of ${project.name} (${project.path}), another project open in Toyon. Propose it with the \`split\` tool: one piece, \`where\` set to \`worktree\` and \`project\` to "${project.path}", with a title and a plan that project's agent builds from, with what is needed and why, how to reproduce it, and the paths here that depend on it.` : "in a new worktree of this project. Propose it with the `split` tool: one piece, `where` set to `worktree`, with a title and a plan its agent builds from, with what is needed and why and the paths here that depend on it.";
16204
16326
  return `The user wants part of this work done ${where}` + (own ? ` The user's own words for what it should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
16205
16327
  }
16206
- function pieceLandedContext(title, project, url, same, again) {
16328
+ function pieceLandedContext(title, project, url, same, again, awaited) {
16207
16329
  const where = url ? ` (${url})` : "";
16330
+ if (awaited) {
16331
+ return `The branch your own piece waited for, "${title}", has landed on this project's main${where}; that piece can go ahead now, and Toyon rebases onto main when this lands.`;
16332
+ }
16208
16333
  if (again) {
16209
16334
  return `The branch this worktree waited for before it started, "${title}", landed again on this project's main${where} with follow-up commits; Toyon rebases onto main when this lands, and its latest commits are readable now if they touch your piece.`;
16210
16335
  }
@@ -16227,6 +16352,10 @@ function archivedFate(a) {
16227
16352
  return "without landing; its commits are kept under an archive ref, not on any checkout";
16228
16353
  return "without landing, and git kept nothing of it";
16229
16354
  }
16355
+ function siblingParagraph(s) {
16356
+ const chat = `Its chat is at ${s.transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`;
16357
+ return s.archived ? `@${s.branch} was the worktree "${s.title}" in this project, archived on ${dayOf(s.archived.at)} ${archivedFate(s.archived)}. Its checkout at ${s.path} is gone. ${chat}` : `@${s.branch} is the worktree "${s.title}" in this project, checked out at ${s.path}. ${chat}`;
16358
+ }
16230
16359
  function worktreeContext(text, siblings) {
16231
16360
  const out = [];
16232
16361
  const said = new Set;
@@ -16237,8 +16366,7 @@ function worktreeContext(text, siblings) {
16237
16366
  if (!named.test(text))
16238
16367
  continue;
16239
16368
  said.add(s.branch);
16240
- const chat = `Its chat is at ${s.transcript}, one JSON object per line: a "user-message" event is what the person wrote, the "text-delta" events after it are the agent's reply, and the rest is tool activity.`;
16241
- out.push(s.archived ? `@${s.branch} was the worktree "${s.title}" in this project, archived on ${dayOf(s.archived.at)} ${archivedFate(s.archived)}. Its checkout at ${s.path} is gone. ${chat}` : `@${s.branch} is the worktree "${s.title}" in this project, checked out at ${s.path}. ${chat}`);
16369
+ out.push(siblingParagraph(s));
16242
16370
  }
16243
16371
  return out;
16244
16372
  }
@@ -16478,14 +16606,6 @@ class Transcript {
16478
16606
  }
16479
16607
  }
16480
16608
 
16481
- // ../daemon/src/agent/quick.ts
16482
- function quickModelFor(spec, offered, current) {
16483
- const quick = spec.quickModel;
16484
- if (typeof quick === "function")
16485
- return quick(offered, current);
16486
- return quick && offered.includes(quick) ? quick : undefined;
16487
- }
16488
-
16489
16609
  // ../daemon/src/agent/acp/ask.ts
16490
16610
  async function askOnce(ctx, a) {
16491
16611
  const meta = a.spec.systemPrompt === "meta-append";
@@ -16761,6 +16881,7 @@ class AcpSession {
16761
16881
  reaper = null;
16762
16882
  own = null;
16763
16883
  tasks;
16884
+ wakeups;
16764
16885
  asking = 0;
16765
16886
  asks = new Map;
16766
16887
  openCards = new Set;
@@ -16776,10 +16897,15 @@ class AcpSession {
16776
16897
  this.log = new Transcript(transcriptPathFor(d.transcriptsDir, d.worktreeId), d.worktreeId);
16777
16898
  this.tasks = new BackgroundTasks({
16778
16899
  note: (e) => this.emit(e),
16779
- onEnd: () => this.maybeArmReaper(),
16900
+ onEnd: () => this.holdingChanged(),
16780
16901
  ...d.taskPollMs !== undefined ? { pollMs: d.taskPollMs } : {},
16781
16902
  ...d.claudeConfigDir !== undefined ? { configDir: d.claudeConfigDir } : {}
16782
16903
  });
16904
+ this.wakeups = new Wakeups({
16905
+ note: (e) => this.emit(e),
16906
+ onChange: () => this.holdingChanged(),
16907
+ ...d.wakeupGraceMs !== undefined ? { graceMs: d.wakeupGraceMs } : {}
16908
+ });
16783
16909
  this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" || event.type === "handed" ? event.attachments : undefined));
16784
16910
  const seal = sealOpenTurn(this.log.entries);
16785
16911
  const open = new Set;
@@ -16863,8 +16989,7 @@ class AcpSession {
16863
16989
  const pieces = this.adopted.pieces;
16864
16990
  this.adopted = null;
16865
16991
  if (event.outcome !== "answered") {
16866
- const list = this.log.entries.findLast(({ event: e }) => e.type === "pieces-proposed" && e.id === pieces)?.event;
16867
- if (list?.type === "pieces-proposed" && list.by === "toyon") {
16992
+ if (this.proposed(pieces)?.by === "toyon") {
16868
16993
  this.note({
16869
16994
  type: "pieces-declined",
16870
16995
  id: pieces,
@@ -16892,9 +17017,7 @@ class AcpSession {
16892
17017
  return;
16893
17018
  this.heldPieces = null;
16894
17019
  this.openCards.add(id);
16895
- const proposed = this.log.entries.findLast(({ event }) => event.type === "pieces-proposed" && event.id === id)?.event;
16896
- const pieces = proposed?.type === "pieces-proposed" ? proposed.pieces : [];
16897
- const plan = this.d.onPiecesShown?.(id, pieces);
17020
+ const plan = this.d.onPiecesShown?.(id, this.proposed(id)?.pieces ?? []);
16898
17021
  this.emit({ type: "pieces-shown", id, ...typeof plan === "string" ? { plan } : {}, ts: Date.now() });
16899
17022
  }
16900
17023
  get commands() {
@@ -17003,6 +17126,16 @@ class AcpSession {
17003
17126
  get stopping() {
17004
17127
  return this.turn?.stopping ?? false;
17005
17128
  }
17129
+ get holding() {
17130
+ return this.tasks.size + this.wakeups.size;
17131
+ }
17132
+ get wakes() {
17133
+ return this.wakeups.next;
17134
+ }
17135
+ holdingChanged() {
17136
+ this.d.onHolding?.();
17137
+ this.maybeArmReaper();
17138
+ }
17006
17139
  setStopping(on) {
17007
17140
  const turn = this.turn;
17008
17141
  if (!turn || turn.stopping === on)
@@ -17150,6 +17283,7 @@ class AcpSession {
17150
17283
  }
17151
17284
  async drain() {
17152
17285
  this.endOwn("end_turn");
17286
+ this.wakeups.wake("a message came first");
17153
17287
  this.turn = { open: false, stopping: false };
17154
17288
  this.clearReaper();
17155
17289
  this.setStatus("working");
@@ -17406,8 +17540,9 @@ class AcpSession {
17406
17540
  const e = entries[i].event;
17407
17541
  if (isMessage(e))
17408
17542
  break;
17409
- if (e.type === "piece-landed")
17410
- out.unshift(pieceLandedContext(e.title, e.repoName, e.url, e.same, e.again));
17543
+ if (e.type === "piece-landed") {
17544
+ out.unshift(pieceLandedContext(e.title, e.repoName, e.url, e.same, e.again, e.awaited));
17545
+ }
17411
17546
  }
17412
17547
  return out;
17413
17548
  }
@@ -17445,6 +17580,7 @@ class AcpSession {
17445
17580
  this.d.mcp?.close();
17446
17581
  this.cancelAsks();
17447
17582
  this.tasks.endAll("the agent stopped, and the command with it");
17583
+ this.wakeups.endAll("the agent stopped before it woke");
17448
17584
  this.endOwn("interrupted");
17449
17585
  }
17450
17586
  });
@@ -17647,7 +17783,12 @@ class AcpSession {
17647
17783
  this.d.onOptionChanged?.(category, opt.current);
17648
17784
  }
17649
17785
  }
17650
- const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => this.tasks.start(toolId, start, input));
17786
+ const events = mapUpdate(params.update, live.tools, this.d.worktreeId, (file, bytes) => fireAndForget(this.d.worktreeId, this.d.attachments.putToolImage(this.d.worktreeId, file, bytes), "tool image"), (toolId, start, input) => {
17787
+ const taken = this.tasks.start(toolId, start, input);
17788
+ if (taken)
17789
+ this.holdingChanged();
17790
+ return taken;
17791
+ }, (toolId, until) => this.wakeups.start(toolId, until));
17651
17792
  this.forward(live, events);
17652
17793
  this.holdInput(live, params.update);
17653
17794
  }
@@ -17750,9 +17891,23 @@ class AcpSession {
17750
17891
  }
17751
17892
  if (plan && approved && planPath)
17752
17893
  return this.sayPlanEdited(detail, planPath).then(() => res);
17894
+ if (plan && !approved && decided && this.proposed(decided)?.by !== "toyon") {
17895
+ return this.sayPiecesDeclined().then(() => res);
17896
+ }
17753
17897
  return res;
17754
17898
  });
17755
17899
  }
17900
+ proposed(id) {
17901
+ const list = this.log.entries.findLast(({ event }) => event.type === "pieces-proposed" && event.id === id)?.event;
17902
+ return list?.type === "pieces-proposed" ? list : undefined;
17903
+ }
17904
+ async sayPiecesDeclined() {
17905
+ try {
17906
+ await this.deliver("The pieces list you handed over went back with the plan. Hand over a list again with `split` before you ask for approval.");
17907
+ } catch (e) {
17908
+ log.warn(this.d.worktreeId, "the declined list was not sent", e);
17909
+ }
17910
+ }
17756
17911
  async sayPlanEdited(proposed, path) {
17757
17912
  try {
17758
17913
  if (!await this.d.planEdited?.(path, proposed))
@@ -17845,6 +18000,7 @@ class AcpSession {
17845
18000
  return;
17846
18001
  this.own = { open: new Set, settle: null };
17847
18002
  this.clearReaper();
18003
+ this.wakeups.wake("woke");
17848
18004
  this.emit({ type: "turn-start", ts: Date.now() });
17849
18005
  this.syncStatus();
17850
18006
  }
@@ -17874,7 +18030,7 @@ class AcpSession {
17874
18030
  this.maybeArmReaper();
17875
18031
  }
17876
18032
  quiet() {
17877
- return !this.running && !this.own && !this.asking && this.asks.size === 0 && this.tasks.size === 0;
18033
+ return !this.running && !this.own && !this.asking && this.asks.size === 0 && this.holding === 0;
17878
18034
  }
17879
18035
  maybeArmReaper() {
17880
18036
  if (this.quiet() && this.conn && !this.stopped)
@@ -17900,6 +18056,7 @@ class AcpSession {
17900
18056
  async dropConn(why) {
17901
18057
  this.cancelAsks();
17902
18058
  this.tasks.endAll("the agent stopped, and the command with it");
18059
+ this.wakeups.endAll("the agent stopped before it woke");
17903
18060
  const conn = this.conn;
17904
18061
  this.conn = null;
17905
18062
  this.live = null;
@@ -19221,6 +19378,7 @@ function defaultAgent(wt, d, preview, onProcess) {
19221
19378
  onEvent: (event, seq) => d.hub.emit("agent", wt.id, seq, event),
19222
19379
  onStatus: (status) => d.hub.emit("agentStatus", wt.id, status),
19223
19380
  onStopping: () => d.hub.emit("agentStopping", wt.id),
19381
+ onHolding: () => d.hub.emit("agentHolding", wt.id),
19224
19382
  beforeTurn: async () => d.turnStarting?.(wt.id),
19225
19383
  onAuth: (agentId, o) => d.readiness?.observe(agentId, o),
19226
19384
  onLimits: (agentId, limits) => d.readiness?.observeLimits(agentId, limits),
@@ -21019,8 +21177,13 @@ function humanMs(ms) {
21019
21177
 
21020
21178
  // ../daemon/src/core/awake.ts
21021
21179
  var WAIT_MS = 30 * 60000;
21180
+ var BRIDGE_MS = 60000;
21181
+ var CARRIED_ENV = "TOYON_KEEP_AWAKE_CARRIED";
21182
+ function hasIdleSleep(raw, platform) {
21183
+ return raw !== "off" && platform === "darwin";
21184
+ }
21022
21185
  function idleSleepAssertion(raw, platform = process.platform) {
21023
- if (raw === "off" || platform !== "darwin")
21186
+ if (!hasIdleSleep(raw, platform))
21024
21187
  return null;
21025
21188
  return () => {
21026
21189
  const child = spawn3("/usr/bin/caffeinate", ["-i", "-w", String(process.pid)], { stdio: "ignore" });
@@ -21031,6 +21194,22 @@ function idleSleepAssertion(raw, platform = process.platform) {
21031
21194
  };
21032
21195
  };
21033
21196
  }
21197
+ function idleSleepBridge(raw, platform = process.platform) {
21198
+ if (!hasIdleSleep(raw, platform))
21199
+ return null;
21200
+ return (ms) => {
21201
+ const child = spawn3("/usr/bin/caffeinate", ["-i", "-t", String(Math.ceil(ms / 1000))], {
21202
+ stdio: "ignore",
21203
+ detached: true
21204
+ });
21205
+ child.on("error", (e) => log.warn("awake", "caffeinate did not start, so the Mac may sleep during the restart", e));
21206
+ child.unref();
21207
+ };
21208
+ }
21209
+ function carryEnv(env, held) {
21210
+ const { [CARRIED_ENV]: _stale, ...rest } = env;
21211
+ return held ? { ...rest, [CARRIED_ENV]: "1" } : rest;
21212
+ }
21034
21213
 
21035
21214
  class KeepAwake {
21036
21215
  d;
@@ -21046,6 +21225,10 @@ class KeepAwake {
21046
21225
  this.d = d;
21047
21226
  this.waitMs = d.waitMs ?? WAIT_MS;
21048
21227
  this.now = d.now ?? Date.now;
21228
+ if (d.carried && d.assert) {
21229
+ this.remoteLeftAt = this.now();
21230
+ log.info("awake", `the last daemon was holding off idle sleep; holding on for ${humanMs(this.waitMs)} unless a device comes back`);
21231
+ }
21049
21232
  d.hub.on("agentStatus", (_id, status) => {
21050
21233
  if (status === "waiting")
21051
21234
  this.waitingAt = this.now();
@@ -21067,10 +21250,14 @@ class KeepAwake {
21067
21250
  this.remote = n;
21068
21251
  this.reconsider();
21069
21252
  }
21070
- stop() {
21253
+ stop(opts = {}) {
21254
+ const held = this.release !== null;
21071
21255
  this.gone = true;
21072
21256
  this.disarm();
21257
+ if (held && opts.handoff)
21258
+ this.d.bridge?.(BRIDGE_MS);
21073
21259
  this.hold(false);
21260
+ return held;
21074
21261
  }
21075
21262
  reconsider() {
21076
21263
  if (this.gone || !this.d.assert)
@@ -21655,12 +21842,12 @@ async function replacementRuns() {
21655
21842
  reason: `the bun Toyon runs on did not start (${process.execPath}: ${why}); an install may still be writing it`
21656
21843
  };
21657
21844
  }
21658
- function respawn(logFile) {
21845
+ function respawn(logFile, env = process.env) {
21659
21846
  const logFd = openLog(logFile);
21660
21847
  const child = spawn4(process.execPath, process.argv.slice(1), {
21661
21848
  detached: true,
21662
21849
  stdio: ["ignore", logFd, logFd],
21663
- env: { ...process.env }
21850
+ env: { ...env }
21664
21851
  });
21665
21852
  child.on("error", (e) => log.warn("daemon", `the replacement did not start: ${e.message}`));
21666
21853
  child.unref();
@@ -29809,6 +29996,9 @@ var handlers = {
29809
29996
  throw new UserError("that attachment is no longer on this machine");
29810
29997
  s.files.revealPath(path);
29811
29998
  },
29999
+ "reveal-transcript"(msg, _ctx, s) {
30000
+ s.files.revealPath(s.worktrees.transcriptPath(msg.worktreeId));
30001
+ },
29812
30002
  async "discard-files"(msg, _ctx, s) {
29813
30003
  try {
29814
30004
  for (const path of msg.paths)
@@ -30572,6 +30762,51 @@ function createFetch(opts) {
30572
30762
  };
30573
30763
  }
30574
30764
 
30765
+ // ../daemon/src/server/rows.ts
30766
+ var rowsStampOf = (rowsJson, trunksJson) => Bun.hash(`{"rows":[${rowsJson.join(",")}],"trunks":${trunksJson}}`).toString(16);
30767
+
30768
+ class Published {
30769
+ byId = new Map;
30770
+ ids = [];
30771
+ trunks = {};
30772
+ trunksJson = "{}";
30773
+ stampNow = "";
30774
+ current() {
30775
+ return { rows: this.ids.map((id) => this.byId.get(id).row), trunks: this.trunks, stamp: this.stampNow };
30776
+ }
30777
+ publish(next) {
30778
+ const seen = new Map;
30779
+ const ids = [];
30780
+ const jsons = [];
30781
+ const changed = [];
30782
+ for (const row of next.rows) {
30783
+ const json = JSON.stringify(row);
30784
+ ids.push(row.id);
30785
+ jsons.push(json);
30786
+ if (this.byId.get(row.id)?.json !== json)
30787
+ changed.push(row);
30788
+ seen.set(row.id, { row, json });
30789
+ }
30790
+ const trunksJson = JSON.stringify(next.trunks);
30791
+ const trunksMoved = trunksJson !== this.trunksJson;
30792
+ const orderMoved = ids.length !== this.ids.length || ids.some((id, i) => id !== this.ids[i]);
30793
+ this.byId = seen;
30794
+ this.ids = ids;
30795
+ this.trunks = next.trunks;
30796
+ this.trunksJson = trunksJson;
30797
+ this.stampNow = rowsStampOf(jsons, trunksJson);
30798
+ if (changed.length === 0 && !trunksMoved && !orderMoved)
30799
+ return null;
30800
+ return {
30801
+ t: "worktrees",
30802
+ ids,
30803
+ rows: changed,
30804
+ ...trunksMoved ? { trunks: next.trunks } : {},
30805
+ stamp: this.stampNow
30806
+ };
30807
+ }
30808
+ }
30809
+
30575
30810
  // ../daemon/src/server/stamps.ts
30576
30811
  var stampOf = (part) => Bun.hash(JSON.stringify(part)).toString(16);
30577
30812
  var visitsStamp = (visits) => stampOf(Object.entries(visits).map(([repoId, list]) => [repoId, list.map((e) => [e.path, e.title ?? ""])]));
@@ -30725,6 +30960,22 @@ function startServer(opts) {
30725
30960
  agents: s.agents.adapters(),
30726
30961
  sockets: [...sockets].map((ws) => ({ subs: [...ws.data.subs], sent: ws.data.sent, bytes: ws.data.bytes }))
30727
30962
  });
30963
+ const published = new Published;
30964
+ const sendRows = (frame) => {
30965
+ const json = JSON.stringify(frame);
30966
+ for (const ws of sockets)
30967
+ if (ws.data.helloed)
30968
+ raw(ws, json);
30969
+ };
30970
+ const publishRows = () => {
30971
+ try {
30972
+ const frame = published.publish(s.worktrees.snapshot());
30973
+ if (frame)
30974
+ sendRows(frame);
30975
+ } catch (e) {
30976
+ log.warn("ws", "worktrees broadcast failed", e);
30977
+ }
30978
+ };
30728
30979
  let statusesQueued = false;
30729
30980
  const worktreesChanged = () => {
30730
30981
  if (statusesQueued)
@@ -30732,20 +30983,16 @@ function startServer(opts) {
30732
30983
  statusesQueued = true;
30733
30984
  setTimeout(() => {
30734
30985
  statusesQueued = false;
30735
- try {
30736
- const { rows, trunks } = s.worktrees.snapshot();
30737
- broadcast({ t: "worktrees", rows, trunks, stamp: stampOf({ rows, trunks }) });
30738
- } catch (e) {
30739
- log.warn("ws", "worktrees broadcast failed", e);
30740
- }
30986
+ publishRows();
30741
30987
  }, 0);
30742
30988
  };
30743
30989
  s.hub.on("worktreesChanged", worktreesChanged);
30744
30990
  s.hub.on("agentStatus", worktreesChanged);
30745
30991
  s.hub.on("agentStopping", worktreesChanged);
30992
+ s.hub.on("agentHolding", worktreesChanged);
30993
+ s.hub.on("proc", worktreesChanged);
30746
30994
  s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos, stamp: stampOf(s.state.repos) }));
30747
30995
  s.hub.on("pendingChanged", () => broadcast({ t: "pending-repos", pending: s.repos.pending }));
30748
- s.hub.on("proc", (worktreeId, proc) => broadcast({ t: "proc", worktreeId, proc }));
30749
30996
  s.hub.on("log", (worktreeId, proc, line, retract) => sendTo(worktreeId, retract ? { t: "log", worktreeId, proc, line, retract } : { t: "log", worktreeId, proc, line }));
30750
30997
  s.hub.on("agent", (worktreeId, seq, event) => sendTo(worktreeId, { t: "agent", worktreeId, seq, event }));
30751
30998
  s.hub.on("queue", (worktreeId, items) => sendTo(worktreeId, { t: "queue", worktreeId, items }));
@@ -30814,15 +31061,19 @@ function startServer(opts) {
30814
31061
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
30815
31062
  s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
30816
31063
  s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
30817
- const helloFrame = async (known = null) => {
31064
+ const helloReads = async () => {
30818
31065
  await s.update.refresh();
30819
- const { rows, trunks } = s.worktrees.snapshot();
31066
+ return { registry: await s.update.registry(), gitIdentity: await s.repos.gitIdentity() };
31067
+ };
31068
+ const helloNow = (read, known) => {
31069
+ publishRows();
31070
+ const { rows, trunks } = published.current();
30820
31071
  const parts = { rows, trunks, repos: s.state.repos, visits: s.routes.historyAll(), themes: s.themes.themes };
30821
31072
  const full = {
30822
31073
  t: "hello",
30823
31074
  version,
30824
31075
  install: s.update.install(),
30825
- registry: await s.update.registry(),
31076
+ registry: read.registry,
30826
31077
  protocol: PROTOCOL_VERSION,
30827
31078
  ...parts,
30828
31079
  stamps: stampsOf(parts),
@@ -30839,7 +31090,7 @@ function startServer(opts) {
30839
31090
  machines: s.state.machines,
30840
31091
  letIn: s.state.letIn,
30841
31092
  knocks: opts.knocks.pending(),
30842
- gitIdentity: await s.repos.gitIdentity(),
31093
+ gitIdentity: read.gitIdentity,
30843
31094
  pending: s.repos.pending,
30844
31095
  self: s.self.get(),
30845
31096
  update: s.update.get(),
@@ -30887,7 +31138,7 @@ function startServer(opts) {
30887
31138
  }),
30888
31139
  managed: { source: opts.managed.source, hash: opts.managed.hash },
30889
31140
  preview: (id) => s.runtime.get(id)?.proxy?.handler ?? null,
30890
- bootstrap: helloFrame,
31141
+ bootstrap: async () => helloNow(await helloReads(), null),
30891
31142
  manifestGround: () => s.themes.current().colors.surface1,
30892
31143
  open: (path) => s.opens.open(path),
30893
31144
  restart: (now) => s.restarter.request({ now }),
@@ -30912,7 +31163,9 @@ function startServer(opts) {
30912
31163
  }
30913
31164
  sockets.add(ws);
30914
31165
  link(ws);
30915
- send(ws, await helloFrame(ws.data.known ?? null));
31166
+ const read = await helloReads();
31167
+ send(ws, helloNow(read, ws.data.known ?? null));
31168
+ ws.data.helloed = true;
30916
31169
  fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
30917
31170
  for (const o of s.opens.takePending())
30918
31171
  send(ws, openedFrame(o));
@@ -32496,6 +32749,146 @@ async function recentSubjects(path, defaultBr) {
32496
32749
  `).filter(Boolean) : [];
32497
32750
  }
32498
32751
 
32752
+ // ../daemon/src/worktrees/lookup.ts
32753
+ import { readFile as readFile3 } from "fs/promises";
32754
+ var ARCHIVED_MAX = 20;
32755
+ var ASKED_MAX = 200;
32756
+
32757
+ class WorktreeLookup {
32758
+ d;
32759
+ constructor(d) {
32760
+ this.d = d;
32761
+ }
32762
+ tool(worktreeId) {
32763
+ const wt = this.d.state.worktree(worktreeId);
32764
+ const open = wt ? this.siblings(wt).length : 0;
32765
+ const gone = wt ? this.d.archived(wt.repoId).length : 0;
32766
+ return {
32767
+ name: WORKTREES_TOOL,
32768
+ description: `List the other worktrees of this project, open and archived, by title and branch, with where each chat is on disk and what it was about. Call it to find which worktree worked on a topic, a ticket or a feature, or to see what else is open. Right now ${open} other ${open === 1 ? "worktree is" : "worktrees are"} open and ${gone} ${gone === 1 ? "is" : "are"} archived. Without a query the open ones are listed and the newest ${ARCHIVED_MAX} archived; with one, only the worktrees whose title, branch or chat mention every word of it.`,
32769
+ inputSchema: {
32770
+ type: "object",
32771
+ properties: {
32772
+ query: {
32773
+ type: "string",
32774
+ description: "Words to look for, such as a ticket key, a product name or a feature: a worktree is listed when its title, branch or what the person wrote in its chat contains every word, in any case. Leave it out to list everything."
32775
+ }
32776
+ }
32777
+ }
32778
+ };
32779
+ }
32780
+ async call(worktreeId, args) {
32781
+ const wt = this.d.state.worktree(worktreeId);
32782
+ if (!wt)
32783
+ return { text: "unknown worktree", isError: true };
32784
+ const raw = args && typeof args === "object" ? args.query : undefined;
32785
+ const query = typeof raw === "string" ? raw.trim() : "";
32786
+ const words = query.toLowerCase().split(/\s+/).filter(Boolean);
32787
+ const open = await Promise.all(this.siblings(wt).map((w) => this.read(w)));
32788
+ const archived = await Promise.all(this.d.archived(wt.repoId).map((a) => this.readArchived(a)));
32789
+ open.sort((a, b) => b.active - a.active);
32790
+ if (words.length === 0) {
32791
+ const shown = [...open, ...archived.slice(0, ARCHIVED_MAX)];
32792
+ const more = archived.length - ARCHIVED_MAX;
32793
+ const head = `${open.length} other ${open.length === 1 ? "worktree" : "worktrees"} of this project ${open.length === 1 ? "is" : "are"} open and ${archived.length} ${archived.length === 1 ? "is" : "are"} archived.`;
32794
+ const tail = more > 0 ? `
32795
+
32796
+ ${more} older archived ${more === 1 ? "worktree is" : "worktrees are"} not listed; pass a query to search them.` : "";
32797
+ return { text: shown.length ? `${head}
32798
+
32799
+ ${shown.map((c) => row(c, [])).join(`
32800
+
32801
+ `)}${tail}` : head };
32802
+ }
32803
+ const hits = [...open, ...archived].filter((c) => matches(c, words));
32804
+ if (hits.length === 0) {
32805
+ return {
32806
+ text: `No worktree of this project matches "${query}". Searched ${open.length} open and ${archived.length} archived by title, branch and what the person wrote in each chat.`
32807
+ };
32808
+ }
32809
+ return {
32810
+ text: `${hits.length} ${hits.length === 1 ? "worktree" : "worktrees"} of this project ${hits.length === 1 ? "matches" : "match"} "${query}".
32811
+
32812
+ ${hits.map((c) => row(c, words)).join(`
32813
+
32814
+ `)}`
32815
+ };
32816
+ }
32817
+ siblings(wt) {
32818
+ return this.d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree");
32819
+ }
32820
+ async read(w) {
32821
+ const transcript = transcriptPathFor(this.d.transcriptsDir, w.id);
32822
+ const said = await userMessages(transcript);
32823
+ return {
32824
+ sibling: { branch: w.branch, title: w.title, path: w.path, transcript },
32825
+ said,
32826
+ active: said.at(-1)?.ts ?? w.createdAt
32827
+ };
32828
+ }
32829
+ async readArchived(a) {
32830
+ const said = await userMessages(a.transcript);
32831
+ return {
32832
+ sibling: {
32833
+ branch: a.branch,
32834
+ title: a.title,
32835
+ path: a.path,
32836
+ transcript: a.transcript,
32837
+ archived: { at: a.archivedAt, landed: a.landed, restorable: a.restorable }
32838
+ },
32839
+ said,
32840
+ prompt: a.prompt,
32841
+ active: said.at(-1)?.ts ?? a.archivedAt
32842
+ };
32843
+ }
32844
+ }
32845
+ async function userMessages(transcript) {
32846
+ let text;
32847
+ try {
32848
+ text = await readFile3(transcript, "utf8");
32849
+ } catch {
32850
+ return [];
32851
+ }
32852
+ const out = [];
32853
+ for (const line of text.split(`
32854
+ `)) {
32855
+ if (!line.includes('"user-message"'))
32856
+ continue;
32857
+ try {
32858
+ const e = JSON.parse(line)?.event;
32859
+ if (e?.type === "user-message" && typeof e.text === "string")
32860
+ out.push({ text: e.text, ts: Number(e.ts) || 0 });
32861
+ } catch {}
32862
+ }
32863
+ return out;
32864
+ }
32865
+ function matches(c, words) {
32866
+ const corpus = [c.sibling.title, c.sibling.branch, c.prompt ?? "", ...c.said.map((s) => s.text)].join(`
32867
+ `).toLowerCase();
32868
+ return words.every((w) => corpus.includes(w));
32869
+ }
32870
+ function row(c, words) {
32871
+ const parts = [siblingParagraph(c.sibling)];
32872
+ const first = c.said[0]?.text ?? c.prompt;
32873
+ if (first)
32874
+ parts.push(`The person first asked: "${clip2(first)}"${first.length > ASKED_MAX ? " (cut there)." : ""}`);
32875
+ if (c.said.length > 0)
32876
+ parts.push(`They last wrote there on ${dayOf(c.active)}.`);
32877
+ if (words.length > 0 && c.said.length > 0) {
32878
+ const mentioning = c.said.filter((s) => {
32879
+ const t = s.text.toLowerCase();
32880
+ return words.some((w) => t.includes(w));
32881
+ }).length;
32882
+ if (mentioning > 0)
32883
+ parts.push(`${mentioning} of their ${c.said.length} messages mention the query.`);
32884
+ }
32885
+ return parts.join(" ");
32886
+ }
32887
+ function clip2(text) {
32888
+ const one = text.replace(/\s+/g, " ").trim();
32889
+ return one.length > ASKED_MAX ? one.slice(0, ASKED_MAX).trimEnd() : one;
32890
+ }
32891
+
32499
32892
  // ../daemon/src/worktrees/pieces.ts
32500
32893
  import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
32501
32894
  import { readFileSync as readFileSync20 } from "fs";
@@ -32554,6 +32947,7 @@ var PROJECT_MAX = 200;
32554
32947
  var refused = (text) => ({ text, isError: true });
32555
32948
  var HELD = "Handed to the person; the list shows beside your plan and they decide where each piece goes. Ask for plan approval now.";
32556
32949
  var SHOWN = "Handed to the person as a card; they decide. Finish your turn.";
32950
+ var SHOWN_HERE = "Handed to the person as a card. This worktree holds no work yet, so the card leads with doing it here; they decide. Finish your turn.";
32557
32951
  function openPlanCard(entries) {
32558
32952
  const open = new Map;
32559
32953
  for (const { event } of entries) {
@@ -32727,7 +33121,7 @@ class PiecesService {
32727
33121
  },
32728
33122
  after: {
32729
33123
  type: "string",
32730
- description: 'worktree only: what the piece waits for before it starts: a branch, another piece of this list by its title, or "this" for your own branch.'
33124
+ description: 'worktree: what the piece waits for before it starts: a branch, another piece of this list by its title, or "this" for your own branch. here: what your own work on the piece waits for, a branch or another piece of this list; Toyon shows the wait under the box until it lands.'
32731
33125
  },
32732
33126
  to: { type: "string", description: "send only: the open worktree it goes to, by branch or title." },
32733
33127
  project: {
@@ -32812,19 +33206,35 @@ class PiecesService {
32812
33206
  return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
32813
33207
  piece.project = { id: found.repo.id, name: found.repo.name };
32814
33208
  }
32815
- if (p.where !== "worktree" && p.after !== undefined) {
32816
- return refused(`Piece ${i + 1} ("${title}") has \`after\`, which only a \`worktree\` piece takes.`);
33209
+ if (p.where === "send" && p.after !== undefined) {
33210
+ return refused(`Piece ${i + 1} ("${title}") has \`after\`, which a \`send\` piece does not take.`);
32817
33211
  }
32818
33212
  if (p.where !== "send" && p.to !== undefined) {
32819
33213
  return refused(`Piece ${i + 1} ("${title}") has \`to\`, which only a \`send\` piece takes.`);
32820
33214
  }
32821
- if (p.where === "worktree" && p.after !== undefined) {
33215
+ if (p.after !== undefined) {
32822
33216
  const q = p.after.trim();
32823
33217
  if (!q)
32824
33218
  return refused(`Piece ${i + 1} ("${title}") has a blank \`after\`; name what it waits for, or leave it out.`);
32825
- const after = this.resolveAfter(wt, raw, i, q);
33219
+ const after = this.resolveAfter(wt, raw, i, q, p.where === "here");
32826
33220
  if ("refused" in after)
32827
33221
  return refused(`Piece ${i + 1} ("${title}"): ${after.refused}`);
33222
+ if (p.where === "here" && !("piece" in after.after) && after.after.id === wt.id) {
33223
+ return refused(`Piece ${i + 1} ("${title}") is \`here\` and waits for its own branch.`);
33224
+ }
33225
+ if (p.where === "here" && "piece" in after.after) {
33226
+ const on = raw[Number(after.after.piece.slice(1)) - 1];
33227
+ const onTitle = sentenceCase(on.title.trim());
33228
+ if (on.where === "here") {
33229
+ return refused(`Piece ${i + 1} ("${title}") waits for "${onTitle}", which is \`here\` too and lands with it.`);
33230
+ }
33231
+ const back = on.after?.trim() ?? "";
33232
+ const bare = back.startsWith("@") ? back.slice(1) : back;
33233
+ const onOrigin = bare !== "" && (bare === "this" || bare === wt.branch || raw.some((o) => o.where === "here" && matchBy([o], bare, [(x) => x.title.trim()]).length > 0));
33234
+ if (onOrigin) {
33235
+ return refused(`Piece ${i + 1} ("${title}") waits for "${onTitle}", which waits for this branch: a cycle.`);
33236
+ }
33237
+ }
32828
33238
  piece.after = after.after;
32829
33239
  }
32830
33240
  if (p.where === "send") {
@@ -32850,6 +33260,9 @@ class PiecesService {
32850
33260
  agent.note({ type: "pieces-declined", id: o.id, reason: "handed over again", ts: Date.now() });
32851
33261
  }
32852
33262
  const held = by !== "person" && wt.mode === "plan";
33263
+ const here = by === "agent" && !held && this.offerHere(wt, pieces);
33264
+ if (here)
33265
+ pieces[0].fate = "here";
32853
33266
  const plan = held ? null : writePlanDocNow(wt.path, listDoc(pieces));
32854
33267
  agent.note({
32855
33268
  type: "pieces-proposed",
@@ -32862,18 +33275,30 @@ class PiecesService {
32862
33275
  ...by === "toyon" && read ? { read } : {},
32863
33276
  ts: Date.now()
32864
33277
  });
32865
- return { text: held ? HELD : SHOWN };
33278
+ return { text: held ? HELD : here ? SHOWN_HERE : SHOWN };
32866
33279
  }
32867
- resolveAfter(wt, raw, at, q) {
33280
+ offerHere(wt, pieces) {
33281
+ if (pieces.length !== 1)
33282
+ return false;
33283
+ const p = pieces[0];
33284
+ if (p.fate !== "worktree" || p.project || p.to || p.after)
33285
+ return false;
33286
+ const counts = this.d.counts(wt.id);
33287
+ return counts?.dirty === 0 && counts.ahead === 0;
33288
+ }
33289
+ resolveAfter(wt, raw, at, q, anywhere = false) {
32868
33290
  const bare = q.startsWith("@") ? q.slice(1) : q;
32869
33291
  const titled = raw.map((p, i) => ({ i, title: p.title.trim() }));
32870
- const onList = matchBy(titled.filter((p) => p.i < at), bare, [(p) => p.title]);
33292
+ const onList = matchBy(titled.filter((p) => anywhere ? p.i !== at : p.i < at), bare, [(p) => p.title]);
32871
33293
  if (onList.length === 1)
32872
33294
  return { after: { piece: `p${onList[0].i + 1}` } };
32873
33295
  if (onList.length > 1)
32874
33296
  return { refused: `"${q}" names more than one piece on the list; give them different titles.` };
32875
- if (matchBy(titled.filter((p) => p.i >= at), bare, [(p) => p.title]).length > 0) {
32876
- return { refused: `"${q}" is this piece or a later one; a piece waits only for one earlier on the list.` };
33297
+ const rest = matchBy(titled.filter((p) => anywhere ? p.i === at : p.i >= at), bare, [(p) => p.title]);
33298
+ if (rest.length > 0) {
33299
+ return {
33300
+ refused: anywhere ? `"${q}" is this piece itself.` : `"${q}" is this piece or a later one; a piece waits only for one earlier on the list.`
33301
+ };
32877
33302
  }
32878
33303
  if (bare === "this" || bare === wt.branch)
32879
33304
  return { after: { id: wt.id, branch: wt.branch, title: wt.title } };
@@ -32912,6 +33337,13 @@ class PiecesService {
32912
33337
  throw new UserError("That piece is not on the list");
32913
33338
  if (fate === "send" && !piece.to)
32914
33339
  throw new UserError("That piece has no worktree to send to");
33340
+ const after = piece.after;
33341
+ if (fate === "worktree" && after && "piece" in after) {
33342
+ const at = proposed.pieces.findIndex((p) => p.id === pieceId);
33343
+ if (proposed.pieces.findIndex((p) => p.id === after.piece) > at) {
33344
+ throw new UserError("That piece waits for one later on the list, so it is done here or skipped");
33345
+ }
33346
+ }
32915
33347
  if (piece.fate === "send" && (fate === "here" || fate === "worktree")) {
32916
33348
  throw new UserError("A piece for an open worktree is sent there or skipped");
32917
33349
  }
@@ -32969,6 +33401,7 @@ class PiecesService {
32969
33401
  }
32970
33402
  }
32971
33403
  agent.note({ type: "pieces", id, outcomes, ts: Date.now() });
33404
+ this.recordAwaits(wt.id, entries, proposed, became);
32972
33405
  if (tell && outcomes.some((o) => o.fate !== "here")) {
32973
33406
  for (let i = 0;i < 30 && openPlanCard(agent.transcript()); i++)
32974
33407
  await Bun.sleep(100);
@@ -33105,7 +33538,9 @@ ${p.section}`;
33105
33538
  const crossed = piece.project ? { repoName: piece.project.name } : {};
33106
33539
  if (fate === "here" || fate === "skip") {
33107
33540
  became.set(piece.id, { fate });
33108
- return { pieceId: piece.id, fate };
33541
+ const waits = fate === "here" ? piece.after : undefined;
33542
+ const after = !waits ? undefined : ("piece" in waits) ? proposed.pieces.find((p) => p.id === waits.piece)?.title ?? waits.piece : waits.branch;
33543
+ return { pieceId: piece.id, fate, ...after ? { after } : {} };
33109
33544
  }
33110
33545
  if (fate === "send") {
33111
33546
  const target = piece.to ? this.d.state.worktree(piece.to.id) : undefined;
@@ -33152,6 +33587,23 @@ ${p.section}`;
33152
33587
  became.set(piece.id, { fate, worktree: { id: made.id, branch: made.branch, title: made.title } });
33153
33588
  return { pieceId: piece.id, fate, worktreeId: made.id, title: made.title, ...crossed };
33154
33589
  }
33590
+ recordAwaits(originId, entries, proposed, became) {
33591
+ const awaits = [];
33592
+ for (const piece of proposed.pieces) {
33593
+ const after = piece.after;
33594
+ if (!after || effectiveFate(entries, proposed, piece) !== "here")
33595
+ continue;
33596
+ const w = "piece" in after ? became.get(after.piece)?.worktree : after;
33597
+ if (w && !awaits.some((a) => a.id === w.id))
33598
+ awaits.push(w);
33599
+ }
33600
+ if (awaits.length === 0)
33601
+ return;
33602
+ const live = this.d.state.requireWorktree(originId);
33603
+ live.awaits = [...(live.awaits ?? []).filter((a) => !awaits.some((b) => b.id === a.id)), ...awaits];
33604
+ this.d.state.save();
33605
+ this.d.hub.emit("worktreesChanged");
33606
+ }
33155
33607
  waitsFor(wt, piece, became) {
33156
33608
  const after = piece.after;
33157
33609
  if (!after)
@@ -33216,7 +33668,10 @@ ${handedPrompt(null, note, at, via)}`
33216
33668
  return out;
33217
33669
  }
33218
33670
  async landed(landedId) {
33219
- this.tellOrigin(landedId);
33671
+ const told = new Set;
33672
+ const origin = this.tellOrigin(landedId);
33673
+ if (origin)
33674
+ told.add(origin);
33220
33675
  const madeNow = new Set;
33221
33676
  for (const origin of this.d.state.worktrees) {
33222
33677
  const due = origin.queued?.filter((q) => !("piece" in q.after) && q.after.id === landedId) ?? [];
@@ -33253,7 +33708,38 @@ ${handedPrompt(null, note, at, via)}`
33253
33708
  }
33254
33709
  }
33255
33710
  }
33256
- this.tellWaiters(landedId, madeNow);
33711
+ for (const id of this.tellWaiters(landedId, madeNow))
33712
+ told.add(id);
33713
+ this.tellAwaiting(landedId, told);
33714
+ }
33715
+ tellAwaiting(landedId, told) {
33716
+ const landed = this.d.state.worktree(landedId);
33717
+ const repo = landed ? this.d.state.repo(landed.repoId) : undefined;
33718
+ let changed = false;
33719
+ for (const w of this.d.state.worktrees) {
33720
+ if (!w.awaits?.some((a) => a.id === landedId))
33721
+ continue;
33722
+ w.awaits = w.awaits.filter((a) => a.id !== landedId);
33723
+ if (w.awaits.length === 0)
33724
+ delete w.awaits;
33725
+ changed = true;
33726
+ if (!landed || !repo || w.kind !== "worktree" || told.has(w.id))
33727
+ continue;
33728
+ this.d.runtime.ensureAgent(w).agent.note({
33729
+ type: "piece-landed",
33730
+ worktreeId: landed.id,
33731
+ repoName: repo.name,
33732
+ title: landed.title,
33733
+ ...landed.pr?.url ? { url: landed.pr.url } : {},
33734
+ ...w.repoId === landed.repoId ? { same: true } : {},
33735
+ awaited: true,
33736
+ ts: Date.now()
33737
+ });
33738
+ }
33739
+ if (changed) {
33740
+ this.d.state.save();
33741
+ this.d.hub.emit("worktreesChanged");
33742
+ }
33257
33743
  }
33258
33744
  repoint(origin, made, worktree) {
33259
33745
  const live = this.d.state.worktree(origin.id);
@@ -33298,13 +33784,13 @@ ${handedPrompt(null, note, at, via)}`
33298
33784
  tellOrigin(landedId) {
33299
33785
  const wt = this.d.state.worktree(landedId);
33300
33786
  if (wt?.from?.kind !== "worktree")
33301
- return;
33787
+ return null;
33302
33788
  const origin = this.d.state.worktree(wt.from.origin.id);
33303
33789
  if (origin?.kind !== "worktree")
33304
- return;
33790
+ return null;
33305
33791
  const repo = this.d.state.repo(wt.repoId);
33306
33792
  if (!repo)
33307
- return;
33793
+ return null;
33308
33794
  this.d.runtime.ensureAgent(origin).agent.note({
33309
33795
  type: "piece-landed",
33310
33796
  worktreeId: wt.id,
@@ -33314,19 +33800,22 @@ ${handedPrompt(null, note, at, via)}`
33314
33800
  ...wt.repoId === origin.repoId ? { same: true } : {},
33315
33801
  ts: Date.now()
33316
33802
  });
33803
+ return origin.id;
33317
33804
  }
33318
33805
  tellWaiters(landedId, madeNow) {
33319
33806
  const landed = this.d.state.worktree(landedId);
33320
33807
  if (!landed)
33321
- return;
33808
+ return [];
33322
33809
  const repo = this.d.state.repo(landed.repoId);
33323
33810
  if (!repo)
33324
- return;
33811
+ return [];
33812
+ const told = [];
33325
33813
  for (const w of this.d.state.worktrees) {
33326
33814
  if (w.kind !== "worktree" || w.landed || w.id === landedId || madeNow.has(w.id))
33327
33815
  continue;
33328
33816
  if (w.from?.kind !== "worktree" || w.from.after?.id !== landedId)
33329
33817
  continue;
33818
+ told.push(w.id);
33330
33819
  this.d.runtime.ensureAgent(w).agent.note({
33331
33820
  type: "piece-landed",
33332
33821
  worktreeId: landed.id,
@@ -33338,6 +33827,7 @@ ${handedPrompt(null, note, at, via)}`
33338
33827
  ts: Date.now()
33339
33828
  });
33340
33829
  }
33830
+ return told;
33341
33831
  }
33342
33832
  sweep() {
33343
33833
  let changed = false;
@@ -35705,7 +36195,7 @@ var idsOf = (rows, trunks) => [
35705
36195
  ...rows.map((r) => r.id),
35706
36196
  ...Object.values(trunks).map((t) => t.id)
35707
36197
  ];
35708
- var heldBy = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
36198
+ var heldBy2 = (found) => `${found.name} is held by another tool${found.lockReason ? `: ${found.lockReason}` : ""}`;
35709
36199
  var commandLine = (args) => args.map((a) => /^[\w./:=@,+-]+$/.test(a) ? a : JSON.stringify(a.split(`
35710
36200
  `, 1)[0])).join(" ");
35711
36201
 
@@ -35821,7 +36311,7 @@ class WorktreeService {
35821
36311
  const task = taskText(prompt, attachments);
35822
36312
  const given = cleanTitle2(opts.title ?? "") || undefined;
35823
36313
  const title = given ?? titleFrom(task);
35824
- const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
36314
+ const spare = opts.cold ? undefined : this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
35825
36315
  const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
35826
36316
  let carriedIn;
35827
36317
  let release;
@@ -35835,7 +36325,7 @@ class WorktreeService {
35835
36325
  profile,
35836
36326
  unnamed: given ? undefined : true
35837
36327
  };
35838
- const claimed = await this.spare.claim(repoId, title, opts.worktreeId, stamps, async (wt) => {
36328
+ const prepare = async (wt) => {
35839
36329
  const rt = this.d.runtime.ensureAgent(wt);
35840
36330
  release = rt.agent.hold();
35841
36331
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
@@ -35847,7 +36337,8 @@ class WorktreeService {
35847
36337
  attachments,
35848
36338
  ...born ? { handed: born.handed } : {}
35849
36339
  });
35850
- });
36340
+ };
36341
+ const claimed = spare ? await this.spare.claim(repoId, title, opts.worktreeId, stamps, prepare) : null;
35851
36342
  if (claimed) {
35852
36343
  const { wt } = claimed;
35853
36344
  const rt = this.d.runtime.ensureAgent(wt);
@@ -36064,7 +36555,7 @@ class WorktreeService {
36064
36555
  const target = canonical(r.path);
36065
36556
  const pushed = this.discoveredById(worktreeId);
36066
36557
  if (pushed?.locked)
36067
- throw new UserError(heldBy(pushed));
36558
+ throw new UserError(heldBy2(pushed));
36068
36559
  if (!r.branch)
36069
36560
  throw new UserError(`${r.name} is detached: check out a branch in it first`);
36070
36561
  const enclosing = [...this.d.state.repos, ...this.d.state.worktrees].find((w) => isInside(target, canonical(w.path)));
@@ -36091,7 +36582,7 @@ class WorktreeService {
36091
36582
  await withRepoLock(repo.path, async () => {
36092
36583
  const found = await this.requireDiscovered(repoId, repo.path, target, wt.id);
36093
36584
  if (found.locked)
36094
- throw new UserError(heldBy(found));
36585
+ throw new UserError(heldBy2(found));
36095
36586
  if (!found.branch)
36096
36587
  throw new UserError(`${found.name} is detached: check out a branch in it first`);
36097
36588
  wt.branch = found.branch;
@@ -36960,7 +37451,7 @@ ${wt.landing.body}` : s;
36960
37451
  return this.landOp(wt, repo, message);
36961
37452
  });
36962
37453
  if (out.result.ok && out.landed)
36963
- this.noteLanded(wt, out.result.message, out.mark);
37454
+ this.noteLanded(wt, out.result.message, out.mark, out.url);
36964
37455
  return { result: this.fixShip(wt, out.result, baseOf(repo)) };
36965
37456
  }
36966
37457
  fixShip(wt, result, base) {
@@ -37005,8 +37496,14 @@ ${wt.landing.body}` : s;
37005
37496
  return null;
37006
37497
  return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
37007
37498
  }
37008
- noteLanded(wt, message, mark) {
37009
- this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, ...mark ? { mark } : {}, ts: Date.now() });
37499
+ noteLanded(wt, message, mark, url) {
37500
+ this.d.runtime.ensureAgent(wt).agent.note({
37501
+ type: "landed",
37502
+ message,
37503
+ ...mark ? { mark } : {},
37504
+ ...url ? { url } : {},
37505
+ ts: Date.now()
37506
+ });
37010
37507
  this.d.hub.emit("landed", wt.id);
37011
37508
  }
37012
37509
  async followBranch(wt, current) {
@@ -37067,7 +37564,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
37067
37564
  if (policy.land === "pr") {
37068
37565
  if (wt.pr?.state === "merged" && !prTaken(wt)) {
37069
37566
  const { mark, ...result } = await this.prMergedOp(wt.id, undefined, w);
37070
- return result.ok ? { result, landed: true, mark } : { result };
37567
+ return result.ok ? { result, landed: true, mark, url: wt.pr.url } : { result };
37071
37568
  }
37072
37569
  const fetched = await this.fetchBase(wt, repo, w);
37073
37570
  if (!fetched.ok)
@@ -37107,7 +37604,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
37107
37604
  const { mark: noted, ...r } = await this.prMergedOp(wt.id, mark, w);
37108
37605
  const on = `origin's ${repo.defaultBranch} has this work already`;
37109
37606
  const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
37110
- return r.ok ? { result, landed: true, mark: noted } : { result };
37607
+ return r.ok ? { result, landed: true, mark: noted, url: wt.pr?.url } : { result };
37111
37608
  }
37112
37609
  const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
37113
37610
  const result = await openPr({
@@ -37296,7 +37793,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
37296
37793
  else
37297
37794
  log.warn(worktreeId, `landing failed: ${result.message}`);
37298
37795
  if (result.ok && wt)
37299
- this.noteLanded(wt, result.message, mark);
37796
+ this.noteLanded(wt, result.message, mark, wt.pr?.url);
37300
37797
  return result;
37301
37798
  }
37302
37799
  async prMergedOp(worktreeId, carried, w = UNWATCHED) {
@@ -37662,6 +38159,16 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
37662
38159
  }
37663
38160
  return this.d.state.requireWorktree(worktreeId);
37664
38161
  }
38162
+ writtenTranscript(worktreeId) {
38163
+ const path = transcriptPathFor(this.d.paths.transcriptsDir, worktreeId);
38164
+ return existsSync36(path) ? path : undefined;
38165
+ }
38166
+ transcriptPath(id) {
38167
+ const path = this.d.state.worktree(id) ? this.writtenTranscript(id) : this.archive.chatFiles(id)?.transcript;
38168
+ if (!path || !existsSync36(path))
38169
+ throw new UserError("nothing has been written to this chat yet");
38170
+ return path;
38171
+ }
37665
38172
  async send(worktreeId, msg) {
37666
38173
  await this.archivingNow(worktreeId);
37667
38174
  if (!this.d.state.worktree(worktreeId) && this.hasArchived(worktreeId)) {
@@ -37755,6 +38262,8 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
37755
38262
  borrowed: rt?.borrowed.size ? [...rt.borrowed] : undefined,
37756
38263
  agent: rt?.agent.status ?? "idle",
37757
38264
  stopping: rt?.agent.stopping || undefined,
38265
+ background: rt?.agent.holding || undefined,
38266
+ wakes: rt?.agent.wakes ?? undefined,
37758
38267
  login: !!rt?.login,
37759
38268
  ahead,
37760
38269
  behind,
@@ -37763,7 +38272,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
37763
38272
  queued: rt?.agent.queueLength || undefined,
37764
38273
  unseen: isUnseen(wt) || undefined,
37765
38274
  usage: this.usageFor(wt.id),
37766
- transcript: transcriptPathFor(this.d.paths.transcriptsDir, wt.id),
38275
+ transcript: this.writtenTranscript(wt.id),
37767
38276
  sessionId: this.d.state.session(wt.id),
37768
38277
  shipping: this.ops.get(wt.id),
37769
38278
  syncQueued: this.syncQueued.has(wt.id) || undefined
@@ -38015,9 +38524,11 @@ var pieces = new PiecesService({
38015
38524
  runtime,
38016
38525
  attachments: attachments2,
38017
38526
  create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts),
38527
+ counts: (id) => worktrees.tree.counts(id),
38018
38528
  reader: makePiecesReader(runtime, agents, state)
38019
38529
  });
38020
38530
  tools.add(pieces);
38531
+ tools.add(new WorktreeLookup({ state, transcriptsDir: paths.transcriptsDir, archived: (repoId) => worktrees.archived(repoId) }));
38021
38532
  var turns = new TurnService({
38022
38533
  state,
38023
38534
  hub,
@@ -38127,6 +38638,8 @@ var keepAwake = new KeepAwake({
38127
38638
  hub,
38128
38639
  demand: () => runtime.demand(),
38129
38640
  assert: idleSleepAssertion(process.env.TOYON_KEEP_AWAKE),
38641
+ bridge: idleSleepBridge(process.env.TOYON_KEEP_AWAKE),
38642
+ carried: process.env[CARRIED_ENV] === "1",
38130
38643
  mode: () => state.keepAwake,
38131
38644
  answerable: () => setting.get() !== null
38132
38645
  });
@@ -38259,7 +38772,7 @@ async function shutdown(signal, opts = {}) {
38259
38772
  shuttingDown = true;
38260
38773
  log.info("daemon", `${signal}: stopping dev servers`);
38261
38774
  idleExit.stop();
38262
- keepAwake.stop();
38775
+ const heldAwake = keepAwake.stop({ handoff: opts.respawn === true });
38263
38776
  tailnet.stop();
38264
38777
  idle.shutdown();
38265
38778
  landing.stop();
@@ -38280,7 +38793,7 @@ async function shutdown(signal, opts = {}) {
38280
38793
  await Bun.sleep(250);
38281
38794
  log.info("daemon", "starting the replacement");
38282
38795
  try {
38283
- respawn(paths.logFile);
38796
+ respawn(paths.logFile, carryEnv(process.env, heldAwake));
38284
38797
  } catch (e) {
38285
38798
  log.error("daemon", "the replacement did not start; run `toyon` to start it again", e);
38286
38799
  process.exit(1);