toyon 0.18.0 → 0.19.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 (220) hide show
  1. package/dist/CHANGELOG.md +41 -0
  2. package/dist/bridge.js +1 -1
  3. package/dist/cli.js +2 -2
  4. package/dist/daemon.js +1719 -684
  5. package/dist/shell/assets/{Editor-CU91PIeX.js → Editor-B-1lrUVF.js} +3 -3
  6. package/dist/shell/assets/Editor-B-1lrUVF.js.br +0 -0
  7. package/dist/shell/assets/Editor-B-1lrUVF.js.gz +0 -0
  8. package/dist/shell/assets/Editor-CB2SckMT.css.br +0 -0
  9. package/dist/shell/assets/Editor-CB2SckMT.css.gz +0 -0
  10. package/dist/shell/assets/XTerm-6GBZ9nXN.css.br +0 -0
  11. package/dist/shell/assets/XTerm-6GBZ9nXN.css.gz +0 -0
  12. package/dist/shell/assets/{XTerm-y7vQoOEu.js → XTerm-CZl-vWEO.js} +1 -1
  13. package/dist/shell/assets/XTerm-CZl-vWEO.js.br +0 -0
  14. package/dist/shell/assets/XTerm-CZl-vWEO.js.gz +0 -0
  15. package/dist/shell/assets/abap-DLDM7-KI.js.br +0 -0
  16. package/dist/shell/assets/abap-DLDM7-KI.js.gz +0 -0
  17. package/dist/shell/assets/apex-DNDY2TF8.js.br +0 -0
  18. package/dist/shell/assets/apex-DNDY2TF8.js.gz +0 -0
  19. package/dist/shell/assets/bat-BwHxbl9M.js.br +0 -0
  20. package/dist/shell/assets/bat-BwHxbl9M.js.gz +0 -0
  21. package/dist/shell/assets/bicep-CFznDFnq.js.br +0 -0
  22. package/dist/shell/assets/bicep-CFznDFnq.js.gz +0 -0
  23. package/dist/shell/assets/cameligo-Bf6VGUru.js.br +0 -0
  24. package/dist/shell/assets/cameligo-Bf6VGUru.js.gz +0 -0
  25. package/dist/shell/assets/clojure-Dnu-v4kV.js.br +0 -0
  26. package/dist/shell/assets/clojure-Dnu-v4kV.js.gz +0 -0
  27. package/dist/shell/assets/coffee-Bd8akH9Z.js.br +0 -0
  28. package/dist/shell/assets/coffee-Bd8akH9Z.js.gz +0 -0
  29. package/dist/shell/assets/cpp-BbWJElDN.js.br +0 -0
  30. package/dist/shell/assets/cpp-BbWJElDN.js.gz +0 -0
  31. package/dist/shell/assets/csharp-Co3qMtFm.js.br +0 -0
  32. package/dist/shell/assets/csharp-Co3qMtFm.js.gz +0 -0
  33. package/dist/shell/assets/csp-D-4FJmMZ.js.br +0 -0
  34. package/dist/shell/assets/csp-D-4FJmMZ.js.gz +0 -0
  35. package/dist/shell/assets/css-DdJfP1eB.js.br +0 -0
  36. package/dist/shell/assets/css-DdJfP1eB.js.gz +0 -0
  37. package/dist/shell/assets/css.worker-CqwWTjeG.js.br +0 -0
  38. package/dist/shell/assets/css.worker-CqwWTjeG.js.gz +0 -0
  39. package/dist/shell/assets/{cssMode-B7sFzM6K.js → cssMode-BpigOakf.js} +1 -1
  40. package/dist/shell/assets/cssMode-BpigOakf.js.br +0 -0
  41. package/dist/shell/assets/cssMode-BpigOakf.js.gz +0 -0
  42. package/dist/shell/assets/cypher-cTPe9QuQ.js.br +0 -0
  43. package/dist/shell/assets/cypher-cTPe9QuQ.js.gz +0 -0
  44. package/dist/shell/assets/dart-BOtBlQCF.js.br +0 -0
  45. package/dist/shell/assets/dart-BOtBlQCF.js.gz +0 -0
  46. package/dist/shell/assets/dockerfile-BG73LgW2.js.br +0 -0
  47. package/dist/shell/assets/dockerfile-BG73LgW2.js.gz +0 -0
  48. package/dist/shell/assets/ecl-BEgZUVRK.js.br +0 -0
  49. package/dist/shell/assets/ecl-BEgZUVRK.js.gz +0 -0
  50. package/dist/shell/assets/editor.worker-DCKwvLbM.js.br +0 -0
  51. package/dist/shell/assets/editor.worker-DCKwvLbM.js.gz +0 -0
  52. package/dist/shell/assets/elixir-BkW5O-1t.js.br +0 -0
  53. package/dist/shell/assets/elixir-BkW5O-1t.js.gz +0 -0
  54. package/dist/shell/assets/flow9-BeJ5waoc.js.br +0 -0
  55. package/dist/shell/assets/flow9-BeJ5waoc.js.gz +0 -0
  56. package/dist/shell/assets/{freemarker2-ChTLQoDC.js → freemarker2-vvh2KLdJ.js} +1 -1
  57. package/dist/shell/assets/freemarker2-vvh2KLdJ.js.br +0 -0
  58. package/dist/shell/assets/freemarker2-vvh2KLdJ.js.gz +0 -0
  59. package/dist/shell/assets/fsharp-PahG7c26.js.br +0 -0
  60. package/dist/shell/assets/fsharp-PahG7c26.js.gz +0 -0
  61. package/dist/shell/assets/go-acbASCJo.js.br +0 -0
  62. package/dist/shell/assets/go-acbASCJo.js.gz +0 -0
  63. package/dist/shell/assets/{grammar-xXcxTc3V.js → grammar-DBMSnNcB.js} +1 -1
  64. package/dist/shell/assets/grammar-DBMSnNcB.js.br +0 -0
  65. package/dist/shell/assets/grammar-DBMSnNcB.js.gz +0 -0
  66. package/dist/shell/assets/graphql-BxJiqAUM.js.br +0 -0
  67. package/dist/shell/assets/graphql-BxJiqAUM.js.gz +0 -0
  68. package/dist/shell/assets/{handlebars-DPJSozQx.js → handlebars-ExFAR1bz.js} +1 -1
  69. package/dist/shell/assets/handlebars-ExFAR1bz.js.br +0 -0
  70. package/dist/shell/assets/handlebars-ExFAR1bz.js.gz +0 -0
  71. package/dist/shell/assets/hcl-DtV1sZF8.js.br +0 -0
  72. package/dist/shell/assets/hcl-DtV1sZF8.js.gz +0 -0
  73. package/dist/shell/assets/{html-Cog2rLcD.js → html-DpIWoe_L.js} +1 -1
  74. package/dist/shell/assets/html-DpIWoe_L.js.br +0 -0
  75. package/dist/shell/assets/html-DpIWoe_L.js.gz +0 -0
  76. package/dist/shell/assets/html.worker-DhJ4VZMp.js.br +0 -0
  77. package/dist/shell/assets/html.worker-DhJ4VZMp.js.gz +0 -0
  78. package/dist/shell/assets/{htmlMode-Xm5_ujKb.js → htmlMode-vCEOKCM-.js} +1 -1
  79. package/dist/shell/assets/htmlMode-vCEOKCM-.js.br +0 -0
  80. package/dist/shell/assets/htmlMode-vCEOKCM-.js.gz +0 -0
  81. package/dist/shell/assets/index-CHbNBHbD.css +1 -0
  82. package/dist/shell/assets/index-CHbNBHbD.css.br +0 -0
  83. package/dist/shell/assets/index-CHbNBHbD.css.gz +0 -0
  84. package/dist/shell/assets/index-D5GN5OOv.js +276 -0
  85. package/dist/shell/assets/index-D5GN5OOv.js.br +0 -0
  86. package/dist/shell/assets/index-D5GN5OOv.js.gz +0 -0
  87. package/dist/shell/assets/java-CXBNlu9o.js.br +0 -0
  88. package/dist/shell/assets/java-CXBNlu9o.js.gz +0 -0
  89. package/dist/shell/assets/{javascript-Dcv6lw7b.js → javascript-CF6f9y1O.js} +1 -1
  90. package/dist/shell/assets/json.worker-BF9eV5mS.js.br +0 -0
  91. package/dist/shell/assets/json.worker-BF9eV5mS.js.gz +0 -0
  92. package/dist/shell/assets/{jsonMode-D5_V0TW5.js → jsonMode-CZzpDv9U.js} +1 -1
  93. package/dist/shell/assets/jsonMode-CZzpDv9U.js.br +0 -0
  94. package/dist/shell/assets/jsonMode-CZzpDv9U.js.gz +0 -0
  95. package/dist/shell/assets/julia-cl7-CwDS.js.br +0 -0
  96. package/dist/shell/assets/julia-cl7-CwDS.js.gz +0 -0
  97. package/dist/shell/assets/kotlin-s7OhZKlX.js.br +0 -0
  98. package/dist/shell/assets/kotlin-s7OhZKlX.js.gz +0 -0
  99. package/dist/shell/assets/less-9HpZscsL.js.br +0 -0
  100. package/dist/shell/assets/less-9HpZscsL.js.gz +0 -0
  101. package/dist/shell/assets/lexon-OrD6JF1K.js.br +0 -0
  102. package/dist/shell/assets/lexon-OrD6JF1K.js.gz +0 -0
  103. package/dist/shell/assets/{liquid-C_zXIF5F.js → liquid-5T0ZgJhW.js} +1 -1
  104. package/dist/shell/assets/liquid-5T0ZgJhW.js.br +0 -0
  105. package/dist/shell/assets/liquid-5T0ZgJhW.js.gz +0 -0
  106. package/dist/shell/assets/{lspLanguageFeatures-CkiTdLmp.js → lspLanguageFeatures-DrkNrp6u.js} +1 -1
  107. package/dist/shell/assets/lspLanguageFeatures-DrkNrp6u.js.br +0 -0
  108. package/dist/shell/assets/lspLanguageFeatures-DrkNrp6u.js.gz +0 -0
  109. package/dist/shell/assets/lua-Cyyb5UIc.js.br +0 -0
  110. package/dist/shell/assets/lua-Cyyb5UIc.js.gz +0 -0
  111. package/dist/shell/assets/m3-B8OfTtLu.js.br +0 -0
  112. package/dist/shell/assets/m3-B8OfTtLu.js.gz +0 -0
  113. package/dist/shell/assets/markdown-BFxVWTOG.js.br +0 -0
  114. package/dist/shell/assets/markdown-BFxVWTOG.js.gz +0 -0
  115. package/dist/shell/assets/{mdx-gvbILQkr.js → mdx-DnLcCYKb.js} +1 -1
  116. package/dist/shell/assets/mdx-DnLcCYKb.js.br +0 -0
  117. package/dist/shell/assets/mdx-DnLcCYKb.js.gz +0 -0
  118. package/dist/shell/assets/mips-CiqrrVzr.js.br +0 -0
  119. package/dist/shell/assets/mips-CiqrrVzr.js.gz +0 -0
  120. package/dist/shell/assets/msdax-DmeGPVcC.js.br +0 -0
  121. package/dist/shell/assets/msdax-DmeGPVcC.js.gz +0 -0
  122. package/dist/shell/assets/mysql-C_tMU-Nz.js.br +0 -0
  123. package/dist/shell/assets/mysql-C_tMU-Nz.js.gz +0 -0
  124. package/dist/shell/assets/objective-c-BDtDVThU.js.br +0 -0
  125. package/dist/shell/assets/objective-c-BDtDVThU.js.gz +0 -0
  126. package/dist/shell/assets/pascal-vHIfCaH5.js.br +0 -0
  127. package/dist/shell/assets/pascal-vHIfCaH5.js.gz +0 -0
  128. package/dist/shell/assets/pascaligo-DtZ0uQbO.js.br +0 -0
  129. package/dist/shell/assets/pascaligo-DtZ0uQbO.js.gz +0 -0
  130. package/dist/shell/assets/perl-Ub6l9XKa.js.br +0 -0
  131. package/dist/shell/assets/perl-Ub6l9XKa.js.gz +0 -0
  132. package/dist/shell/assets/pgsql-BlNEE0v7.js.br +0 -0
  133. package/dist/shell/assets/pgsql-BlNEE0v7.js.gz +0 -0
  134. package/dist/shell/assets/php-BBUBE1dy.js.br +0 -0
  135. package/dist/shell/assets/php-BBUBE1dy.js.gz +0 -0
  136. package/dist/shell/assets/pla-DSh2-awV.js.br +0 -0
  137. package/dist/shell/assets/pla-DSh2-awV.js.gz +0 -0
  138. package/dist/shell/assets/postiats-CocnycG-.js.br +0 -0
  139. package/dist/shell/assets/postiats-CocnycG-.js.gz +0 -0
  140. package/dist/shell/assets/powerquery-tScXyioY.js.br +0 -0
  141. package/dist/shell/assets/powerquery-tScXyioY.js.gz +0 -0
  142. package/dist/shell/assets/powershell-COWaemsV.js.br +0 -0
  143. package/dist/shell/assets/powershell-COWaemsV.js.gz +0 -0
  144. package/dist/shell/assets/protobuf-Brw8urJB.js.br +0 -0
  145. package/dist/shell/assets/protobuf-Brw8urJB.js.gz +0 -0
  146. package/dist/shell/assets/pug-8SOpv6rk.js.br +0 -0
  147. package/dist/shell/assets/pug-8SOpv6rk.js.gz +0 -0
  148. package/dist/shell/assets/{python-CZTrOkh_.js → python-Di4WuyJe.js} +1 -1
  149. package/dist/shell/assets/python-Di4WuyJe.js.br +0 -0
  150. package/dist/shell/assets/python-Di4WuyJe.js.gz +0 -0
  151. package/dist/shell/assets/qsharp-Bw9ernYp.js.br +0 -0
  152. package/dist/shell/assets/qsharp-Bw9ernYp.js.gz +0 -0
  153. package/dist/shell/assets/r-j7ic8hl3.js.br +0 -0
  154. package/dist/shell/assets/r-j7ic8hl3.js.gz +0 -0
  155. package/dist/shell/assets/{razor-DowlBj1i.js → razor-NpEB1IHs.js} +1 -1
  156. package/dist/shell/assets/razor-NpEB1IHs.js.br +0 -0
  157. package/dist/shell/assets/razor-NpEB1IHs.js.gz +0 -0
  158. package/dist/shell/assets/redis-Bu5POkcn.js.br +0 -0
  159. package/dist/shell/assets/redis-Bu5POkcn.js.gz +0 -0
  160. package/dist/shell/assets/redshift-Bs9aos_-.js.br +0 -0
  161. package/dist/shell/assets/redshift-Bs9aos_-.js.gz +0 -0
  162. package/dist/shell/assets/restructuredtext-CqXO7rUv.js.br +0 -0
  163. package/dist/shell/assets/restructuredtext-CqXO7rUv.js.gz +0 -0
  164. package/dist/shell/assets/ruby-zBfavPgS.js.br +0 -0
  165. package/dist/shell/assets/ruby-zBfavPgS.js.gz +0 -0
  166. package/dist/shell/assets/rust-BzKRNQWT.js.br +0 -0
  167. package/dist/shell/assets/rust-BzKRNQWT.js.gz +0 -0
  168. package/dist/shell/assets/sb-BBc9UKZt.js.br +0 -0
  169. package/dist/shell/assets/sb-BBc9UKZt.js.gz +0 -0
  170. package/dist/shell/assets/scala-D9hQfWCl.js.br +0 -0
  171. package/dist/shell/assets/scala-D9hQfWCl.js.gz +0 -0
  172. package/dist/shell/assets/scheme-BPhDTwHR.js.br +0 -0
  173. package/dist/shell/assets/scheme-BPhDTwHR.js.gz +0 -0
  174. package/dist/shell/assets/scss-CBJaRo0y.js.br +0 -0
  175. package/dist/shell/assets/scss-CBJaRo0y.js.gz +0 -0
  176. package/dist/shell/assets/shell-DiJ1NA_G.js.br +0 -0
  177. package/dist/shell/assets/shell-DiJ1NA_G.js.gz +0 -0
  178. package/dist/shell/assets/solidity-Db0IVjzk.js.br +0 -0
  179. package/dist/shell/assets/solidity-Db0IVjzk.js.gz +0 -0
  180. package/dist/shell/assets/sophia-CnS9iZB_.js.br +0 -0
  181. package/dist/shell/assets/sophia-CnS9iZB_.js.gz +0 -0
  182. package/dist/shell/assets/sparql-CJmd_6j2.js.br +0 -0
  183. package/dist/shell/assets/sparql-CJmd_6j2.js.gz +0 -0
  184. package/dist/shell/assets/sql-ClhHkBeG.js.br +0 -0
  185. package/dist/shell/assets/sql-ClhHkBeG.js.gz +0 -0
  186. package/dist/shell/assets/st-CHwy0fLd.js.br +0 -0
  187. package/dist/shell/assets/st-CHwy0fLd.js.gz +0 -0
  188. package/dist/shell/assets/swift-Bqt4WxQ4.js.br +0 -0
  189. package/dist/shell/assets/swift-Bqt4WxQ4.js.gz +0 -0
  190. package/dist/shell/assets/systemverilog-Bs9z6M-B.js.br +0 -0
  191. package/dist/shell/assets/systemverilog-Bs9z6M-B.js.gz +0 -0
  192. package/dist/shell/assets/tcl-Dm6ycUr_.js.br +0 -0
  193. package/dist/shell/assets/tcl-Dm6ycUr_.js.gz +0 -0
  194. package/dist/shell/assets/ts.worker-DTZAwq0V.js.br +0 -0
  195. package/dist/shell/assets/ts.worker-DTZAwq0V.js.gz +0 -0
  196. package/dist/shell/assets/{tsMode-BNl2cV_K.js → tsMode-BylSuxTF.js} +1 -1
  197. package/dist/shell/assets/tsMode-BylSuxTF.js.br +0 -0
  198. package/dist/shell/assets/tsMode-BylSuxTF.js.gz +0 -0
  199. package/dist/shell/assets/twig-Csy3S7wG.js.br +0 -0
  200. package/dist/shell/assets/twig-Csy3S7wG.js.gz +0 -0
  201. package/dist/shell/assets/typespec-Btyra-wh.js.br +0 -0
  202. package/dist/shell/assets/typespec-Btyra-wh.js.gz +0 -0
  203. package/dist/shell/assets/vb-Db0cS2oM.js.br +0 -0
  204. package/dist/shell/assets/vb-Db0cS2oM.js.gz +0 -0
  205. package/dist/shell/assets/wasm-inlined-CG6Dc4jp.js.br +0 -0
  206. package/dist/shell/assets/wasm-inlined-CG6Dc4jp.js.gz +0 -0
  207. package/dist/shell/assets/wgsl-BTesnYfV.js.br +0 -0
  208. package/dist/shell/assets/wgsl-BTesnYfV.js.gz +0 -0
  209. package/dist/shell/assets/{xml-DIiuHFHw.js → xml-BYRl5HAh.js} +1 -1
  210. package/dist/shell/assets/xml-BYRl5HAh.js.br +0 -0
  211. package/dist/shell/assets/xml-BYRl5HAh.js.gz +0 -0
  212. package/dist/shell/assets/{yaml-CBnMyn4j.js → yaml-DeSD0TUR.js} +1 -1
  213. package/dist/shell/assets/yaml-DeSD0TUR.js.br +0 -0
  214. package/dist/shell/assets/yaml-DeSD0TUR.js.gz +0 -0
  215. package/dist/shell/index.html +11 -8
  216. package/dist/shell/manifest.json +13 -0
  217. package/dist/shell/sw.js +117 -22
  218. package/package.json +2 -2
  219. package/dist/shell/assets/index-BpW1Kufz.css +0 -1
  220. package/dist/shell/assets/index-CdY8WF4P.js +0 -270
package/dist/daemon.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // ../daemon/src/index.ts
3
3
  import { rmSync as rmSync9, writeFileSync as writeFileSync14 } from "fs";
4
4
  import { homedir as homedir15 } from "os";
5
- import { dirname as dirname14, join as join33 } from "path";
5
+ import { dirname as dirname14, join as join34 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // ../shared/src/protocol/events.ts
@@ -11,7 +11,24 @@ var CHECK_TOOL = "check";
11
11
  var TOOL_SEARCH = "ToolSearch";
12
12
  var ASK_TOOL = "AskUserQuestion";
13
13
  var TOYON_MCP_SERVER = "toyon";
14
- var HANDOFF_TOOL = "handoff";
14
+ var SPLIT_TOOL = "split";
15
+ function coalesce(entries) {
16
+ const out = [];
17
+ for (const entry of entries) {
18
+ const last = out.at(-1);
19
+ const { event } = entry;
20
+ if (event.type === "text-delta" && last?.event.type === "text-delta" && last.event.messageId === event.messageId) {
21
+ out[out.length - 1] = { seq: last.seq, event: { ...last.event, text: last.event.text + event.text } };
22
+ } else if (event.type === "thinking-delta" && last?.event.type === "thinking-delta") {
23
+ out[out.length - 1] = { seq: last.seq, event: { type: "thinking-delta", text: last.event.text + event.text } };
24
+ } else if (event.type === "tool-delta" && last?.event.type === "tool-delta" && last.event.toolId === event.toolId) {
25
+ out[out.length - 1] = { seq: last.seq, event: { ...event, text: last.event.text + event.text } };
26
+ } else {
27
+ out.push(entry);
28
+ }
29
+ }
30
+ return out;
31
+ }
15
32
 
16
33
  // ../shared/src/agent-tools.ts
17
34
  var EDIT_KINDS = new Set(["edit", "delete", "move", "execute"]);
@@ -39,12 +56,6 @@ function emptyInput(input) {
39
56
  var DEFAULT_TIMEOUT_MS = 10 * 60000;
40
57
  var DEFAULT_PERMISSION_MODE = "auto";
41
58
  var LAND_SUBJECTS_MAX = 8;
42
- function siblingsOf(wt, rows) {
43
- if (!wt.variant)
44
- return [];
45
- const group = wt.variant.group;
46
- return rows.filter((w) => w.id !== wt.id && w.variant?.group === group);
47
- }
48
59
  function hasOwnBranch(wt) {
49
60
  return wt.branch.startsWith("toyon/");
50
61
  }
@@ -100,45 +111,22 @@ function canGraft(wt) {
100
111
  // ../shared/src/railOrder.ts
101
112
  var sentAt = (w) => w.promptedAt ?? w.lastTurn?.at ?? w.createdAt;
102
113
  var tierOf = (w) => isLead(w) ? 0 : 1;
103
- var railUnitOf = (w) => w.variant ? `group:${w.variant.group}` : `row:${w.id}`;
104
114
  function railOrder(rows) {
105
- const units = [];
106
- const groups = new Map;
107
- for (const row of rows) {
108
- const w = row.worktree;
109
- const key = railUnitOf(w);
110
- const unit = groups.get(key);
111
- if (unit) {
112
- unit.rows.push(row);
113
- unit.at = Math.max(unit.at, sentAt(w));
114
- continue;
115
- }
116
- const fresh = { tier: tierOf(w), at: sentAt(w), rows: [row] };
117
- units.push(fresh);
118
- groups.set(key, fresh);
119
- }
120
- units.sort((a, b) => a.tier - b.tier || b.at - a.at);
121
- return units.flatMap((u) => u.rows.length > 1 ? u.rows.sort((a, b) => (a.worktree.variant?.index ?? 0) - (b.worktree.variant?.index ?? 0)) : u.rows);
115
+ return [...rows].sort((a, b) => tierOf(a.worktree) - tierOf(b.worktree) || sentAt(b.worktree) - sentAt(a.worktree));
122
116
  }
123
117
 
124
118
  // ../shared/src/archiveRule.ts
125
119
  var ARCHIVE_KEEP_RECENT = 5;
126
120
  function archiveReason(wt, f) {
127
- const group = [wt, ...siblingsOf(wt, f.rows)];
128
- const decided = group.some((w) => !!w.lands?.length);
129
- const lost = decided && !wt.lands?.length;
130
- if (decided) {
131
- if (!nothingToKeep(wt, f, lost))
132
- return null;
133
- } else if (!group.every((w) => nothingToKeep(w, f, false)))
121
+ if (!nothingToKeep(wt, f))
134
122
  return null;
135
- if (unitsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
123
+ if (rowsAhead(wt, f.rows) < ARCHIVE_KEEP_RECENT)
136
124
  return null;
137
- const what = lost ? "sibling landed" : wt.landed ? "landed" : "no changes";
125
+ const what = wt.landed ? "landed" : "no changes";
138
126
  return `${what}, ${UNOPENED}${duration(f.afterMs)}`;
139
127
  }
140
128
  var UNOPENED = "not opened in ";
141
- function nothingToKeep(w, f, lost) {
129
+ function nothingToKeep(w, f) {
142
130
  if (w.kind !== "worktree" || !hasOwnBranch(w))
143
131
  return false;
144
132
  if (w.lastTurn?.end !== "done" || isUnseen(w))
@@ -150,19 +138,12 @@ function nothingToKeep(w, f, lost) {
150
138
  if (f.now - (w.viewedAt ?? w.createdAt) < f.afterMs)
151
139
  return false;
152
140
  const c = f.counts(w.id);
153
- return c.dirty === 0 && (lost ? c.ahead !== undefined : c.ahead === 0);
141
+ return c.dirty === 0 && c.ahead === 0;
154
142
  }
155
- function unitsAhead(wt, rows) {
143
+ function rowsAhead(wt, rows) {
156
144
  const tasks = railOrder(rows.filter((w) => w.kind === "worktree").map((worktree) => ({ worktree })));
157
- const mine = railUnitOf(wt);
158
- const seen = new Set;
159
- for (const { worktree: w } of tasks) {
160
- const unit = railUnitOf(w);
161
- if (unit === mine)
162
- return seen.size;
163
- seen.add(unit);
164
- }
165
- return seen.size;
145
+ const at = tasks.findIndex(({ worktree: w }) => w.id === wt.id);
146
+ return at < 0 ? tasks.length : at;
166
147
  }
167
148
  function duration(ms) {
168
149
  if (ms >= 60 * 60000)
@@ -536,6 +517,9 @@ function prTaken(wt) {
536
517
  function shipNoun(op) {
537
518
  return { land: "a land", commit: "a commit", "sync-main": "a sync", "pull-main": "a pull" }[op];
538
519
  }
520
+ function prStanding(wt) {
521
+ return prTaken(wt) && !wt.landed ? undefined : wt.pr;
522
+ }
539
523
 
540
524
  // ../../node_modules/.bun/zod@4.5.4/node_modules/zod/v4/core/util.js
541
525
  function getEnumValues(entries) {
@@ -6602,7 +6586,7 @@ ${text.slice(text.length - (CONTEXT_CHARS - HEAD_CHARS))}`;
6602
6586
  }
6603
6587
 
6604
6588
  // ../shared/src/protocol/limits.ts
6605
- var PROTOCOL_VERSION = 119;
6589
+ var PROTOCOL_VERSION = 129;
6606
6590
  var PAGE_ERROR_MAX_CHARS = 2000;
6607
6591
  var PAGE_TITLE_MAX_CHARS = 1000;
6608
6592
  var FILE_MAX_CHARS = 5000000;
@@ -6612,7 +6596,6 @@ var IMAGE_MAX_BYTES = 5 * 1024 * 1024;
6612
6596
  var PASTE_MAX_CHARS = 1e5;
6613
6597
  var FILE_INLINE_CHARS = 20000;
6614
6598
  var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
6615
- var VARIANTS_MAX = 3;
6616
6599
 
6617
6600
  // ../shared/src/paste.ts
6618
6601
  var PREVIEW_CHARS = 80;
@@ -6677,6 +6660,19 @@ var elementTraitsSchema = object({
6677
6660
  attrs: array(tuple([string2().max(40), string2().max(200)])).max(12)
6678
6661
  });
6679
6662
 
6663
+ // ../shared/src/protocol/stamps.ts
6664
+ var STAMP_PARTS = ["rows", "repos", "visits", "themes"];
6665
+ var PART = /^[0-9a-f]*$/;
6666
+ function parseKnown(q) {
6667
+ if (!q)
6668
+ return null;
6669
+ const parts = q.split(".");
6670
+ if (parts.length !== STAMP_PARTS.length || !parts.every((p) => p.length <= 32 && PART.test(p)))
6671
+ return null;
6672
+ return { rows: parts[0], repos: parts[1], visits: parts[2], themes: parts[3] };
6673
+ }
6674
+ var sameStamp = (held, now) => held !== "" && held === now;
6675
+
6680
6676
  // ../shared/src/protocol/ws.ts
6681
6677
  var id = string2().min(1).max(200);
6682
6678
  var permissionModeSchema = _enum(["auto", "ask", "plan"]);
@@ -6848,7 +6844,7 @@ var themePrefsSchema = object({
6848
6844
  dark: string2()
6849
6845
  });
6850
6846
  var clientMsgSchema = discriminatedUnion("t", [
6851
- object({ t: literal("subscribe"), worktreeId: id }),
6847
+ object({ t: literal("subscribe"), worktreeId: id, since: number2().int().min(0).optional() }),
6852
6848
  object({ t: literal("unsubscribe"), worktreeId: id }),
6853
6849
  object({ t: literal("view"), worktreeId: id.nullable() }),
6854
6850
  object({
@@ -6866,7 +6862,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6866
6862
  repoId: id,
6867
6863
  worktreeId: id.optional(),
6868
6864
  prompt,
6869
- variants: number2().int().min(1).max(VARIANTS_MAX).optional(),
6870
6865
  context: ambient.optional(),
6871
6866
  attachments,
6872
6867
  agent: id.optional(),
@@ -6877,15 +6872,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6877
6872
  carry: boolean2().optional(),
6878
6873
  boxId
6879
6874
  }),
6880
- object({
6881
- t: literal("batch-worktrees"),
6882
- repoId: id,
6883
- prompt,
6884
- agent: id.optional(),
6885
- model: string2().max(200).optional(),
6886
- effort: string2().max(100).optional(),
6887
- boxId
6888
- }),
6889
6875
  object({ t: literal("set-worktree-mode"), worktreeId: id, mode: permissionModeSchema }),
6890
6876
  object({ t: literal("set-worktree-model"), worktreeId: id, model: string2().max(200) }),
6891
6877
  object({ t: literal("set-worktree-effort"), worktreeId: id, effort: string2().max(100) }),
@@ -6978,7 +6964,6 @@ var clientMsgSchema = discriminatedUnion("t", [
6978
6964
  object({ t: literal("forget-visit"), repoId: id, path: routePath }),
6979
6965
  object({ t: literal("page-error"), worktreeId: id, message: string2().min(1).max(PAGE_ERROR_MAX_CHARS) }),
6980
6966
  object({ t: literal("page-loaded"), worktreeId: id }),
6981
- object({ t: literal("pick-variant"), worktreeId: id }),
6982
6967
  object({
6983
6968
  t: literal("unqueue"),
6984
6969
  worktreeId: id,
@@ -7047,14 +7032,21 @@ var clientMsgSchema = discriminatedUnion("t", [
7047
7032
  choiceId: string2().max(200),
7048
7033
  note: string2().max(1e4).optional()
7049
7034
  }),
7035
+ object({ t: literal("pieces-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
7036
+ object({
7037
+ t: literal("pieces-fate"),
7038
+ worktreeId: id,
7039
+ id: string2().max(64),
7040
+ pieceId: string2().max(16),
7041
+ fate: _enum(["here", "worktree", "send", "skip"])
7042
+ }),
7050
7043
  object({
7051
- t: literal("handoff-answer"),
7044
+ t: literal("pieces-answer"),
7052
7045
  worktreeId: id,
7053
7046
  id: string2().max(64),
7054
7047
  go: boolean2(),
7055
7048
  note: string2().max(5000).optional()
7056
7049
  }),
7057
- object({ t: literal("handoff-ask"), worktreeId: id, repoId: id, text: prompt.optional() }),
7058
7050
  object({ t: literal("agent-logout"), agent: id }),
7059
7051
  object({ t: literal("agent-check"), agent: id }),
7060
7052
  object({ t: literal("agent-config"), agent: id, repoId: id.optional() }),
@@ -8526,7 +8518,7 @@ async function turnRemoteOff(ts, daemonPort, file, was) {
8526
8518
  // ../daemon/package.json
8527
8519
  var package_default = {
8528
8520
  name: "@toyon/daemon",
8529
- version: "0.18.0",
8521
+ version: "0.19.0",
8530
8522
  private: true,
8531
8523
  type: "module",
8532
8524
  main: "src/index.ts",
@@ -13354,7 +13346,7 @@ class ToolSet {
13354
13346
  }
13355
13347
  var PROTOCOL_VERSIONS = ["2025-11-25", "2025-06-18", "2025-03-26"];
13356
13348
  var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
13357
- var BODY_MAX_BYTES = 64 * 1024;
13349
+ var BODY_MAX_BYTES = 256 * 1024;
13358
13350
  var PARSE_ERROR = -32700;
13359
13351
  var INVALID_PARAMS = -32602;
13360
13352
  var METHOD_NOT_FOUND = -32601;
@@ -13613,10 +13605,14 @@ class AgentReadiness {
13613
13605
  const e = this.known.get(info.id);
13614
13606
  const models = this.d.options(info.id, "model");
13615
13607
  const efforts = this.d.options(info.id, "thought_level");
13608
+ const defaultModel = this.d.runs(info.id, "model");
13609
+ const defaultEffort = this.d.runs(info.id, "thought_level");
13616
13610
  return {
13617
13611
  ...info,
13618
13612
  ...models.length > 0 ? { models } : {},
13619
13613
  ...efforts.length > 0 ? { efforts } : {},
13614
+ ...defaultModel !== undefined ? { defaultModel } : {},
13615
+ ...defaultEffort !== undefined ? { defaultEffort } : {},
13620
13616
  ...this.d.quick(info.id) ? { quick: true } : {},
13621
13617
  ...e?.status ? { auth: e.status } : {},
13622
13618
  ...e?.canLogout ? { canLogout: true } : {},
@@ -13685,8 +13681,9 @@ class AgentReadiness {
13685
13681
  });
13686
13682
  found.canLogout = supportsLogout(init.agentCapabilities);
13687
13683
  const s = await link.conn.agent.request(methods.agent.session.new, { cwd: this.d.cwd, mcpServers: [] });
13688
- for (const [category, opt] of readOptions(s.configOptions))
13689
- this.d.learned(agentId, category, opt.choices);
13684
+ for (const [category, opt] of readOptions(s.configOptions)) {
13685
+ this.d.learned(agentId, category, opt.choices, opt.current);
13686
+ }
13690
13687
  if (init.agentCapabilities?.sessionCapabilities?.close) {
13691
13688
  await link.conn.agent.request(methods.agent.session.close, { sessionId: s.sessionId }).catch((e) => {
13692
13689
  log.debug(tag, `session/close failed: ${e instanceof Error ? e.message : String(e)}`);
@@ -14591,6 +14588,12 @@ function sameAsLast(cwd, rel, markdown) {
14591
14588
  }
14592
14589
  }
14593
14590
  async function writePlanDoc(cwd, markdown) {
14591
+ const rel = writePlanDocNow(cwd, markdown);
14592
+ if (rel)
14593
+ await excludePlans(cwd);
14594
+ return rel;
14595
+ }
14596
+ function writePlanDocNow(cwd, markdown) {
14594
14597
  const dir = join5(cwd, PLANS_DIR);
14595
14598
  const last = lastNumber(dir);
14596
14599
  if (last > 0) {
@@ -14608,9 +14611,18 @@ async function writePlanDoc(cwd, markdown) {
14608
14611
  log.warn(cwd, `could not write ${rel}; the plan stays on the card`, e);
14609
14612
  return null;
14610
14613
  }
14611
- await excludePlans(cwd);
14614
+ excludePlans(cwd).catch((e) => log.warn(cwd, "could not exclude the plans folder", e));
14612
14615
  return rel;
14613
14616
  }
14617
+ function listDoc(pieces) {
14618
+ return `# Worktrees
14619
+
14620
+ ${pieces.map((p) => `## ${p.title}
14621
+
14622
+ ${p.plan.trim()}
14623
+ `).join(`
14624
+ `)}`;
14625
+ }
14614
14626
  function planEdited(cwd, rel, proposed) {
14615
14627
  const file = join5(cwd, rel);
14616
14628
  if (!existsSync7(file))
@@ -14728,9 +14740,8 @@ var BUILTIN_AGENTS = [
14728
14740
  name: "Claude Code",
14729
14741
  short: "Claude",
14730
14742
  builtin: true,
14731
- run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.84.0", bin: "claude-agent-acp" },
14743
+ run: { kind: "npm-bin", pkg: "@agentclientprotocol/claude-agent-acp", version: "0.95.0", bin: "claude-agent-acp" },
14732
14744
  env: {
14733
- CLAUDE_CODE_ENABLE_FUNCTION_HOOKS: "1",
14734
14745
  ...cloud.enabled ? { NO_BROWSER: "1" } : {}
14735
14746
  },
14736
14747
  terminalLogins: {
@@ -15600,11 +15611,8 @@ function toyonTool(update) {
15600
15611
  }
15601
15612
  return null;
15602
15613
  }
15603
- function toyonTitle(tool, rawInput, title) {
15604
- if (tool !== HANDOFF_TOOL)
15605
- return title && toyonTool({ name: title }) === null ? title : tool;
15606
- const project = asRecord2(rawInput).project;
15607
- return typeof project === "string" && project.trim() ? `Continue in ${project.trim()}` : "Continue in another project";
15614
+ function toyonTitle(tool, title) {
15615
+ return title && toyonTool({ name: title }) === null ? title : tool;
15608
15616
  }
15609
15617
  function heading(update) {
15610
15618
  const name = typeof update.name === "string" ? update.name : "";
@@ -15616,7 +15624,7 @@ function heading(update) {
15616
15624
  }
15617
15625
  const own = toyonTool(update);
15618
15626
  if (own)
15619
- return { name: own, title: toyonTitle(own, update.rawInput, update.title), kind: "other" };
15627
+ return { name: own, title: toyonTitle(own, update.title), kind: "other" };
15620
15628
  return { name, title: update.title, ...update.kind ? { kind: update.kind } : {} };
15621
15629
  }
15622
15630
  function endOfAsk(req, res, memos) {
@@ -15641,18 +15649,21 @@ function release(toolId, memo) {
15641
15649
  delete memo.held;
15642
15650
  if (!emptyInput(memo.input))
15643
15651
  memo.writing = false;
15652
+ memo.released = true;
15644
15653
  return memo.ended ? null : { type: "tool-update", toolId, ...held };
15645
15654
  }
15646
15655
  function preempted(memos) {
15647
15656
  for (const memo of memos.values()) {
15648
- if (!memo.ended && memo.writing && !memo.parent)
15657
+ if (!memo.ended && !memo.parent)
15649
15658
  memo.cut = true;
15650
15659
  }
15651
15660
  }
15652
15661
  function abandoned(memos, parent, opening = false) {
15653
15662
  const out = [];
15654
15663
  for (const [toolId, memo] of memos) {
15655
- if (memo.ended || !memo.writing || memo.parent !== parent)
15664
+ if (memo.ended || memo.parent !== parent)
15665
+ continue;
15666
+ if (!(memo.writing || memo.cut || memo.released && !opening))
15656
15667
  continue;
15657
15668
  if (opening && memo.held && !memo.cut) {
15658
15669
  const sent = release(toolId, memo);
@@ -15661,7 +15672,7 @@ function abandoned(memos, parent, opening = false) {
15661
15672
  continue;
15662
15673
  }
15663
15674
  memo.ended = true;
15664
- out.push({ type: "tool-end", toolId });
15675
+ out.push({ type: "tool-end", toolId, cut: true });
15665
15676
  }
15666
15677
  return out;
15667
15678
  }
@@ -15744,7 +15755,7 @@ function mapUpdate(update, memos, tag, sink, background) {
15744
15755
  });
15745
15756
  }
15746
15757
  const refined = { ...memo.held };
15747
- const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) : memo.name === HANDOFF_TOOL ? update.rawInput !== undefined ? toyonTitle(HANDOFF_TOOL, update.rawInput, update.title) : undefined : update.title;
15758
+ const title = memo.name === TOOL_SEARCH ? toolSearchTitle(update.rawInput) : update.title;
15748
15759
  if (title && title !== memo.title)
15749
15760
  refined.title = memo.title = title;
15750
15761
  if (update.name && update.name !== memo.name && toyonTool(update) !== memo.name)
@@ -16000,7 +16011,9 @@ function pickOption(options, verdict) {
16000
16011
  var SYSTEM_APPEND = [
16001
16012
  "You are working inside a dedicated git worktree managed by Toyon.",
16002
16013
  "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.",
16003
- "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 `handoff` tool when you have one, naming the project and writing the message its agent will start 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.",
16014
+ "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.",
16004
16017
  "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.",
16005
16018
  "Never run `git push`, delete branches, or create pull requests, and never offer to; shipping is handled by the Toyon UI.",
16006
16019
  "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.",
@@ -16009,6 +16022,7 @@ var SYSTEM_APPEND = [
16009
16022
  "An answer to a question of yours can carry typed text beside the option picked, and the text outranks the pick. Text that doubts the pick, asks you something or says the user cannot decide is not a yes: answer it in prose and change nothing until they have decided. Text that only adds detail to the pick is an instruction for carrying it out.",
16010
16023
  "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.",
16011
16024
  "`@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
+ "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.",
16012
16026
  '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.',
16013
16027
  "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.",
16014
16028
  "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.",
@@ -16146,30 +16160,85 @@ function procDiagnosis(p) {
16146
16160
  return p.exitCode != null ? `It exited with code ${p.exitCode}.` : "It crashed.";
16147
16161
  return "It never answered on that port.";
16148
16162
  }
16149
- function handoffPrompt(message, note, origin) {
16163
+ function piecesActedPrompt(pieces, outcomes) {
16164
+ const titled = (id) => `"${pieces.find((p) => p.id === id)?.title ?? id}"`;
16165
+ const list = (xs) => xs.join(", ");
16166
+ const started = outcomes.filter((o) => o.fate === "worktree" && !o.after && !o.error).map((o) => titled(o.pieceId));
16167
+ const queued = outcomes.filter((o) => o.fate === "worktree" && o.after && !o.error);
16168
+ const sent = outcomes.filter((o) => o.fate === "send" && !o.error);
16169
+ const here = outcomes.filter((o) => o.fate === "here").map((o) => titled(o.pieceId));
16170
+ const skipped = outcomes.filter((o) => o.fate === "skip").map((o) => titled(o.pieceId));
16171
+ const failed = outcomes.filter((o) => o.error).map((o) => `${titled(o.pieceId)} (${o.error})`);
16172
+ const parts = [];
16173
+ if (started.length)
16174
+ parts.push(`${list(started)} started in ${started.length === 1 ? "a sibling worktree" : "sibling worktrees"} of this project`);
16175
+ if (queued.length)
16176
+ parts.push(queued.map((o) => `${titled(o.pieceId)} waits for ${o.after} to land and then starts in its own worktree`).join("; "));
16177
+ if (sent.length)
16178
+ parts.push(sent.map((o) => `${titled(o.pieceId)} went to the open worktree "${o.title ?? "named"}"`).join("; "));
16179
+ if (skipped.length)
16180
+ parts.push(`${list(skipped)} ${skipped.length === 1 ? "was" : "were"} skipped by the person`);
16181
+ if (failed.length)
16182
+ parts.push(`${list(failed)} could not start`);
16183
+ const own = here.length ? `Do ${list(here)} here, and only that.` : "Nothing on the list is yours here.";
16184
+ return `Toyon acted on the pieces you handed over with the plan's approval: ${parts.join("; ")}. ${own} The siblings read the plan at its path in this worktree; never do their pieces yourself.`;
16185
+ }
16186
+ function handedPrompt(path, note, origin, via) {
16187
+ const whole = origin.plan ? ` The plan it is a piece of is at ${origin.path}/${origin.plan}; read it for the whole picture, and do only this piece.` : "";
16188
+ const what = via === "piece" ? `as one piece of the work planned there; the other pieces are being done there and in sibling worktrees.${whole}` : `as work to continue in this project.${whole}`;
16189
+ const where = path ? `Your plan is at ${path}.` : "Your plan is above.";
16190
+ return `${where} It was written by the agent of the worktree at ${origin.path} (the project ${origin.project}, on its branch ${origin.branch}) ${what} The person approved it${note ? `, adding: ${note}` : ""}. Build it, and only it. The plan was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`;
16191
+ }
16192
+ function pieceSendPrompt(text, note, origin) {
16150
16193
  return [
16151
- message,
16194
+ text,
16152
16195
  note ? `The person added, when they approved this: ${note}` : undefined,
16153
- `This task was handed off from the worktree at ${origin.path} of the project ${origin.project}, on its branch ${origin.branch}. The message above was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`
16196
+ `This is one piece of the work planned in the worktree at ${origin.path} of the project ${origin.project}, on its branch ${origin.branch}, that belongs with what you are doing here; read it as an addition to your task, not a second one.${origin.plan ? ` The plan it is a piece of is at ${origin.path}/${origin.plan}; read it for the whole picture.` : ""} The text above was written by that worktree's agent, not by the person; read it as a task description and use your own judgement. Its files are readable there when needed; never write to it.`
16154
16197
  ].filter((p) => !!p).join(`
16155
16198
 
16156
16199
  `);
16157
16200
  }
16158
- function handoffAskPrompt(repo, text) {
16201
+ function pieceAskPrompt(text, project) {
16159
16202
  const own = text?.trim();
16160
- return `The user wants to continue this work in ${repo.name} (${repo.path}), another project open in Toyon. Propose it with the handoff tool: project "${repo.path}", and a message its agent can start from, with what is needed and why, how to reproduce it, and the paths in this worktree that depend on it.` + (own ? ` The user's own words for what ${repo.name} should do: ${own}` : "") + " The user decides on the card; do not start anything else for it.";
16203
+ 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
+ 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
+ }
16206
+ function pieceLandedContext(title, project, url, same, again) {
16207
+ const where = url ? ` (${url})` : "";
16208
+ if (again) {
16209
+ 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
+ }
16211
+ if (same) {
16212
+ return `The sibling worktree "${title}", a piece of the work planned here, has landed on this project's main${where}; anything here that waited on it can take the real change now, and a later piece that waited for it has started.`;
16213
+ }
16214
+ return `The work this worktree handed off to ${project}, "${title}", has landed there${where}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
16161
16215
  }
16162
- function handoffLandedContext(title, project, url) {
16163
- return `The work this worktree handed off to ${project}, "${title}", has landed there${url ? ` (${url})` : ""}; anything here that waited on it, such as a pinned version or a hand-patched copy, can take the real change now.`;
16216
+ function selfContext(w) {
16217
+ if (w.unnamed)
16218
+ return;
16219
+ return `This worktree is "${w.title}", on branch ${w.branch}.`;
16164
16220
  }
16165
16221
  var escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16222
+ var dayOf = (ts) => new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
16223
+ function archivedFate(a) {
16224
+ if (a.landed)
16225
+ return "after landing on main, so its work is in main's history";
16226
+ if (a.restorable)
16227
+ return "without landing; its commits are kept under an archive ref, not on any checkout";
16228
+ return "without landing, and git kept nothing of it";
16229
+ }
16166
16230
  function worktreeContext(text, siblings) {
16167
16231
  const out = [];
16232
+ const said = new Set;
16168
16233
  for (const s of siblings) {
16234
+ if (said.has(s.branch))
16235
+ continue;
16169
16236
  const named = new RegExp(`(^|\\s)@${escapeRegExp(s.branch)}(?=$|[\\s.,;:!?)\\]}'"])`);
16170
16237
  if (!named.test(text))
16171
16238
  continue;
16172
- out.push(`@${s.branch} is the worktree "${s.title}" in this project. 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.`);
16239
+ 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}`);
16173
16242
  }
16174
16243
  return out;
16175
16244
  }
@@ -16208,6 +16277,52 @@ function previewContext(p) {
16208
16277
  }
16209
16278
  return line;
16210
16279
  }
16280
+ var STANDING_SUBJECTS = 3;
16281
+ var plural = (n, one) => `${n} ${one}${n === 1 ? "" : "s"}`;
16282
+ function standingContext(s) {
16283
+ if (!s)
16284
+ return;
16285
+ const tree = s.dirty === undefined ? undefined : s.dirty === 0 ? "The tree is clean." : `The tree has ${plural(s.dirty, "uncommitted file")}${s.main ? "" : ", which the user commits from the commit box"}.`;
16286
+ if (s.main) {
16287
+ const behind = s.behind ? ` It is ${plural(s.behind, "commit")} behind origin.` : "";
16288
+ return `This is the project's main checkout.${tree ? ` ${tree}` : ""}${behind}`;
16289
+ }
16290
+ const parts = [];
16291
+ if (s.landed) {
16292
+ const named = s.landed.subjects.slice(0, STANDING_SUBJECTS).map((t) => `"${t}"`);
16293
+ const more = s.landed.subjects.length - named.length;
16294
+ const as = named.length ? ` as ${named.join("; ")}${more > 0 ? ` (and ${more} more)` : ""}` : "";
16295
+ const through = s.landed.pr === undefined ? "" : `, through PR #${s.landed.pr}`;
16296
+ parts.push(`This worktree's work has landed on ${s.base}${as}${through}. The branch restarted from ${s.base}, so there is nothing here to commit or land until new work is written.`);
16297
+ } else {
16298
+ if (tree)
16299
+ parts.push(tree);
16300
+ if (s.ahead !== undefined) {
16301
+ const behind = s.behind ? ` and ${s.behind} behind it` : "";
16302
+ parts.push(s.ahead === 0 && !s.behind ? `The branch is level with ${s.base}.` : `The branch is ${plural(s.ahead, "commit")} ahead of ${s.base}${behind}.`);
16303
+ }
16304
+ }
16305
+ const pr = s.pr;
16306
+ if (pr && s.landed?.pr !== pr.number) {
16307
+ switch (pr.state) {
16308
+ case "open": {
16309
+ const checks = pr.checks ? `, checks ${{ pass: "passing", pending: "pending", fail: "failing" }[pr.checks]}` : "";
16310
+ const review = pr.review ? `, review ${{ approved: "approved", changes_requested: "changes requested", review_required: "required" }[pr.review]}` : "";
16311
+ const auto = pr.automerge ? ", auto-merge on" : "";
16312
+ const unpushed = s.unpushed ? `; ${plural(s.unpushed, "commit")} here ${s.unpushed === 1 ? "is" : "are"} not on it yet` : "";
16313
+ parts.push(`Pull request #${pr.number} is open at ${pr.url}${checks}${review}${auto}${unpushed}.`);
16314
+ break;
16315
+ }
16316
+ case "merged":
16317
+ parts.push(`PR #${pr.number} has merged on GitHub; Toyon lands it here on its next poll.`);
16318
+ break;
16319
+ case "closed":
16320
+ parts.push(`PR #${pr.number} was closed without merging; the next land press creates a new one.`);
16321
+ break;
16322
+ }
16323
+ }
16324
+ return parts.length ? parts.join(" ") : undefined;
16325
+ }
16211
16326
  function imageCaption(ref, path) {
16212
16327
  return `${attachmentLabel("image", ref.n)}: ${ref.name} (${ref.width}\xD7${ref.height}), saved at ${path}`;
16213
16328
  }
@@ -16286,28 +16401,7 @@ import { join as join11 } from "path";
16286
16401
  function transcriptPathFor(transcriptsDir, worktreeId) {
16287
16402
  return join11(transcriptsDir, `${worktreeId}.jsonl`);
16288
16403
  }
16289
- function coalesce(entries) {
16290
- const out = [];
16291
- for (const entry of entries) {
16292
- const last = out.at(-1);
16293
- const { event } = entry;
16294
- if (event.type === "text-delta" && last?.event.type === "text-delta") {
16295
- const messageId = event.messageId ?? last.event.messageId;
16296
- out[out.length - 1] = {
16297
- seq: last.seq,
16298
- event: { type: "text-delta", text: last.event.text + event.text, ...messageId ? { messageId } : {} }
16299
- };
16300
- } else if (event.type === "thinking-delta" && last?.event.type === "thinking-delta") {
16301
- out[out.length - 1] = { seq: last.seq, event: { type: "thinking-delta", text: last.event.text + event.text } };
16302
- } else if (event.type === "tool-delta" && last?.event.type === "tool-delta" && last.event.toolId === event.toolId) {
16303
- out[out.length - 1] = { seq: last.seq, event: { ...event, text: last.event.text + event.text } };
16304
- } else {
16305
- out.push(entry);
16306
- }
16307
- }
16308
- return out;
16309
- }
16310
- var NOT_THE_TURNS = new Set(["usage", "session-info", "user-message", "fix-asked"]);
16404
+ var NOT_THE_TURNS = new Set(["usage", "session-info", "user-message", "fix-asked", "handed"]);
16311
16405
  function sealOpenTurn(entries) {
16312
16406
  let reason = null;
16313
16407
  let ts = 0;
@@ -16342,6 +16436,19 @@ function parseTranscript(text, tag) {
16342
16436
  log.warn(tag, `transcript: skipped ${torn} unparsable line(s)`);
16343
16437
  return out;
16344
16438
  }
16439
+ var lastSeq = (entries) => entries.at(-1)?.seq ?? -1;
16440
+ function after(entries, since) {
16441
+ let lo = 0;
16442
+ let hi = entries.length;
16443
+ while (lo < hi) {
16444
+ const mid = lo + hi >>> 1;
16445
+ if (entries[mid].seq <= since)
16446
+ lo = mid + 1;
16447
+ else
16448
+ hi = mid;
16449
+ }
16450
+ return entries.slice(lo);
16451
+ }
16345
16452
 
16346
16453
  class Transcript {
16347
16454
  path;
@@ -16656,7 +16763,10 @@ class AcpSession {
16656
16763
  tasks;
16657
16764
  asking = 0;
16658
16765
  asks = new Map;
16659
- openHandoffs = new Set;
16766
+ openCards = new Set;
16767
+ heldPieces = null;
16768
+ adopted = null;
16769
+ openPlanAsk = null;
16660
16770
  log;
16661
16771
  seq;
16662
16772
  commandList = [];
@@ -16670,7 +16780,7 @@ class AcpSession {
16670
16780
  ...d.taskPollMs !== undefined ? { pollMs: d.taskPollMs } : {},
16671
16781
  ...d.claudeConfigDir !== undefined ? { configDir: d.claudeConfigDir } : {}
16672
16782
  });
16673
- this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" ? event.attachments : undefined));
16783
+ this.seq = nextNumbers(this.log.entries.map(({ event }) => event.type === "user-message" || event.type === "handed" ? event.attachments : undefined));
16674
16784
  const seal = sealOpenTurn(this.log.entries);
16675
16785
  const open = new Set;
16676
16786
  for (const { event } of this.log.entries) {
@@ -16697,18 +16807,18 @@ class AcpSession {
16697
16807
  }
16698
16808
  const going = new Set;
16699
16809
  for (const { event } of this.log.entries) {
16700
- this.trackHandoff(event);
16701
- if (event.type === "handoff-go")
16810
+ this.trackCards(event);
16811
+ if (event.type === "pieces-go")
16702
16812
  going.add(event.id);
16703
- else if (event.type === "handoff" || event.type === "handoff-declined")
16813
+ else if (event.type === "pieces" || event.type === "pieces-declined")
16704
16814
  going.delete(event.id);
16705
16815
  }
16706
16816
  for (const id of going) {
16707
16817
  this.emit({
16708
- type: "handoff-declined",
16818
+ type: "pieces-declined",
16709
16819
  id,
16710
16820
  outcome: "failed",
16711
- reason: "Toyon stopped while the worktree was being made",
16821
+ reason: "Toyon stopped while the worktrees were being made",
16712
16822
  ts: Date.now()
16713
16823
  });
16714
16824
  }
@@ -16716,16 +16826,77 @@ class AcpSession {
16716
16826
  log.info(d.worktreeId, `sealed a turn left open: ${seal.stopReason}`);
16717
16827
  this.emit(seal);
16718
16828
  }
16719
- if (this.openHandoffs.size > 0)
16829
+ this.showHeld();
16830
+ if (this.openCards.size > 0)
16720
16831
  this.syncStatus();
16721
- }
16722
- trackHandoff(event) {
16723
- if (event.type === "handoff-proposed")
16724
- return this.openHandoffs.add(event.id).size > 0;
16725
- if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
16726
- return this.openHandoffs.delete(event.id);
16832
+ this.restored = true;
16833
+ }
16834
+ restored = false;
16835
+ trackCards(event) {
16836
+ if (event.type === "pieces-proposed") {
16837
+ if (event.held) {
16838
+ this.heldPieces = event.id;
16839
+ if (this.openPlanAsk)
16840
+ this.adopted = { ask: this.openPlanAsk, pieces: event.id };
16841
+ return false;
16842
+ }
16843
+ return this.openCards.add(event.id).size > 0;
16844
+ }
16845
+ if (event.type === "pieces-shown") {
16846
+ if (this.heldPieces === event.id)
16847
+ this.heldPieces = null;
16848
+ return this.openCards.add(event.id).size > 0;
16849
+ }
16850
+ if (event.type === "agent-permission") {
16851
+ if (event.plan)
16852
+ this.openPlanAsk = event.id;
16853
+ if (event.pieces && this.heldPieces === event.pieces) {
16854
+ this.heldPieces = null;
16855
+ this.adopted = { ask: event.id, pieces: event.pieces };
16856
+ }
16857
+ return false;
16858
+ }
16859
+ if (event.type === "agent-ask-end") {
16860
+ if (this.openPlanAsk === event.id)
16861
+ this.openPlanAsk = null;
16862
+ if (this.adopted?.ask === event.id) {
16863
+ const pieces = this.adopted.pieces;
16864
+ this.adopted = null;
16865
+ 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") {
16868
+ this.note({
16869
+ type: "pieces-declined",
16870
+ id: pieces,
16871
+ reason: "the plan card closed before it was answered",
16872
+ ts: Date.now()
16873
+ });
16874
+ } else
16875
+ this.heldPieces = pieces;
16876
+ }
16877
+ }
16878
+ return false;
16879
+ }
16880
+ if (event.type === "pieces-go" || event.type === "pieces" || event.type === "pieces-declined") {
16881
+ if (this.heldPieces === event.id)
16882
+ this.heldPieces = null;
16883
+ if (this.adopted?.pieces === event.id)
16884
+ this.adopted = null;
16885
+ return this.openCards.delete(event.id);
16886
+ }
16727
16887
  return false;
16728
16888
  }
16889
+ showHeld() {
16890
+ const id = this.heldPieces;
16891
+ if (!id || this.mode() === "plan")
16892
+ return;
16893
+ this.heldPieces = null;
16894
+ 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);
16898
+ this.emit({ type: "pieces-shown", id, ...typeof plan === "string" ? { plan } : {}, ts: Date.now() });
16899
+ }
16729
16900
  get commands() {
16730
16901
  return this.commandList;
16731
16902
  }
@@ -16791,7 +16962,7 @@ class AcpSession {
16791
16962
  return this.waiting().map(queued);
16792
16963
  }
16793
16964
  waiting() {
16794
- return this.queue.filter((q) => !q.recorded);
16965
+ return this.queue.filter((q) => !q.recorded && !q.handed);
16795
16966
  }
16796
16967
  onQueueChange = null;
16797
16968
  queueChanged() {
@@ -16815,12 +16986,12 @@ class AcpSession {
16815
16986
  }
16816
16987
  note(event) {
16817
16988
  this.emit(event);
16818
- if (this.trackHandoff(event))
16819
- this.syncStatus();
16820
16989
  }
16821
16990
  emit(event) {
16822
16991
  const entry = this.log.append(event);
16823
16992
  this.d.onEvent(event, entry.seq);
16993
+ if (this.restored && this.trackCards(event))
16994
+ this.syncStatus();
16824
16995
  }
16825
16996
  setStatus(s) {
16826
16997
  this.status = s;
@@ -16842,9 +17013,10 @@ class AcpSession {
16842
17013
  send(text, opts = {}) {
16843
17014
  if (this.stopped)
16844
17015
  return log.warn(this.d.worktreeId, "send after close dropped");
16845
- if (!opts.asked)
17016
+ if (!opts.asked && !opts.keepCards)
16846
17017
  for (const ask of [...this.asks.values()])
16847
17018
  ask.settle("skipped");
17019
+ this.showHeld();
16848
17020
  this.post(text, opts);
16849
17021
  }
16850
17022
  post(text, opts = {}) {
@@ -16853,12 +17025,13 @@ class AcpSession {
16853
17025
  fireAndForget(this.d.worktreeId, steer, "agent steer");
16854
17026
  }
16855
17027
  deliver(text, opts = {}) {
16856
- const { context, attachments, asked } = opts;
17028
+ const { context, attachments, asked, handed } = opts;
16857
17029
  const item = {
16858
17030
  text,
16859
17031
  context,
16860
17032
  ...attachments?.length ? { attachments } : {},
16861
- ...asked ? { asked: asked.kind } : {}
17033
+ ...asked ? { asked: asked.kind } : {},
17034
+ ...handed ? { handed } : {}
16862
17035
  };
16863
17036
  if (asked) {
16864
17037
  this.emit({ type: "fix-asked", text, ts: Date.now(), ...asked });
@@ -17002,7 +17175,7 @@ class AcpSession {
17002
17175
  q.handedBack = false;
17003
17176
  this.setStopping(false);
17004
17177
  }
17005
- this.setStatus(this.openHandoffs.size > 0 ? "waiting" : "idle");
17178
+ this.setStatus(this.openCards.size > 0 ? "waiting" : "idle");
17006
17179
  } catch (e) {
17007
17180
  const aws = this.conn && AWS_EXPIRED_RE.test(this.describe(e)) ? this.awsAuth() : null;
17008
17181
  if (this.conn && (aws || isAuthRequired(e) || this.rejectedCredential(e))) {
@@ -17046,6 +17219,7 @@ class AcpSession {
17046
17219
  return;
17047
17220
  turn.open = false;
17048
17221
  this.emit({ type: "turn-end", stopReason, ts: Date.now() });
17222
+ this.showHeld();
17049
17223
  this.setStopping(false);
17050
17224
  }
17051
17225
  async ask(system, prompt, opts = {}) {
@@ -17157,7 +17331,7 @@ class AcpSession {
17157
17331
  const message = (e instanceof Error ? e.message : String(e)).replace(/^Internal error: /, "");
17158
17332
  return /connection closed/i.test(message) ? this.conn?.link.exitInfo() ?? message : message;
17159
17333
  }
17160
- async record({ text, attachments }) {
17334
+ async record({ text, attachments, handed }) {
17161
17335
  const stored = [];
17162
17336
  try {
17163
17337
  for (const a of attachments ?? [])
@@ -17165,12 +17339,11 @@ class AcpSession {
17165
17339
  } finally {
17166
17340
  this.d.attachments.uploads.release(uploadIds(attachments));
17167
17341
  }
17168
- this.emit({
17169
- type: "user-message",
17170
- text,
17171
- ts: Date.now(),
17172
- ...stored.length ? { attachments: stored.map((s) => s.ref) } : {}
17173
- });
17342
+ const attached = stored.length ? { attachments: stored.map((s) => s.ref) } : {};
17343
+ if (handed)
17344
+ this.emit({ type: "handed", ...handed, text, ...attached, ts: Date.now() });
17345
+ else
17346
+ this.emit({ type: "user-message", text, ts: Date.now(), ...attached });
17174
17347
  return { attachments: stored };
17175
17348
  }
17176
17349
  carried(live, stored) {
@@ -17202,17 +17375,20 @@ class AcpSession {
17202
17375
  return;
17203
17376
  }
17204
17377
  const carried = this.carried(live, item.recorded.attachments);
17378
+ const standing = await this.d.standing?.();
17205
17379
  const prefix = live.prefixPending ? SYSTEM_APPEND : undefined;
17206
17380
  live.prefixPending = false;
17207
17381
  const res = await live.conn.ctx.request(methods.agent.session.prompt, {
17208
17382
  sessionId: live.sessionId,
17209
- prompt: buildPrompt(item.text, this.contextFor(item), prefix, carried)
17383
+ prompt: buildPrompt(item.text, this.contextFor(item, standing), prefix, carried)
17210
17384
  });
17211
17385
  this.endTurn(mapStopReason(res.stopReason));
17212
17386
  }
17213
- contextFor(item) {
17387
+ contextFor(item, standing) {
17214
17388
  return ambientBlock([
17215
17389
  ...item.context ?? [],
17390
+ this.d.self?.(),
17391
+ standing,
17216
17392
  ...this.d.mentioned?.(item.text) ?? [],
17217
17393
  this.d.preview?.(),
17218
17394
  this.d.pageErrors?.(),
@@ -17221,7 +17397,7 @@ class AcpSession {
17221
17397
  }
17222
17398
  landedSince() {
17223
17399
  const entries = this.log.entries;
17224
- const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked";
17400
+ const isMessage = (e) => e.type === "user-message" || e.type === "fix-asked" || e.type === "handed";
17225
17401
  let i = entries.length - 1;
17226
17402
  while (i >= 0 && !isMessage(entries[i].event))
17227
17403
  i--;
@@ -17230,8 +17406,8 @@ class AcpSession {
17230
17406
  const e = entries[i].event;
17231
17407
  if (isMessage(e))
17232
17408
  break;
17233
- if (e.type === "handoff-landed")
17234
- out.unshift(handoffLandedContext(e.title, e.repoName, e.url));
17409
+ if (e.type === "piece-landed")
17410
+ out.unshift(pieceLandedContext(e.title, e.repoName, e.url, e.same, e.again));
17235
17411
  }
17236
17412
  return out;
17237
17413
  }
@@ -17358,7 +17534,7 @@ class AcpSession {
17358
17534
  }
17359
17535
  const options = readOptions(configOptions);
17360
17536
  this.emit({ type: "session-info", sessionId, ...currentValues(options) });
17361
- this.learn(options);
17537
+ this.learn(options, !resumed);
17362
17538
  const modeOption = modes ? null : readModeOption(configOptions);
17363
17539
  const live = {
17364
17540
  conn,
@@ -17426,9 +17602,10 @@ class AcpSession {
17426
17602
  live.modeId = modes.current;
17427
17603
  }
17428
17604
  }
17429
- learn(options) {
17430
- for (const [category, opt] of options)
17431
- this.d.onOptionsLearned?.(category, opt.choices);
17605
+ learn(options, fresh = false) {
17606
+ for (const [category, opt] of options) {
17607
+ this.d.onOptionsLearned?.(category, opt.choices, fresh ? opt.current : undefined);
17608
+ }
17432
17609
  }
17433
17610
  onUpdate(params, side, commands) {
17434
17611
  if (params.update.sessionUpdate === "available_commands_update") {
@@ -17542,6 +17719,9 @@ class AcpSession {
17542
17719
  const planPath = plan ? await this.d.onPlan?.(detail, ownFile) ?? null : null;
17543
17720
  const offered = plan ? orderPlanOptions(params.options) : params.options.filter((o) => o.kind !== "allow_always");
17544
17721
  const choices = offered.map((o) => ({ id: o.optionId, name: o.name, kind: o.kind }));
17722
+ const pieces = plan ? this.heldPieces : null;
17723
+ if (plan && planPath && !pieces && detail.trim())
17724
+ this.d.onPlanUnsplit?.(detail, planPath);
17545
17725
  return this.openAsk((id) => ({
17546
17726
  type: "agent-permission",
17547
17727
  id,
@@ -17549,6 +17729,7 @@ class AcpSession {
17549
17729
  ...parts.command ? { command: parts.command } : {},
17550
17730
  ...detail ? { detail } : {},
17551
17731
  ...planPath ? { plan: planPath } : {},
17732
+ ...pieces ? { pieces } : {},
17552
17733
  choices,
17553
17734
  ...params.toolCall.toolCallId ? { toolId: params.toolCall.toolCallId } : {},
17554
17735
  ts: Date.now()
@@ -17558,11 +17739,17 @@ class AcpSession {
17558
17739
  if (!choice)
17559
17740
  return { outcome: { outcome: "cancelled" } };
17560
17741
  const res = { outcome: { outcome: "selected", optionId: choice.id } };
17561
- if (plan && (choice.kind === "allow_once" || choice.kind === "allow_always")) {
17742
+ const approved = choice.kind === "allow_once" || choice.kind === "allow_always";
17743
+ if (plan && approved)
17562
17744
  this.d.setMode?.(modeAfterPlan(choice.name));
17563
- if (planPath)
17564
- return this.sayPlanEdited(detail, planPath).then(() => res);
17565
- }
17745
+ const decided = pieces ?? (plan ? this.heldPieces : null);
17746
+ if (decided) {
17747
+ if (this.heldPieces === decided)
17748
+ this.heldPieces = null;
17749
+ this.d.onPieces?.(decided, approved);
17750
+ }
17751
+ if (plan && approved && planPath)
17752
+ return this.sayPlanEdited(detail, planPath).then(() => res);
17566
17753
  return res;
17567
17754
  });
17568
17755
  }
@@ -17648,7 +17835,7 @@ class AcpSession {
17648
17835
  syncStatus() {
17649
17836
  if (this.status === "error")
17650
17837
  return;
17651
- const waiting = this.asks.size > 0 || this.openHandoffs.size > 0;
17838
+ const waiting = this.asks.size > 0 || this.openCards.size > 0;
17652
17839
  this.setStatus(waiting ? "waiting" : this.running || this.own ? "working" : "idle");
17653
17840
  }
17654
17841
  trackOwn(events) {
@@ -19001,12 +19188,27 @@ function defaultAgent(wt, d, preview, onProcess) {
19001
19188
  worktreeId: wt.id,
19002
19189
  cwd: wt.path,
19003
19190
  preview: () => previewContext(preview()),
19191
+ self: () => {
19192
+ const w = d.state.worktrees.find((w) => w.id === wt.id);
19193
+ return w ? selfContext(w) : undefined;
19194
+ },
19195
+ standing: async () => standingContext(await d.standing?.(wt.id) ?? null),
19004
19196
  pageErrors: () => d.pageErrors?.(wt.id),
19005
- mentioned: (text) => worktreeContext(text, d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree").map((w) => ({
19006
- branch: w.branch,
19007
- title: w.title,
19008
- transcript: transcriptPathFor(d.paths.transcriptsDir, w.id)
19009
- }))),
19197
+ mentioned: (text) => !text.includes("@") ? [] : worktreeContext(text, [
19198
+ ...d.state.worktrees.filter((w) => w.repoId === wt.repoId && w.id !== wt.id && w.kind === "worktree").map((w) => ({
19199
+ branch: w.branch,
19200
+ title: w.title,
19201
+ path: w.path,
19202
+ transcript: transcriptPathFor(d.paths.transcriptsDir, w.id)
19203
+ })),
19204
+ ...(d.archived?.(wt.repoId) ?? []).map((a) => ({
19205
+ branch: a.branch,
19206
+ title: a.title,
19207
+ path: a.path,
19208
+ transcript: a.transcript,
19209
+ archived: { at: a.archivedAt, landed: a.landed, restorable: a.restorable }
19210
+ }))
19211
+ ]),
19010
19212
  onProcess,
19011
19213
  spec,
19012
19214
  connect: (app, spec, prepared) => spawnAcp(app, d.agents.launch(spec, prepared), wt.path, wt.id),
@@ -19030,6 +19232,9 @@ function defaultAgent(wt, d, preview, onProcess) {
19030
19232
  if (d.state.setMode(wt.id, mode))
19031
19233
  d.hub.emit("worktreesChanged");
19032
19234
  },
19235
+ onPieces: (id, approved) => d.hub.emit("piecesDecided", wt.id, id, approved),
19236
+ onPlanUnsplit: (markdown, planPath) => d.hub.emit("planUnsplit", wt.id, markdown, planPath),
19237
+ onPiecesShown: (_id, pieces) => writePlanDocNow(wt.path, listDoc(pieces)),
19033
19238
  onPlan: async (markdown, ownFile) => {
19034
19239
  const w = d.state.worktree(wt.id);
19035
19240
  if (w && !w.planned) {
@@ -19048,9 +19253,9 @@ function defaultAgent(wt, d, preview, onProcess) {
19048
19253
  },
19049
19254
  planEdited: async (path, proposed) => planEdited(wt.path, path, proposed),
19050
19255
  option: (category) => d.state.requireWorktree(wt.id)[OPTION_FIELDS[category]],
19051
- onOptionsLearned: (category, choices) => {
19256
+ onOptionsLearned: (category, choices, runs) => {
19052
19257
  const agentId = d.state.requireWorktree(wt.id).agent ?? "";
19053
- if (d.state.setCachedOptions(agentId, category, choices))
19258
+ if (d.state.setCachedOptions(agentId, category, choices, runs))
19054
19259
  d.hub.emit("agentsChanged");
19055
19260
  },
19056
19261
  onOptionChanged: (category, value) => {
@@ -19877,9 +20082,19 @@ function freeSlot(parent) {
19877
20082
  return { id, dir };
19878
20083
  }
19879
20084
  }
20085
+ var NAME_WORDS_MAX = 3;
20086
+ var NAME_CHARS_MAX = 20;
20087
+ var NAME_SHAPE = `1 to ${NAME_WORDS_MAX} plain words, ${NAME_CHARS_MAX} characters at most, like "sticky header"`;
20088
+ function overNameShape(title) {
20089
+ const words = title.split(/\s+/).filter(Boolean).length;
20090
+ const over = words > NAME_WORDS_MAX ? `${words} words` : title.length > NAME_CHARS_MAX ? `${title.length} characters` : null;
20091
+ return over && `runs to ${over}; a title is ${NAME_SHAPE}.`;
20092
+ }
19880
20093
  function titleFrom(prompt) {
19881
- const words = prompt.split(/\s+/).filter(Boolean).slice(0, 3).join(" ");
19882
- return sentenceCase(cleanTitle2(words) || "task");
20094
+ const words = prompt.split(/\s+/).filter(Boolean).slice(0, NAME_WORDS_MAX);
20095
+ while (words.length > 1 && words.join(" ").length > NAME_CHARS_MAX)
20096
+ words.pop();
20097
+ return sentenceCase(cleanTitle2(words.join(" ").slice(0, NAME_CHARS_MAX)) || "task");
19883
20098
  }
19884
20099
  function cleanTitle2(title) {
19885
20100
  return title.replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").slice(0, 40).replace(/^[\s:;,.!?]+|[\s:;,.!?]+$/g, "");
@@ -19890,15 +20105,6 @@ function branchSlug(title) {
19890
20105
  function sentenceCase(title) {
19891
20106
  return title.charAt(0).toUpperCase() + title.slice(1);
19892
20107
  }
19893
- var VARIANT_LENSES = [
19894
- "Take the most direct approach: the change most people would expect, in the smallest diff.",
19895
- "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.",
19896
- "Lean toward behaviour: edge cases, empty and error states, and what can go wrong, over visual change."
19897
- ];
19898
- function variantLens(index) {
19899
- const lens = VARIANT_LENSES[(index - 1) % VARIANT_LENSES.length];
19900
- 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})`;
19901
- }
19902
20108
 
19903
20109
  // ../daemon/src/agent/recap.ts
19904
20110
  var DAEMON_STOPS = new Set(["interrupted", "failed", "auth", "lost"]);
@@ -19939,6 +20145,11 @@ function turnsSince(entries, seenAt) {
19939
20145
  (into?.fixes ?? pendingFixes).push(e.why);
19940
20146
  break;
19941
20147
  }
20148
+ case "handed": {
20149
+ const into = open && open.stop === undefined && open.error === undefined && !open.auth ? open : null;
20150
+ (into?.asks ?? pending).push(e.body ?? e.text);
20151
+ break;
20152
+ }
19942
20153
  case "turn-start":
19943
20154
  open = start(e.ts);
19944
20155
  break;
@@ -20014,10 +20225,6 @@ function openAskOf(entries) {
20014
20225
  open.set(e.id, e.title);
20015
20226
  else if (e.type === "agent-ask-end")
20016
20227
  open.delete(e.id);
20017
- else if (e.type === "handoff-proposed")
20018
- open.set(e.id, `Continue in ${e.repo.name}?`);
20019
- else if (e.type === "handoff-go" || e.type === "handoff" || e.type === "handoff-declined")
20020
- open.delete(e.id);
20021
20228
  }
20022
20229
  const last = [...open.values()].at(-1)?.trim();
20023
20230
  return last ? clip(last) : undefined;
@@ -20037,9 +20244,15 @@ function factsOf(turns, end, ask) {
20037
20244
  };
20038
20245
  }
20039
20246
  function firstAskOf(entries) {
20040
- for (const { event: e } of entries)
20247
+ for (const { event: e } of entries) {
20041
20248
  if (e.type === "user-message" && e.text.trim())
20042
20249
  return e.text;
20250
+ if (e.type === "handed") {
20251
+ const said = e.body ?? e.text;
20252
+ if (said.trim())
20253
+ return said;
20254
+ }
20255
+ }
20043
20256
  return;
20044
20257
  }
20045
20258
  var RECAP_MAX = 160;
@@ -20195,49 +20408,6 @@ function plain(line) {
20195
20408
  return line.replace(/(\d)\s*[\u2013\u2014]\s*(\d)/g, "$1-$2").replace(/\s*[\u2013\u2014]\s*/g, ", ").replace(/\s+--\s+/g, ", ").replace(/\s*(\u2192|->)\s*/g, " to ").replace(/\u2026/g, "...");
20196
20409
  }
20197
20410
 
20198
- // ../daemon/src/agent/oneshot.ts
20199
- async function askFreshAgent(agents, agentId, cwd, system, prompt, opts = {}) {
20200
- const spec = agents.require(agentId);
20201
- const prepared = await prepareLaunch(cwd, spec);
20202
- const listeners = new Map;
20203
- const app = client({ name: "toyon" }).onRequest(methods.client.session.requestPermission, (c) => decideUnattended(c.params, prepared.bounds, cwd)).onNotification(methods.client.session.update, (c) => {
20204
- const u = c.params.update;
20205
- if (u.sessionUpdate === "agent_message_chunk" && u.content.type === "text") {
20206
- listeners.get(c.params.sessionId)?.(u.content.text);
20207
- }
20208
- });
20209
- const link = spawnAcp(app, agents.launch(spec, prepared), cwd, `ask:${agentId}`);
20210
- try {
20211
- const init = await link.conn.agent.request(methods.agent.initialize, {
20212
- protocolVersion: PROTOCOL_VERSION2,
20213
- clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
20214
- clientInfo: { name: "toyon", version: "0" }
20215
- });
20216
- return await askOnce(link.conn.agent, {
20217
- cwd,
20218
- system,
20219
- prompt,
20220
- spec,
20221
- ...opts.quick ? { quick: opts.quick } : {},
20222
- route: (id, onText) => {
20223
- listeners.set(id, onText);
20224
- return () => listeners.delete(id);
20225
- },
20226
- caps: {
20227
- close: !!init.agentCapabilities?.sessionCapabilities?.close,
20228
- delete: !!init.agentCapabilities?.sessionCapabilities?.delete
20229
- },
20230
- tag: `ask:${agentId}`
20231
- });
20232
- } catch (e) {
20233
- log.warn(`ask:${agentId}`, "one-shot question failed", e);
20234
- return null;
20235
- } finally {
20236
- link.conn.close();
20237
- await link.kill();
20238
- }
20239
- }
20240
-
20241
20411
  // ../daemon/src/agent/tasks.ts
20242
20412
  var NAME_SYSTEM = "You are a naming assistant. Reply with only the requested name.";
20243
20413
  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.
@@ -20265,22 +20435,34 @@ function parseName(text) {
20265
20435
  const line = text.trim().split(`
20266
20436
  `).at(-1) ?? "";
20267
20437
  const name = line.toLowerCase().replace(/[`"'.]/g, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim();
20268
- const words = name.split(" ").filter(Boolean);
20269
- if (words.length < 1 || words.length > 3 || name.length < 3 || name.length > 20)
20438
+ if (name.length < 3 || overNameShape(name))
20270
20439
  return null;
20271
20440
  if (/error|fail|sorry|cannot|unable|unauthori|forbidden|denied|\b[45]\d\d\b/.test(name))
20272
20441
  return null;
20273
20442
  return sentenceCase(name);
20274
20443
  }
20275
- var PLAN_SYSTEM = "You are a task-planning assistant. Reply with only the requested JSON.";
20276
- var planPrompt = (request) => [
20277
- "Split this request into independent coding tasks that could each be done in a separate git branch by a separate engineer.",
20278
- "Reply with ONLY a JSON array of task description strings (1 to 5 items), nothing else.",
20279
- "If the request is really one task, reply with a single-item array.",
20280
- `Request: ${request.slice(0, 2000)}`
20444
+ function makeNamer(runtime) {
20445
+ return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
20446
+ }
20447
+ function lacksQuickModel(spec, state) {
20448
+ const quick = spec.quickModel;
20449
+ if (!quick)
20450
+ return true;
20451
+ if (typeof quick === "function")
20452
+ return false;
20453
+ const offered = state.cachedOptions(spec.id, "model");
20454
+ return offered.length > 0 && !offered.some((m) => m.id === quick);
20455
+ }
20456
+ var PIECES_SYSTEM = "You read plans and reply with only the requested JSON.";
20457
+ var piecesPrompt = (markdown) => [
20458
+ "Below is a plan. List the parts of it the plan itself means to build apart from the rest: a step or section it marks as its own branch, a sibling worktree, a follow-up, or independent work. Also list the part it keeps in this worktree, if it says so.",
20459
+ `Reply with ONLY a JSON array of 0 to 6 objects, each {"title": a name for the part, ${NAME_SHAPE}, "section": the exact heading text of that part, "plan": the plan for that part's agent in markdown, from the plan's own words: what to build, why, the files, and what done looks like, 150 to 500 words, "where": "worktree" or "here", "after": optional, "this" when the plan says the part waits for this branch to land}.`,
20460
+ "Reply with an empty array when the plan is one piece of work with no part set apart.",
20461
+ `Plan:
20462
+ ${markdown.slice(0, 12000)}`
20281
20463
  ].join(`
20282
20464
  `);
20283
- function parsePlan(text) {
20465
+ function parsePieces(text) {
20284
20466
  if (!text)
20285
20467
  return null;
20286
20468
  const start = text.indexOf("[");
@@ -20291,23 +20473,31 @@ function parsePlan(text) {
20291
20473
  const arr = JSON.parse(text.slice(start, end + 1));
20292
20474
  if (!Array.isArray(arr))
20293
20475
  return null;
20294
- const tasks = arr.filter((x) => typeof x === "string" && x.trim()).slice(0, 5);
20295
- return tasks.length > 0 ? tasks : null;
20476
+ const out = [];
20477
+ for (const x of arr) {
20478
+ if (!x || typeof x !== "object")
20479
+ continue;
20480
+ const { title, section, plan, where, after } = x;
20481
+ if (typeof title !== "string" || !title.trim() || typeof section !== "string" || !section.trim())
20482
+ continue;
20483
+ if (where !== "here" && where !== "worktree")
20484
+ continue;
20485
+ out.push({
20486
+ title: title.trim(),
20487
+ section: section.trim(),
20488
+ ...typeof plan === "string" && plan.trim() ? { plan: plan.trim() } : {},
20489
+ where,
20490
+ ...typeof after === "string" && after.trim() ? { after: after.trim() } : {}
20491
+ });
20492
+ }
20493
+ return out.slice(0, 6);
20296
20494
  } catch {
20297
20495
  return null;
20298
20496
  }
20299
20497
  }
20300
- function makeNamer(runtime) {
20301
- return async (task, wt) => parseName(await runtime.ensureAgent(wt).agent.ask(NAME_SYSTEM, namePrompt(task), { quick: "prefer" }));
20302
- }
20303
- function lacksQuickModel(spec, state) {
20304
- const quick = spec.quickModel;
20305
- if (!quick)
20306
- return true;
20307
- if (typeof quick === "function")
20308
- return false;
20309
- const offered = state.cachedOptions(spec.id, "model");
20310
- return offered.length > 0 && !offered.some((m) => m.id === quick);
20498
+ function makePiecesReader(...deps) {
20499
+ const ask = makeQuickAsker(...deps);
20500
+ return async (wt, markdown) => parsePieces(await ask(wt, PIECES_SYSTEM, piecesPrompt(markdown)));
20311
20501
  }
20312
20502
  function makeQuickAsker(runtime, agents, state) {
20313
20503
  return async (wt, system, prompt) => {
@@ -20326,17 +20516,14 @@ function makeAnswerRecapper(...deps) {
20326
20516
  const ask = makeQuickAsker(...deps);
20327
20517
  return async (wt, prompt) => parseRecap(await ask(wt, ANSWER_SYSTEM, prompt));
20328
20518
  }
20329
- function makePlanner(agents) {
20330
- return async (request, cwd, agentId) => parsePlan(await askFreshAgent(agents, agentId, cwd, PLAN_SYSTEM, planPrompt(request), { quick: "prefer" }));
20331
- }
20332
20519
 
20333
20520
  // ../daemon/src/agent/transcriptForward.ts
20334
20521
  import { existsSync as existsSync15, readdirSync as readdirSync5, readFileSync as readFileSync12, renameSync as renameSync3, writeFileSync as writeFileSync7 } from "fs";
20335
20522
  import { join as join13 } from "path";
20336
- var FORWARD_STEP = "1";
20337
- var MARK = '"page';
20523
+ var FORWARD_STEP = "3";
20524
+ var MARKS = ['"page', '"brief"', '"handoff'];
20338
20525
  function forwardLine(line) {
20339
- if (!line.includes(MARK))
20526
+ if (!MARKS.some((m) => line.includes(m)))
20340
20527
  return null;
20341
20528
  let entry;
20342
20529
  try {
@@ -20349,6 +20536,73 @@ function forwardLine(line) {
20349
20536
  return { drop: true };
20350
20537
  if (e?.type === "fix-asked" && e.kind === "page")
20351
20538
  return { line: JSON.stringify({ ...entry, event: { ...e, kind: "command" } }) };
20539
+ if (e?.type === "user-message" && e.brief && typeof e.brief === "object") {
20540
+ const brief = e.brief;
20541
+ const { brief: _brief, ...rest } = e;
20542
+ return {
20543
+ line: JSON.stringify({
20544
+ ...entry,
20545
+ event: {
20546
+ ...rest,
20547
+ type: "handed",
20548
+ via: "message",
20549
+ body: brief.text,
20550
+ origin: brief.origin,
20551
+ ...brief.note ? { note: brief.note } : {}
20552
+ }
20553
+ })
20554
+ };
20555
+ }
20556
+ if (e?.type === "handoff-proposed") {
20557
+ const repo = e.repo ?? {};
20558
+ return {
20559
+ line: JSON.stringify({
20560
+ ...entry,
20561
+ event: {
20562
+ type: "pieces-proposed",
20563
+ id: e.id,
20564
+ pieces: [
20565
+ {
20566
+ id: "p1",
20567
+ title: e.title ?? "Continue",
20568
+ plan: e.message ?? "",
20569
+ attachments: [],
20570
+ fate: "worktree",
20571
+ project: { id: repo.id, name: repo.name }
20572
+ }
20573
+ ],
20574
+ mode: e.mode,
20575
+ ts: e.ts
20576
+ }
20577
+ })
20578
+ };
20579
+ }
20580
+ if (e?.type === "handoff-landed")
20581
+ return { line: JSON.stringify({ ...entry, event: { ...e, type: "piece-landed" } }) };
20582
+ if (e?.type === "pieces-proposed" && Array.isArray(e.pieces) && e.pieces.some((p) => p && typeof p === "object" && ("brief" in p))) {
20583
+ const pieces = e.pieces.map(({ brief, ...rest }) => brief === undefined ? rest : { ...rest, plan: brief });
20584
+ return { line: JSON.stringify({ ...entry, event: { ...e, pieces } }) };
20585
+ }
20586
+ if (e?.type === "handoff-go")
20587
+ return { line: JSON.stringify({ ...entry, event: { ...e, type: "pieces-go" } }) };
20588
+ if (e?.type === "handoff") {
20589
+ return {
20590
+ line: JSON.stringify({
20591
+ ...entry,
20592
+ event: {
20593
+ type: "pieces",
20594
+ id: e.id,
20595
+ outcomes: [
20596
+ { pieceId: "p1", fate: "worktree", worktreeId: e.worktreeId, title: e.title, repoName: e.repoName }
20597
+ ],
20598
+ ts: e.ts
20599
+ }
20600
+ })
20601
+ };
20602
+ }
20603
+ if (e?.type === "handoff-declined") {
20604
+ return { line: JSON.stringify({ ...entry, event: { ...e, type: "pieces-declined" } }) };
20605
+ }
20352
20606
  return null;
20353
20607
  }
20354
20608
  function forwardText(text) {
@@ -21576,6 +21830,15 @@ function loadState(paths) {
21576
21830
  log.error("state", `${STATE_FILE} is not valid JSON; copied to ${backup} and starting empty`, e);
21577
21831
  return structuredClone(empty);
21578
21832
  }
21833
+ for (const w of state.worktrees) {
21834
+ for (const q of w.queued ?? []) {
21835
+ const old = q;
21836
+ if (old.brief !== undefined && old.plan === undefined) {
21837
+ old.plan = old.brief;
21838
+ delete old.brief;
21839
+ }
21840
+ }
21841
+ }
21579
21842
  const ids = new Set(state.worktrees.map((w) => w.id));
21580
21843
  for (const id of Object.keys(state.sessions))
21581
21844
  if (!ids.has(id))
@@ -21786,12 +22049,22 @@ class StateStore {
21786
22049
  cachedOptions(agentId, category) {
21787
22050
  return this.state.optionCache?.[agentId]?.[category] ?? [];
21788
22051
  }
21789
- setCachedOptions(agentId, category, choices) {
21790
- if (JSON.stringify(this.cachedOptions(agentId, category)) === JSON.stringify(choices))
22052
+ cachedDefault(agentId, category) {
22053
+ return this.state.optionDefaults?.[agentId]?.[category];
22054
+ }
22055
+ setCachedOptions(agentId, category, choices, runs) {
22056
+ const sameList = JSON.stringify(this.cachedOptions(agentId, category)) === JSON.stringify(choices);
22057
+ const sameDefault = runs === undefined || runs === this.cachedDefault(agentId, category);
22058
+ if (sameList && sameDefault)
21791
22059
  return false;
21792
22060
  this.state.optionCache ??= {};
21793
22061
  this.state.optionCache[agentId] ??= {};
21794
22062
  this.state.optionCache[agentId][category] = choices;
22063
+ if (runs !== undefined) {
22064
+ this.state.optionDefaults ??= {};
22065
+ this.state.optionDefaults[agentId] ??= {};
22066
+ this.state.optionDefaults[agentId][category] = runs;
22067
+ }
21795
22068
  this.save();
21796
22069
  return true;
21797
22070
  }
@@ -28610,9 +28883,6 @@ class Interest {
28610
28883
  // ../daemon/src/server/ws.ts
28611
28884
  import { homedir as homedir12, hostname as hostname2 } from "os";
28612
28885
 
28613
- // ../daemon/src/server/handlers.ts
28614
- import { randomBytes as randomBytes7 } from "crypto";
28615
-
28616
28886
  // ../daemon/src/agent/config.ts
28617
28887
  import { existsSync as existsSync28, readFileSync as readFileSync17 } from "fs";
28618
28888
  import { homedir as homedir11 } from "os";
@@ -29234,6 +29504,12 @@ var notify = async (s, ctx, worktreeId, msg) => {
29234
29504
  await gitStatus(s, ctx, worktreeId);
29235
29505
  };
29236
29506
  var requireRun = (s, id) => s.worktrees.requireRun(id);
29507
+ function backfillOf(entries, since) {
29508
+ const last = lastSeq(entries);
29509
+ if (since === undefined || since > last)
29510
+ return { events: coalesce(entries), last };
29511
+ return { events: coalesce(after(entries, since)), last, after: since };
29512
+ }
29237
29513
  var handlers = {
29238
29514
  async subscribe(msg, ctx, s) {
29239
29515
  const r = s.worktrees.readable(msg.worktreeId);
@@ -29243,7 +29519,7 @@ var handlers = {
29243
29519
  throw new UserError("unknown worktree");
29244
29520
  if (!ctx.subscribe(msg.worktreeId))
29245
29521
  return;
29246
- ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: coalesce(kept), log: [] });
29522
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, ...backfillOf(kept, msg.since), log: [] });
29247
29523
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
29248
29524
  ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
29249
29525
  await gitStatus(s, ctx, msg.worktreeId);
@@ -29252,7 +29528,7 @@ var handlers = {
29252
29528
  if (!ctx.subscribe(msg.worktreeId))
29253
29529
  return;
29254
29530
  if (!r.wt) {
29255
- ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: [], log: [] });
29531
+ ctx.reply({ t: "backfill", worktreeId: msg.worktreeId, events: [], last: -1, log: [] });
29256
29532
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: [] });
29257
29533
  ctx.reply({ t: "agent-commands", worktreeId: msg.worktreeId, commands: [] });
29258
29534
  await gitAndPages(s, ctx, msg.worktreeId);
@@ -29262,7 +29538,7 @@ var handlers = {
29262
29538
  ctx.reply({
29263
29539
  t: "backfill",
29264
29540
  worktreeId: msg.worktreeId,
29265
- events: coalesce(agent.transcript()),
29541
+ ...backfillOf(agent.transcript(), msg.since),
29266
29542
  log: s.runtime.recentLogs(msg.worktreeId)
29267
29543
  });
29268
29544
  ctx.reply({ t: "queue", worktreeId: msg.worktreeId, items: agent.queueItems });
@@ -29329,29 +29605,8 @@ var handlers = {
29329
29605
  effort: msg.effort,
29330
29606
  carry: msg.carry
29331
29607
  };
29332
- const of = msg.variants ?? 1;
29333
29608
  await sending(s, ctx, msg, msg.attachments, async () => {
29334
- if (of === 1) {
29335
- await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
29336
- return;
29337
- }
29338
- const group = randomBytes7(4).toString("hex");
29339
- const made = await Promise.allSettled(Array.from({ length: of }, (_, i) => s.worktrees.create(msg.repoId, msg.prompt, {
29340
- ...opts,
29341
- ...i === 0 ? { worktreeId: msg.worktreeId } : {},
29342
- variant: { group, index: i + 1, of }
29343
- })));
29344
- const failed = made.flatMap((m) => m.status === "rejected" ? [m.reason] : []);
29345
- if (failed.length === 0)
29346
- return;
29347
- const went = failed.find((e) => e instanceof UserError && e.delivered);
29348
- if (went)
29349
- throw went;
29350
- if (failed.length < of) {
29351
- const why = failed[0] instanceof Error ? failed[0].message : String(failed[0]);
29352
- throw new UserError(`${failed.length} of ${of} attempts did not start: ${why}`, { delivered: true });
29353
- }
29354
- throw failed[0];
29609
+ await s.worktrees.create(msg.repoId, msg.prompt, { ...opts, worktreeId: msg.worktreeId });
29355
29610
  });
29356
29611
  },
29357
29612
  "set-worktree-profile"(msg, _ctx, s) {
@@ -29372,27 +29627,6 @@ var handlers = {
29372
29627
  "set-worktree-effort"(msg, _ctx, s) {
29373
29628
  s.worktrees.setEffort(msg.worktreeId, msg.effort);
29374
29629
  },
29375
- "batch-worktrees"(msg, ctx, s) {
29376
- const repo = s.state.requireRepo(msg.repoId);
29377
- if (msg.boxId)
29378
- s.drafts.take(msg.boxId);
29379
- fireAndForget(msg.repoId, (async () => {
29380
- const agent = msg.agent ?? s.state.defaultAgent ?? DEFAULT_AGENT_ID;
29381
- const tasks = await s.planTasks(msg.prompt, repo.path, agent) ?? [msg.prompt];
29382
- let failed = 0;
29383
- for (const task of tasks) {
29384
- try {
29385
- await s.worktrees.create(msg.repoId, task, { agent: msg.agent, model: msg.model, effort: msg.effort });
29386
- } catch (e) {
29387
- failed++;
29388
- log.warn(msg.repoId, `batch: could not start "${task.slice(0, 60)}"`, e);
29389
- }
29390
- }
29391
- const started = tasks.length - failed;
29392
- if (failed)
29393
- ctx.reply(shipped("", false, `batch: ${started} started, ${failed} failed (see daemon log)`));
29394
- })(), "batch");
29395
- },
29396
29630
  async "archive-worktree"(msg, _ctx, s) {
29397
29631
  await s.worktrees.archiveWorktree(msg.worktreeId);
29398
29632
  },
@@ -29541,9 +29775,6 @@ var handlers = {
29541
29775
  s.turns.stoppedByPerson(msg.worktreeId);
29542
29776
  s.runtime.agentFor(msg.worktreeId)?.stop();
29543
29777
  },
29544
- async "pick-variant"(msg, _ctx, s) {
29545
- await s.worktrees.pickVariant(msg.worktreeId);
29546
- },
29547
29778
  unqueue(msg, _ctx, s) {
29548
29779
  requireRun(s, msg.worktreeId);
29549
29780
  s.runtime.agentFor(msg.worktreeId)?.unqueue(msg.index, msg.edit ? (m) => s.drafts.putBack(msg.worktreeId, { text: m.text, items: m.attachments ?? [] }) : undefined);
@@ -29718,13 +29949,17 @@ var handlers = {
29718
29949
  s.runtime.agentFor(msg.worktreeId)?.answer(msg.askId, { kind: "choice", choiceId: msg.choiceId, ...msg.note ? { note: msg.note } : {} });
29719
29950
  s.worktrees.markPrompted(msg.worktreeId);
29720
29951
  },
29721
- async "handoff-answer"(msg, _ctx, s) {
29952
+ async "pieces-ask"(msg, _ctx, s) {
29953
+ requireRun(s, msg.worktreeId);
29954
+ await s.pieces.ask(msg.worktreeId, msg.repoId, msg.text);
29955
+ },
29956
+ "pieces-fate"(msg, _ctx, s) {
29722
29957
  requireRun(s, msg.worktreeId);
29723
- await s.handoff.answer(msg.worktreeId, msg.id, msg.go, msg.note);
29958
+ s.pieces.fate(msg.worktreeId, msg.id, msg.pieceId, msg.fate);
29724
29959
  },
29725
- async "handoff-ask"(msg, _ctx, s) {
29960
+ async "pieces-answer"(msg, _ctx, s) {
29726
29961
  requireRun(s, msg.worktreeId);
29727
- await s.handoff.ask(msg.worktreeId, msg.repoId, msg.text);
29962
+ await s.pieces.answer(msg.worktreeId, msg.id, msg.go, msg.note);
29728
29963
  },
29729
29964
  "agent-config"(msg, ctx, s) {
29730
29965
  const spec = s.agents.require(msg.agent);
@@ -29806,7 +30041,7 @@ var handlers = {
29806
30041
  },
29807
30042
  "preview-start"(msg, _ctx, s) {
29808
30043
  requireRun(s, msg.worktreeId);
29809
- s.worktrees.startPreview(msg.worktreeId);
30044
+ s.worktrees.retryPreview(msg.worktreeId);
29810
30045
  },
29811
30046
  async "search-refs"(msg, ctx, s) {
29812
30047
  const refs = await s.refs.search(msg.repoId, msg.query);
@@ -29889,6 +30124,47 @@ function withCors(res, origin) {
29889
30124
  return res;
29890
30125
  }
29891
30126
 
30127
+ // ../daemon/src/server/encoding.ts
30128
+ import { promisify } from "util";
30129
+ import { gzip } from "zlib";
30130
+ var gzipAsync = promisify(gzip);
30131
+ function acceptedEncodings(header) {
30132
+ const out = new Set;
30133
+ if (!header)
30134
+ return out;
30135
+ for (const part of header.split(",")) {
30136
+ const [name = "", ...params] = part.trim().toLowerCase().split(";");
30137
+ const coding = name.trim();
30138
+ if (coding !== "br" && coding !== "gzip")
30139
+ continue;
30140
+ const refused = params.some((p) => /^\s*q\s*=\s*0(\.0*)?\s*$/.test(p));
30141
+ if (!refused)
30142
+ out.add(coding);
30143
+ }
30144
+ return out;
30145
+ }
30146
+ function pickSibling(file, accepted) {
30147
+ const candidates = [
30148
+ ["br", `${file}.br`],
30149
+ ["gzip", `${file}.gz`]
30150
+ ];
30151
+ for (const [encoding, path] of candidates) {
30152
+ if (accepted.has(encoding) && Bun.file(path).size > 0)
30153
+ return { path, encoding };
30154
+ }
30155
+ return { path: file, encoding: null };
30156
+ }
30157
+ var GZIP_MIN_BYTES = 1024;
30158
+ async function encoded(req, body, headers) {
30159
+ const bytes = Buffer.from(body);
30160
+ if (bytes.byteLength < GZIP_MIN_BYTES || !acceptedEncodings(req.headers.get("accept-encoding")).has("gzip")) {
30161
+ return new Response(bytes, { headers });
30162
+ }
30163
+ return new Response(await gzipAsync(bytes), {
30164
+ headers: { ...headers, "content-encoding": "gzip", vary: "accept-encoding" }
30165
+ });
30166
+ }
30167
+
29892
30168
  // ../daemon/src/server/pairing.ts
29893
30169
  var NO_STORE = { "cache-control": "no-store" };
29894
30170
  var json = (body, headers = {}) => Response.json(body, { headers: { ...NO_STORE, ...headers } });
@@ -30008,6 +30284,7 @@ function createFetch(opts) {
30008
30284
  const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${opts.token}`);
30009
30285
  const unauthorized = () => new Response("unauthorized", { status: 401 });
30010
30286
  const gateFor = (remote) => remote && { grant, host: remote.host, redeem: (code) => opts.grants.redeem(code) };
30287
+ const page = async (req, file) => encoded(req, await Bun.file(file).text(), { "content-type": "text/html;charset=utf-8", "cache-control": NO_STORE2 });
30011
30288
  const health = async () => {
30012
30289
  const remote = opts.remote();
30013
30290
  return Response.json({
@@ -30060,6 +30337,7 @@ function createFetch(opts) {
30060
30337
  const authed = sameSecret(url.searchParams.get("token"), opts.token);
30061
30338
  if (authed)
30062
30339
  opts.noteShellOrigin(req.headers.get("origin"));
30340
+ const known = parseKnown(url.searchParams.get("known"));
30063
30341
  const data = {
30064
30342
  authed,
30065
30343
  subs: new Set,
@@ -30068,6 +30346,7 @@ function createFetch(opts) {
30068
30346
  sent: 0,
30069
30347
  bytes: 0,
30070
30348
  view: null,
30349
+ ...known ? { known } : {},
30071
30350
  ...remoteShell ? { remote: true } : {}
30072
30351
  };
30073
30352
  const headers = authed && remote && remoteShell ? { "set-cookie": grantCookie(grant, remote.host) } : undefined;
@@ -30080,10 +30359,13 @@ function createFetch(opts) {
30080
30359
  if (url.pathname === "/bootstrap") {
30081
30360
  if (!sameSecret(url.searchParams.get("token"), opts.token))
30082
30361
  return new Response("unauthorized", { status: 401 });
30083
- const headers = { "cache-control": NO_STORE2 };
30362
+ const headers = {
30363
+ "content-type": "application/json;charset=utf-8",
30364
+ "cache-control": NO_STORE2
30365
+ };
30084
30366
  if (remote && remoteShell)
30085
30367
  headers["set-cookie"] = grantCookie(grant, remote.host);
30086
- return Response.json(await opts.bootstrap(), { headers });
30368
+ return encoded(req, JSON.stringify(await opts.bootstrap()), headers);
30087
30369
  }
30088
30370
  if (url.pathname === "/restart" && (req.method === "POST" || req.method === "GET")) {
30089
30371
  if (!sameSecret(url.searchParams.get("token"), opts.token))
@@ -30107,6 +30389,9 @@ function createFetch(opts) {
30107
30389
  }
30108
30390
  });
30109
30391
  }
30392
+ if (url.pathname === "/share" && req.method === "POST") {
30393
+ return new Response(null, { status: 303, headers: { location: "/", "cache-control": NO_STORE2 } });
30394
+ }
30110
30395
  const paired = await opts.pairing(req, url, d);
30111
30396
  if (paired)
30112
30397
  return paired;
@@ -30228,7 +30513,7 @@ function createFetch(opts) {
30228
30513
  const hashed = rel.startsWith("/assets/");
30229
30514
  if (existsSync29(file) && Bun.file(file).size > 0) {
30230
30515
  if (rel === "/manifest.json") {
30231
- const ground = opts.manifestGround();
30516
+ const ground = url.searchParams.get("black") === "1" ? "#000000" : opts.manifestGround();
30232
30517
  const manifest = { ...await Bun.file(file).json(), theme_color: ground, background_color: ground };
30233
30518
  return Response.json(manifest, {
30234
30519
  headers: { "content-type": "application/manifest+json", "cache-control": NO_STORE2 }
@@ -30241,13 +30526,26 @@ function createFetch(opts) {
30241
30526
  headers: { "content-type": "text/html; charset=utf-8", "cache-control": NO_STORE2 }
30242
30527
  });
30243
30528
  }
30244
- return new Response(Bun.file(file), { headers: { "cache-control": hashed ? IMMUTABLE : NO_STORE2 } });
30529
+ if (hashed) {
30530
+ const sibling = pickSibling(file, acceptedEncodings(req.headers.get("accept-encoding")));
30531
+ return new Response(Bun.file(sibling.path), {
30532
+ headers: {
30533
+ "content-type": Bun.file(file).type,
30534
+ "cache-control": IMMUTABLE,
30535
+ vary: "accept-encoding",
30536
+ ...sibling.encoding ? { "content-encoding": sibling.encoding } : {}
30537
+ }
30538
+ });
30539
+ }
30540
+ if (rel === "/index.html")
30541
+ return page(req, file);
30542
+ return new Response(Bun.file(file), { headers: { "cache-control": NO_STORE2 } });
30245
30543
  }
30246
30544
  if (hashed)
30247
30545
  return new Response("not found", { status: 404 });
30248
30546
  const index = join25(opts.shellDist, "index.html");
30249
30547
  if (existsSync29(index))
30250
- return new Response(Bun.file(index), { headers: { "cache-control": NO_STORE2 } });
30548
+ return page(req, index);
30251
30549
  return new Response("Toyon daemon running; shell not built (run: bun run build)", { status: 200 });
30252
30550
  }
30253
30551
  return async function fetch2(req, srv) {
@@ -30274,12 +30572,35 @@ function createFetch(opts) {
30274
30572
  };
30275
30573
  }
30276
30574
 
30575
+ // ../daemon/src/server/stamps.ts
30576
+ var stampOf = (part) => Bun.hash(JSON.stringify(part)).toString(16);
30577
+ var visitsStamp = (visits) => stampOf(Object.entries(visits).map(([repoId, list]) => [repoId, list.map((e) => [e.path, e.title ?? ""])]));
30578
+ function stampsOf(h) {
30579
+ return {
30580
+ rows: stampOf({ rows: h.rows, trunks: h.trunks }),
30581
+ repos: stampOf(h.repos),
30582
+ visits: visitsStamp(h.visits),
30583
+ themes: stampOf(h.themes)
30584
+ };
30585
+ }
30586
+ function shapeHello(full, known) {
30587
+ if (!known)
30588
+ return full;
30589
+ const { themes, ...rest } = full;
30590
+ const withThemes = sameStamp(known.themes, full.stamps.themes) ? rest : full;
30591
+ const same = sameStamp(known.rows, full.stamps.rows) && sameStamp(known.repos, full.stamps.repos) && sameStamp(known.visits, full.stamps.visits);
30592
+ if (!same)
30593
+ return withThemes;
30594
+ const { rows: _r, trunks: _t, repos: _p, visits: _v, same: _s, ...light } = withThemes;
30595
+ return { ...light, same: true };
30596
+ }
30597
+
30277
30598
  // ../daemon/src/server/ws.ts
30278
30599
  var DROP_ABOVE_BYTES = 4 * 1024 * 1024;
30279
30600
  var DROPPED_NOTICE = `\r
30280
30601
  [toyon] output dropped: this pane was too far behind\r
30281
30602
  `;
30282
- var SENDS = new Set(["chat", "create-worktree", "restore-worktree", "batch-worktrees"]);
30603
+ var SENDS = new Set(["chat", "create-worktree", "restore-worktree"]);
30283
30604
  function sentFrom(json) {
30284
30605
  if (!json || typeof json !== "object")
30285
30606
  return null;
@@ -30313,7 +30634,7 @@ function startServer(opts) {
30313
30634
  };
30314
30635
  const raw = (ws, json) => {
30315
30636
  try {
30316
- ws.send(json);
30637
+ ws.send(json, true);
30317
30638
  ws.data.sent++;
30318
30639
  ws.data.bytes += json.length;
30319
30640
  } catch (e) {
@@ -30413,7 +30734,7 @@ function startServer(opts) {
30413
30734
  statusesQueued = false;
30414
30735
  try {
30415
30736
  const { rows, trunks } = s.worktrees.snapshot();
30416
- broadcast({ t: "worktrees", rows, trunks });
30737
+ broadcast({ t: "worktrees", rows, trunks, stamp: stampOf({ rows, trunks }) });
30417
30738
  } catch (e) {
30418
30739
  log.warn("ws", "worktrees broadcast failed", e);
30419
30740
  }
@@ -30422,7 +30743,7 @@ function startServer(opts) {
30422
30743
  s.hub.on("worktreesChanged", worktreesChanged);
30423
30744
  s.hub.on("agentStatus", worktreesChanged);
30424
30745
  s.hub.on("agentStopping", worktreesChanged);
30425
- s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos }));
30746
+ s.hub.on("reposChanged", () => broadcast({ t: "repos", repos: s.state.repos, stamp: stampOf(s.state.repos) }));
30426
30747
  s.hub.on("pendingChanged", () => broadcast({ t: "pending-repos", pending: s.repos.pending }));
30427
30748
  s.hub.on("proc", (worktreeId, proc) => broadcast({ t: "proc", worktreeId, proc }));
30428
30749
  s.hub.on("log", (worktreeId, proc, line, retract) => sendTo(worktreeId, retract ? { t: "log", worktreeId, proc, line, retract } : { t: "log", worktreeId, proc, line }));
@@ -30463,7 +30784,7 @@ function startServer(opts) {
30463
30784
  const themesChanged = () => {
30464
30785
  const cur = s.themes.current();
30465
30786
  setWaitingColors({ bg: cur.colors.surface0, fg: cur.colors.text1 });
30466
- broadcast({ t: "themes", themes: s.themes.themes, prefs: s.themes.prefs });
30787
+ broadcast({ t: "themes", themes: s.themes.themes, prefs: s.themes.prefs, stamp: stampOf(s.themes.themes) });
30467
30788
  };
30468
30789
  s.hub.on("themesChanged", themesChanged);
30469
30790
  themesChanged();
@@ -30489,23 +30810,22 @@ function startServer(opts) {
30489
30810
  s.hub.on("selfChanged", () => broadcast({ t: "self", self: s.self.get() }));
30490
30811
  s.hub.on("failed", (worktreeId, message) => broadcast({ t: "error", message, worktreeId }));
30491
30812
  s.hub.on("updateChanged", () => broadcast({ t: "update", update: s.update.get() }));
30492
- s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId) }));
30813
+ s.hub.on("visitsChanged", (repoId) => broadcast({ t: "visits", repoId, pages: s.routes.history(repoId), stamp: visitsStamp(s.routes.historyAll()) }));
30493
30814
  s.hub.on("archiveChanged", (repoId) => broadcast({ t: "archived", repoId, items: s.worktrees.archived(repoId) }));
30494
30815
  s.hub.on("draftChanged", (boxId, text, clientId) => broadcast({ t: "draft", boxId, text, ...clientId ? { clientId } : {} }));
30495
30816
  s.hub.on("attachmentsChanged", (boxId, items, clientId) => broadcast({ t: "attachments", boxId, items, ...clientId ? { clientId } : {} }));
30496
- const helloFrame = async () => {
30817
+ const helloFrame = async (known = null) => {
30497
30818
  await s.update.refresh();
30498
30819
  const { rows, trunks } = s.worktrees.snapshot();
30499
- return {
30820
+ const parts = { rows, trunks, repos: s.state.repos, visits: s.routes.historyAll(), themes: s.themes.themes };
30821
+ const full = {
30500
30822
  t: "hello",
30501
30823
  version,
30502
30824
  install: s.update.install(),
30503
30825
  registry: await s.update.registry(),
30504
30826
  protocol: PROTOCOL_VERSION,
30505
- repos: s.state.repos,
30506
- rows,
30507
- trunks,
30508
- themes: s.themes.themes,
30827
+ ...parts,
30828
+ stamps: stampsOf(parts),
30509
30829
  themePrefs: s.themes.prefs,
30510
30830
  agents: s.readiness.describe(),
30511
30831
  defaultAgent: s.agents.defaultId(s.state.defaultAgent),
@@ -30521,13 +30841,13 @@ function startServer(opts) {
30521
30841
  knocks: opts.knocks.pending(),
30522
30842
  gitIdentity: await s.repos.gitIdentity(),
30523
30843
  pending: s.repos.pending,
30524
- visits: s.routes.historyAll(),
30525
30844
  self: s.self.get(),
30526
30845
  update: s.update.get(),
30527
30846
  drafts: s.drafts.all(),
30528
30847
  attachments: s.drafts.allAttachments(),
30529
30848
  managed: managedView(opts.managed)
30530
30849
  };
30850
+ return shapeHello(full, known);
30531
30851
  };
30532
30852
  let branded = false;
30533
30853
  const authed = (req) => sameSecret(req.headers.get("authorization"), `Bearer ${token}`);
@@ -30578,6 +30898,7 @@ function startServer(opts) {
30578
30898
  changelog: () => Bun.file(opts.changelog).text().catch(() => null)
30579
30899
  }),
30580
30900
  websocket: {
30901
+ perMessageDeflate: true,
30581
30902
  async open(ws) {
30582
30903
  const preview = ws.data.preview;
30583
30904
  if (preview) {
@@ -30591,7 +30912,7 @@ function startServer(opts) {
30591
30912
  }
30592
30913
  sockets.add(ws);
30593
30914
  link(ws);
30594
- send(ws, await helloFrame());
30915
+ send(ws, await helloFrame(ws.data.known ?? null));
30595
30916
  fireAndForget("agents", s.agents.retryFailed(), "agent install retry");
30596
30917
  for (const o of s.opens.takePending())
30597
30918
  send(ws, openedFrame(o));
@@ -30889,7 +31210,7 @@ function discover(dir) {
30889
31210
  import { homedir as homedir14 } from "os";
30890
31211
  async function latestVersion() {
30891
31212
  const registry = await npmRegistry();
30892
- const r = await run("npm", ["view", "toyon", "version", "--json"], homedir14());
31213
+ const r = await run("npm", ["view", "toyon", "version", "--json"], homedir14(), undefined, { timeoutMs: NPM_MS });
30893
31214
  if (!r.ok)
30894
31215
  return { version: null, registry };
30895
31216
  try {
@@ -30899,8 +31220,9 @@ async function latestVersion() {
30899
31220
  return { version: null, registry };
30900
31221
  }
30901
31222
  }
31223
+ var NPM_MS = 30000;
30902
31224
  async function npmRegistry() {
30903
- const config = await run("npm", ["config", "get", "registry"], homedir14());
31225
+ const config = await run("npm", ["config", "get", "registry"], homedir14(), undefined, { timeoutMs: NPM_MS });
30904
31226
  return config.ok && config.out !== "" ? config.out : "the npm registry";
30905
31227
  }
30906
31228
  var LOG_POINTER = /complete log of this run/i;
@@ -30939,6 +31261,7 @@ class UpdateService {
30939
31261
  latest = null;
30940
31262
  unreachable = null;
30941
31263
  registryName = null;
31264
+ registryAsk = null;
30942
31265
  seen = null;
30943
31266
  seenAt = 0;
30944
31267
  installing = false;
@@ -30969,6 +31292,7 @@ class UpdateService {
30969
31292
  d.hub.on("holdsChanged", () => {
30970
31293
  this.busyAt = this.now();
30971
31294
  });
31295
+ fireAndForget("update", this.registry(), "npm registry");
30972
31296
  }
30973
31297
  async tick() {
30974
31298
  await this.refresh();
@@ -30993,7 +31317,15 @@ class UpdateService {
30993
31317
  async registry() {
30994
31318
  if (this.d.method === "none" || this.d.managedBy)
30995
31319
  return null;
30996
- this.registryName ??= await this.d.registry();
31320
+ if (this.registryName !== null)
31321
+ return this.registryName;
31322
+ this.registryAsk ??= this.d.registry();
31323
+ try {
31324
+ this.registryName = await this.registryAsk;
31325
+ } catch (e) {
31326
+ this.registryAsk = null;
31327
+ throw e;
31328
+ }
30997
31329
  return this.registryName;
30998
31330
  }
30999
31331
  async check() {
@@ -31572,6 +31904,9 @@ function saidRows(entries) {
31572
31904
  if (event.type === "user-message") {
31573
31905
  turnTs = event.ts;
31574
31906
  push(seq, "user", event.text, event.ts);
31907
+ } else if (event.type === "handed") {
31908
+ turnTs = event.ts;
31909
+ push(seq, "user", event.body ?? event.text, event.ts);
31575
31910
  } else if (event.type === "turn-start") {
31576
31911
  turnTs = event.ts;
31577
31912
  } else if (event.type === "text-delta") {
@@ -31705,7 +32040,7 @@ function lastAsked(entries) {
31705
32040
  const e = entries[i].event;
31706
32041
  if (e.type === "fix-asked")
31707
32042
  return e.kind;
31708
- if (e.type === "user-message")
32043
+ if (e.type === "user-message" || e.type === "handed")
31709
32044
  return;
31710
32045
  }
31711
32046
  return;
@@ -31810,240 +32145,6 @@ class FixService {
31810
32145
  }
31811
32146
  }
31812
32147
 
31813
- // ../daemon/src/worktrees/handoff.ts
31814
- import { randomUUID as randomUUID2 } from "crypto";
31815
- var MESSAGE_MAX = 20000;
31816
- var TITLE_MAX2 = 200;
31817
- var PROJECT_MAX = 200;
31818
- var PER_TURN_MAX = 3;
31819
- var refused = (text) => ({ text, isError: true });
31820
- function openProposals(entries) {
31821
- const open = new Map;
31822
- for (const { event } of entries) {
31823
- if (event.type === "handoff-proposed")
31824
- open.set(event.id, event);
31825
- else if (event.type === "handoff-go" || event.type === "handoff" || event.type === "handoff-declined")
31826
- open.delete(event.id);
31827
- }
31828
- return [...open.values()];
31829
- }
31830
- function proposedThisTurn(entries) {
31831
- let n = 0;
31832
- for (let i = entries.length - 1;i >= 0; i--) {
31833
- const e = entries[i].event;
31834
- if (e.type === "turn-start")
31835
- break;
31836
- if (e.type === "handoff-proposed")
31837
- n++;
31838
- }
31839
- return n;
31840
- }
31841
-
31842
- class HandoffService {
31843
- d;
31844
- constructor(d) {
31845
- this.d = d;
31846
- d.hub.on("landed", (worktreeId) => this.landed(worktreeId));
31847
- }
31848
- targets(worktreeId) {
31849
- const own = this.d.state.worktree(worktreeId)?.repoId;
31850
- return this.d.state.repos.filter((r) => r.id !== own);
31851
- }
31852
- resolve(worktreeId, project) {
31853
- const q = project.trim();
31854
- const others = this.targets(worktreeId);
31855
- if (others.length === 0)
31856
- return { refused: "No other project is open in Toyon, so there is nothing to hand off to." };
31857
- const names = others.map((r) => r.name).join(", ");
31858
- if (!q)
31859
- return { refused: `Name the project to continue in. Open projects: ${names}.` };
31860
- const found = match(others, q);
31861
- if (found.length === 1)
31862
- return { repo: found[0] };
31863
- if (found.length > 1) {
31864
- return {
31865
- refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
31866
- };
31867
- }
31868
- const wt = this.d.state.worktree(worktreeId);
31869
- const own = wt ? this.d.state.repo(wt.repoId) : undefined;
31870
- if (own && match([own], q).length === 1) {
31871
- return {
31872
- refused: `${own.name} is this worktree's own project; a handoff goes to another one. Open projects: ${names}.`
31873
- };
31874
- }
31875
- return { refused: `No open project is called "${q}". Open projects: ${names}.` };
31876
- }
31877
- tool(worktreeId) {
31878
- const names = this.targets(worktreeId).map((r) => r.name);
31879
- const open = names.length ? `Open projects you can name: ${names.join(", ")}.` : "No other project is open right now.";
31880
- return {
31881
- name: HANDOFF_TOOL,
31882
- description: `Propose continuing the current task in another project Toyon has open, when a change belongs there and not in this worktree. Nothing starts: a card with your message appears in this chat and the person decides. ${open}`,
31883
- inputSchema: {
31884
- type: "object",
31885
- properties: {
31886
- project: { type: "string", description: "The other project's name as listed, or its path." },
31887
- message: {
31888
- type: "string",
31889
- description: "What that project's agent starts from: what is needed and why, how to reproduce it, and the paths in this worktree that depend on it. It is sent as that agent's first message."
31890
- },
31891
- title: { type: "string", description: "A short name for the new worktree, a few words." }
31892
- },
31893
- required: ["project", "message"],
31894
- additionalProperties: false
31895
- }
31896
- };
31897
- }
31898
- async call(worktreeId, args) {
31899
- const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
31900
- const shaped = a && typeof a.project === "string" && typeof a.message === "string" && (a.title === undefined || typeof a.title === "string");
31901
- if (!shaped)
31902
- return refused("handoff takes { project: string, message: string, title?: string }.");
31903
- return this.propose(worktreeId, { project: a.project, message: a.message, ...a.title ? { title: a.title } : {} }, "agent");
31904
- }
31905
- propose(worktreeId, p, by) {
31906
- const wt = this.d.state.worktree(worktreeId);
31907
- if (!wt)
31908
- throw new UserError("that chat is gone");
31909
- if (p.project.length > PROJECT_MAX)
31910
- return refused(`The project name is over ${PROJECT_MAX} characters.`);
31911
- if ((p.title ?? "").length > TITLE_MAX2)
31912
- return refused(`The title is over ${TITLE_MAX2} characters; a few words.`);
31913
- if (p.message.length > MESSAGE_MAX) {
31914
- return refused(`The message is over ${MESSAGE_MAX} characters; cut it to what the other agent needs.`);
31915
- }
31916
- const target = this.resolve(worktreeId, p.project);
31917
- if ("refused" in target)
31918
- return refused(target.refused);
31919
- const message = p.message.trim();
31920
- if (!message)
31921
- return refused("Write the message the other project's agent should start from.");
31922
- const agent = this.d.runtime.ensureAgent(wt).agent;
31923
- const entries = agent.transcript();
31924
- if (openProposals(entries).length > 0) {
31925
- return refused("A handoff is already waiting for the person on this chat; finish your turn.");
31926
- }
31927
- if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
31928
- return refused("Three handoffs were proposed this turn already; finish your turn and let the person answer.");
31929
- }
31930
- const title = p.title?.trim();
31931
- const { repo } = target;
31932
- agent.note({
31933
- type: "handoff-proposed",
31934
- id: randomUUID2(),
31935
- repo: { id: repo.id, name: repo.name, path: repo.path },
31936
- message,
31937
- ...title ? { title } : {},
31938
- by,
31939
- mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
31940
- ts: Date.now()
31941
- });
31942
- return { text: "Proposed to the person as a card; they decide. Finish your turn." };
31943
- }
31944
- async answer(worktreeId, id, go, note) {
31945
- const wt = this.d.state.worktree(worktreeId);
31946
- if (!wt)
31947
- throw new UserError("that chat is gone");
31948
- const agent = this.d.runtime.ensureAgent(wt).agent;
31949
- const entries = agent.transcript();
31950
- const proposed = entries.find(({ event }) => event.type === "handoff-proposed" && event.id === id)?.event;
31951
- if (!proposed)
31952
- throw new UserError("That card is not in this chat");
31953
- if (!openProposals(entries).some((p) => p.id === id)) {
31954
- return log.debug(worktreeId, `answer for a handoff that already closed (${id})`);
31955
- }
31956
- if (!go) {
31957
- agent.note({ type: "handoff-declined", id, ts: Date.now() });
31958
- return;
31959
- }
31960
- agent.note({ type: "handoff-go", id, ts: Date.now() });
31961
- try {
31962
- const repo = this.d.state.repo(proposed.repo.id);
31963
- if (!repo)
31964
- throw new UserError(`${proposed.repo.name} is no longer open in Toyon`);
31965
- const origin = this.d.state.requireRepo(wt.repoId);
31966
- const added = note?.trim() || undefined;
31967
- const prompt = handoffPrompt(proposed.message, added, { path: wt.path, project: origin.name, branch: wt.branch });
31968
- const made = await this.d.create(repo.id, prompt, {
31969
- ...proposed.title ? { title: proposed.title } : {},
31970
- mode: proposed.mode,
31971
- from: { kind: "worktree", ref: wt.branch, origin: { id: wt.id, repoId: wt.repoId } }
31972
- });
31973
- agent.note({
31974
- type: "handoff",
31975
- id,
31976
- worktreeId: made.id,
31977
- repoId: repo.id,
31978
- repoName: repo.name,
31979
- title: made.title,
31980
- ts: Date.now()
31981
- });
31982
- } catch (e) {
31983
- const reason = e instanceof Error ? e.message : String(e);
31984
- agent.note({ type: "handoff-declined", id, outcome: "failed", reason, ts: Date.now() });
31985
- throw e;
31986
- }
31987
- }
31988
- async ask(worktreeId, repoId, text) {
31989
- const wt = this.d.state.requireWorktree(worktreeId);
31990
- const repo = this.d.state.repo(repoId);
31991
- if (!repo)
31992
- throw new UserError("no such project");
31993
- if (repo.id === wt.repoId)
31994
- throw new UserError(`${repo.name} is this worktree's own project`);
31995
- const agent = this.d.runtime.agentFor(worktreeId);
31996
- if (!agent)
31997
- throw new UserError("worktree still starting; try again in a moment");
31998
- if (agent.mcpTools === null)
31999
- await agent.warm();
32000
- if (agent.mcpTools === true) {
32001
- agent.send(handoffAskPrompt(repo, text), { asked: { kind: "handoff", why: `continue in ${repo.name}` } });
32002
- return;
32003
- }
32004
- const own = text?.trim();
32005
- if (!own)
32006
- throw new UserError(`Write what ${repo.name} should do first`);
32007
- const r = this.propose(worktreeId, { project: repo.path, message: own }, "person");
32008
- if (r.isError)
32009
- throw new UserError(r.text);
32010
- }
32011
- landed(worktreeId) {
32012
- const wt = this.d.state.worktree(worktreeId);
32013
- if (wt?.from?.kind !== "worktree")
32014
- return;
32015
- const origin = this.d.state.worktree(wt.from.origin.id);
32016
- if (origin?.kind !== "worktree")
32017
- return;
32018
- const repo = this.d.state.repo(wt.repoId);
32019
- if (!repo)
32020
- return;
32021
- this.d.runtime.ensureAgent(origin).agent.note({
32022
- type: "handoff-landed",
32023
- worktreeId: wt.id,
32024
- repoName: repo.name,
32025
- title: wt.title,
32026
- ...wt.pr?.url ? { url: wt.pr.url } : {},
32027
- ts: Date.now()
32028
- });
32029
- }
32030
- }
32031
- function match(repos, q) {
32032
- const lower = q.toLowerCase();
32033
- const ladder = [
32034
- (r) => r.path === q,
32035
- (r) => r.name === q,
32036
- (r) => r.name.toLowerCase() === lower,
32037
- (r) => r.name.toLowerCase().startsWith(lower)
32038
- ];
32039
- for (const rung of ladder) {
32040
- const found = repos.filter(rung);
32041
- if (found.length > 0)
32042
- return found;
32043
- }
32044
- return [];
32045
- }
32046
-
32047
32148
  // ../daemon/src/worktrees/landing.ts
32048
32149
  var TAIL_CHARS = 400;
32049
32150
  var DIFF_CHARS = 2000;
@@ -32395,6 +32496,900 @@ async function recentSubjects(path, defaultBr) {
32395
32496
  `).filter(Boolean) : [];
32396
32497
  }
32397
32498
 
32499
+ // ../daemon/src/worktrees/pieces.ts
32500
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
32501
+ import { readFileSync as readFileSync20 } from "fs";
32502
+ import { join as join28 } from "path";
32503
+
32504
+ // ../daemon/src/worktrees/proposals.ts
32505
+ var PER_TURN_MAX = 3;
32506
+ function proposedThisTurn(entries) {
32507
+ let n = 0;
32508
+ for (let i = entries.length - 1;i >= 0; i--) {
32509
+ const e = entries[i].event;
32510
+ if (e.type === "turn-start")
32511
+ break;
32512
+ if (e.type === "pieces-proposed")
32513
+ n++;
32514
+ }
32515
+ return n;
32516
+ }
32517
+ function matchBy(items, q, keys) {
32518
+ if (!q.trim())
32519
+ return [];
32520
+ const lower = q.toLowerCase();
32521
+ const rungs = [
32522
+ (v) => v === q,
32523
+ (v) => v.toLowerCase() === lower,
32524
+ (v) => v.toLowerCase().startsWith(lower)
32525
+ ];
32526
+ for (const rung of rungs) {
32527
+ const found = items.filter((item) => keys.some((key) => rung(key(item) ?? "")));
32528
+ if (found.length > 0)
32529
+ return found;
32530
+ }
32531
+ return [];
32532
+ }
32533
+
32534
+ // ../daemon/src/worktrees/pieces.ts
32535
+ function handedOrigin(origin, project) {
32536
+ return {
32537
+ id: origin.id,
32538
+ repoId: origin.repoId,
32539
+ branch: origin.branch,
32540
+ title: origin.title,
32541
+ ...project ? { project } : {}
32542
+ };
32543
+ }
32544
+ function pieceDoc(piece) {
32545
+ return `# ${piece.title}
32546
+
32547
+ ${piece.plan.trim()}
32548
+ `;
32549
+ }
32550
+ var PIECES_MAX = 6;
32551
+ var PLAN_MAX = 20000;
32552
+ var ATTACH_MAX = 10;
32553
+ var PROJECT_MAX = 200;
32554
+ var refused = (text) => ({ text, isError: true });
32555
+ 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
+ var SHOWN = "Handed to the person as a card; they decide. Finish your turn.";
32557
+ function openPlanCard(entries) {
32558
+ const open = new Map;
32559
+ for (const { event } of entries) {
32560
+ if (event.type === "agent-permission" && event.plan)
32561
+ open.set(event.id, event);
32562
+ else if (event.type === "agent-ask-end")
32563
+ open.delete(event.id);
32564
+ }
32565
+ return [...open.values()].at(-1) ?? null;
32566
+ }
32567
+ function sectionText(markdown, heading) {
32568
+ const norm = (s) => s.toLowerCase().replace(/[`*_~]/g, "").replace(/[^a-z0-9]+/g, " ").trim();
32569
+ const want = norm(heading);
32570
+ if (!want)
32571
+ return null;
32572
+ const lines = markdown.split(`
32573
+ `);
32574
+ const heads = [];
32575
+ for (let i = 0;i < lines.length; i++) {
32576
+ const m = /^(#{1,6})\s+(.*)$/.exec(lines[i] ?? "");
32577
+ const have = m ? norm(m[2] ?? "") : "";
32578
+ if (m && have)
32579
+ heads.push({ i, level: m[1].length, have });
32580
+ }
32581
+ const rungs = [
32582
+ (have) => have === want,
32583
+ (have) => have.includes(want),
32584
+ (have) => have.length >= 12 && want.includes(have)
32585
+ ];
32586
+ for (const rung of rungs) {
32587
+ const hit = heads.find((h) => rung(h.have));
32588
+ if (!hit)
32589
+ continue;
32590
+ const { i, level } = hit;
32591
+ let end = lines.length;
32592
+ for (let j = i + 1;j < lines.length; j++) {
32593
+ const n = /^(#{1,6})\s/.exec(lines[j] ?? "");
32594
+ if (n && n[1].length <= level) {
32595
+ end = j;
32596
+ break;
32597
+ }
32598
+ }
32599
+ const text = lines.slice(i, end).join(`
32600
+ `).trim();
32601
+ return text.length > PLAN_MAX ? text.slice(0, PLAN_MAX) : text;
32602
+ }
32603
+ return null;
32604
+ }
32605
+ function hashText(text) {
32606
+ return createHash5("sha1").update(text.trim()).digest("hex").slice(0, 16);
32607
+ }
32608
+ function heldUnshown(entries) {
32609
+ const shown = new Set;
32610
+ for (const { event } of entries)
32611
+ if (event.type === "pieces-shown")
32612
+ shown.add(event.id);
32613
+ return openPieces(entries).find((p) => p.held && !shown.has(p.id)) ?? null;
32614
+ }
32615
+ function openPieces(entries) {
32616
+ const open = new Map;
32617
+ for (const { event } of entries) {
32618
+ if (event.type === "pieces-proposed")
32619
+ open.set(event.id, event);
32620
+ else if (event.type === "pieces-go" || event.type === "pieces" || event.type === "pieces-declined")
32621
+ open.delete(event.id);
32622
+ }
32623
+ return [...open.values()];
32624
+ }
32625
+ function listFile(entries, id) {
32626
+ let file;
32627
+ for (const { event } of entries) {
32628
+ if ((event.type === "pieces-proposed" || event.type === "pieces-shown") && event.id === id && event.plan) {
32629
+ file = event.plan;
32630
+ } else if (event.type === "agent-permission" && event.pieces === id && event.plan)
32631
+ file = event.plan;
32632
+ }
32633
+ return file;
32634
+ }
32635
+ function effectiveFate(entries, proposal, piece) {
32636
+ let fate = piece.fate;
32637
+ for (const { event } of entries) {
32638
+ if (event.type === "pieces-fate" && event.id === proposal.id && event.pieceId === piece.id)
32639
+ fate = event.fate;
32640
+ }
32641
+ return fate;
32642
+ }
32643
+ var LABEL = /^(image|paste|pasted text|file|element)\s+(\d+)$/i;
32644
+ var KIND_OF = {
32645
+ image: "image",
32646
+ paste: "paste",
32647
+ "pasted text": "paste",
32648
+ file: "file",
32649
+ element: "pick"
32650
+ };
32651
+
32652
+ class PiecesService {
32653
+ d;
32654
+ constructor(d) {
32655
+ this.d = d;
32656
+ d.hub.on("piecesDecided", (worktreeId, id, approved) => fireAndForget(worktreeId, approved ? this.act(worktreeId, id, undefined, true) : this.decline(worktreeId, id), "pieces"));
32657
+ d.hub.on("planUnsplit", (worktreeId, markdown, planPath) => fireAndForget(worktreeId, this.read(worktreeId, markdown, planPath), "pieces read off the plan"));
32658
+ d.hub.on("treeChanged", (worktreeId) => fireAndForget(worktreeId, this.stale(worktreeId), "pieces stale"));
32659
+ d.hub.on("landed", (worktreeId) => fireAndForget(worktreeId, this.landed(worktreeId), "pieces after landing"));
32660
+ d.hub.on("worktreesChanged", () => this.sweep());
32661
+ this.sweep();
32662
+ }
32663
+ siblings(wt, repoId = wt.repoId) {
32664
+ return this.d.state.worktrees.filter((w) => w.repoId === repoId && w.kind === "worktree" && w.id !== wt.id);
32665
+ }
32666
+ otherProjects(wt) {
32667
+ return this.d.state.repos.filter((r) => r.id !== wt.repoId);
32668
+ }
32669
+ resolveProject(wt, project) {
32670
+ const q = project.trim();
32671
+ const others = this.otherProjects(wt);
32672
+ if (others.length === 0)
32673
+ return { refused: "no other project is open in Toyon." };
32674
+ const names = others.map((r) => r.name).join(", ");
32675
+ if (!q)
32676
+ return { refused: `name the project. Open projects: ${names}.` };
32677
+ const found = matchRepos(others, q);
32678
+ if (found.length === 1)
32679
+ return { repo: found[0] };
32680
+ if (found.length > 1) {
32681
+ return {
32682
+ refused: `"${q}" names more than one open project: ${found.map((r) => r.path).join(", ")}. Say which by its path.`
32683
+ };
32684
+ }
32685
+ const own = this.d.state.repo(wt.repoId);
32686
+ if (own && matchRepos([own], q).length === 1) {
32687
+ return { refused: `${own.name} is this worktree's own project; leave \`project\` out for a worktree here.` };
32688
+ }
32689
+ return { refused: `no open project is called "${q}". Open projects: ${names}.` };
32690
+ }
32691
+ tool(worktreeId) {
32692
+ const wt = this.d.state.worktree(worktreeId);
32693
+ const names = wt ? this.siblings(wt).map((w) => `${w.branch}, "${w.title}"`) : [];
32694
+ const open = names.length ? `Open worktrees of this project you can name in \`after\` or \`to\`: ${names.join("; ")}.` : "No other worktree of this project is open right now.";
32695
+ const projects = wt ? this.otherProjects(wt).map((r) => r.name) : [];
32696
+ const elsewhere = projects.length ? ` Other projects open in Toyon you can name in \`project\`: ${projects.join(", ")}.` : "";
32697
+ return {
32698
+ name: SPLIT_TOOL,
32699
+ description: `Hand Toyon the pieces of the work you are planning, or one piece you came across: each goes on this worktree, a new worktree of this project or of another open one, or an open worktree already on that part; the person decides on the list beside your plan. Nothing starts. ${open}${elsewhere}`,
32700
+ inputSchema: {
32701
+ type: "object",
32702
+ properties: {
32703
+ pieces: {
32704
+ type: "array",
32705
+ minItems: 1,
32706
+ maxItems: PIECES_MAX,
32707
+ items: {
32708
+ type: "object",
32709
+ properties: {
32710
+ title: {
32711
+ type: "string",
32712
+ description: `A name for the piece, the way a teammate would say it: ${NAME_SHAPE}. It titles the worktree.`
32713
+ },
32714
+ plan: {
32715
+ type: "string",
32716
+ description: "The plan its agent builds from, in markdown that stands on its own: what is needed and why, the paths that depend on it, and what done looks like."
32717
+ },
32718
+ attachments: {
32719
+ type: "array",
32720
+ items: { type: "string" },
32721
+ description: 'The attachments from this chat the piece needs, by their labels as captioned: "Image 2", "Paste 1".'
32722
+ },
32723
+ where: {
32724
+ type: "string",
32725
+ enum: ["here", "worktree", "send"],
32726
+ description: "here: you do it in this worktree. worktree: a new worktree, of this project or of the one `project` names. send: an open worktree already working on that part."
32727
+ },
32728
+ after: {
32729
+ 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.'
32731
+ },
32732
+ to: { type: "string", description: "send only: the open worktree it goes to, by branch or title." },
32733
+ project: {
32734
+ type: "string",
32735
+ description: "Another project open in Toyon, by its name as listed or its path: the worktree is made there, or `to` is one of its worktrees. Leave out for this project."
32736
+ }
32737
+ },
32738
+ required: ["title", "plan", "where"],
32739
+ additionalProperties: false
32740
+ }
32741
+ }
32742
+ },
32743
+ required: ["pieces"],
32744
+ additionalProperties: false
32745
+ }
32746
+ };
32747
+ }
32748
+ async call(worktreeId, args) {
32749
+ const shape = `split takes { pieces: [{ title: string, plan: string, where: "here" | "worktree" | "send", attachments?: string[], after?: string, to?: string, project?: string }] }, 1 to ${PIECES_MAX} pieces.`;
32750
+ const a = args && typeof args === "object" && !Array.isArray(args) ? args : null;
32751
+ const list = a && Array.isArray(a.pieces) ? a.pieces : null;
32752
+ if (!list || list.length === 0)
32753
+ return refused(shape);
32754
+ const pieces = [];
32755
+ for (const raw of list) {
32756
+ const p = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
32757
+ const shaped = p && typeof p.title === "string" && typeof p.plan === "string" && (p.where === "here" || p.where === "worktree" || p.where === "send") && (p.attachments === undefined || Array.isArray(p.attachments) && p.attachments.every((l) => typeof l === "string")) && (p.after === undefined || typeof p.after === "string") && (p.to === undefined || typeof p.to === "string") && (p.project === undefined || typeof p.project === "string");
32758
+ if (!shaped)
32759
+ return refused(shape);
32760
+ pieces.push({
32761
+ title: p.title,
32762
+ plan: p.plan,
32763
+ where: p.where,
32764
+ ...p.attachments ? { attachments: p.attachments } : {},
32765
+ ...p.after !== undefined ? { after: p.after } : {},
32766
+ ...p.to !== undefined ? { to: p.to } : {},
32767
+ ...p.project !== undefined ? { project: p.project } : {}
32768
+ });
32769
+ }
32770
+ return this.propose(worktreeId, pieces, "agent");
32771
+ }
32772
+ async propose(worktreeId, raw, by = "agent", read) {
32773
+ const wt = this.d.state.worktree(worktreeId);
32774
+ if (!wt)
32775
+ throw new UserError("that chat is gone");
32776
+ if (raw.length > PIECES_MAX)
32777
+ return refused(`At most ${PIECES_MAX} pieces in one list; fold the rest into \`here\`.`);
32778
+ const agent = this.d.runtime.ensureAgent(wt).agent;
32779
+ const entries = agent.transcript();
32780
+ const pieces = [];
32781
+ for (const [i, p] of raw.entries()) {
32782
+ const title = sentenceCase(p.title.trim());
32783
+ const plan = p.plan.trim();
32784
+ if (!title)
32785
+ return refused(`Piece ${i + 1} has no title.`);
32786
+ const over = overNameShape(title);
32787
+ if (over)
32788
+ return refused(`Piece ${i + 1}'s title ${over}`);
32789
+ if (!plan)
32790
+ return refused(`Piece ${i + 1} ("${title}") has no plan; write the plan its agent builds from.`);
32791
+ if (plan.length > PLAN_MAX) {
32792
+ return refused(`Piece ${i + 1}'s plan is over ${PLAN_MAX} characters; cut it to what its agent needs.`);
32793
+ }
32794
+ if ((p.attachments?.length ?? 0) > ATTACH_MAX) {
32795
+ return refused(`Piece ${i + 1} names more than ${ATTACH_MAX} attachments.`);
32796
+ }
32797
+ if ((p.project?.length ?? 0) > PROJECT_MAX)
32798
+ return refused(`Piece ${i + 1}'s project name is too long.`);
32799
+ const attachments = [];
32800
+ for (const label of p.attachments ?? []) {
32801
+ const ref = attachmentNamed(entries, label);
32802
+ if (!ref)
32803
+ return refused(`This chat has no attachment "${label.trim()}"; name them as the messages caption them.`);
32804
+ attachments.push(ref);
32805
+ }
32806
+ const piece = { id: `p${i + 1}`, title, plan, attachments, fate: p.where };
32807
+ if (p.project !== undefined) {
32808
+ if (p.where === "here")
32809
+ return refused(`Piece ${i + 1} ("${title}") is \`here\` and names a project.`);
32810
+ const found = this.resolveProject(wt, p.project);
32811
+ if ("refused" in found)
32812
+ return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
32813
+ piece.project = { id: found.repo.id, name: found.repo.name };
32814
+ }
32815
+ if (p.where !== "worktree" && p.after !== undefined) {
32816
+ return refused(`Piece ${i + 1} ("${title}") has \`after\`, which only a \`worktree\` piece takes.`);
32817
+ }
32818
+ if (p.where !== "send" && p.to !== undefined) {
32819
+ return refused(`Piece ${i + 1} ("${title}") has \`to\`, which only a \`send\` piece takes.`);
32820
+ }
32821
+ if (p.where === "worktree" && p.after !== undefined) {
32822
+ const q = p.after.trim();
32823
+ if (!q)
32824
+ 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);
32826
+ if ("refused" in after)
32827
+ return refused(`Piece ${i + 1} ("${title}"): ${after.refused}`);
32828
+ piece.after = after.after;
32829
+ }
32830
+ if (p.where === "send") {
32831
+ const q = p.to?.trim() ?? "";
32832
+ if (!q)
32833
+ return refused(`Piece ${i + 1} ("${title}") is \`send\` and names no worktree in \`to\`.`);
32834
+ const found = this.named(this.siblings(wt, piece.project?.id).filter((w) => !w.landed), q, piece.project?.name);
32835
+ if ("refused" in found)
32836
+ return refused(`Piece ${i + 1} ("${title}"): ${found.refused}`);
32837
+ piece.to = found.worktree;
32838
+ }
32839
+ pieces.push(piece);
32840
+ }
32841
+ const waiting = heldUnshown(entries);
32842
+ const replaced = openPieces(entries).filter((o) => by === "agent" && o.id === waiting?.id && o.by !== "toyon");
32843
+ if (openPieces(entries).some((o) => !replaced.includes(o))) {
32844
+ return refused("A pieces list is already waiting for the person on this chat; finish your turn.");
32845
+ }
32846
+ if (by === "agent" && proposedThisTurn(entries) >= PER_TURN_MAX) {
32847
+ return refused(`${PER_TURN_MAX} proposals were made this turn already; finish your turn and let the person answer.`);
32848
+ }
32849
+ for (const o of replaced) {
32850
+ agent.note({ type: "pieces-declined", id: o.id, reason: "handed over again", ts: Date.now() });
32851
+ }
32852
+ const held = by !== "person" && wt.mode === "plan";
32853
+ const plan = held ? null : writePlanDocNow(wt.path, listDoc(pieces));
32854
+ agent.note({
32855
+ type: "pieces-proposed",
32856
+ id: randomUUID2(),
32857
+ pieces,
32858
+ mode: wt.mode ?? DEFAULT_PERMISSION_MODE,
32859
+ ...held ? { held: true } : {},
32860
+ ...plan ? { plan } : {},
32861
+ ...by === "toyon" ? { by: "toyon" } : {},
32862
+ ...by === "toyon" && read ? { read } : {},
32863
+ ts: Date.now()
32864
+ });
32865
+ return { text: held ? HELD : SHOWN };
32866
+ }
32867
+ resolveAfter(wt, raw, at, q) {
32868
+ const bare = q.startsWith("@") ? q.slice(1) : q;
32869
+ 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]);
32871
+ if (onList.length === 1)
32872
+ return { after: { piece: `p${onList[0].i + 1}` } };
32873
+ if (onList.length > 1)
32874
+ 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.` };
32877
+ }
32878
+ if (bare === "this" || bare === wt.branch)
32879
+ return { after: { id: wt.id, branch: wt.branch, title: wt.title } };
32880
+ const found = this.named(this.siblings(wt), bare);
32881
+ if ("refused" in found)
32882
+ return found;
32883
+ return { after: found.worktree };
32884
+ }
32885
+ named(among, q, where = "this project") {
32886
+ const bare = q.startsWith("@") ? q.slice(1) : q;
32887
+ const list = among.map((w) => `${w.branch}, "${w.title}"`).join("; ");
32888
+ if (among.length === 0)
32889
+ return { refused: `no other worktree of ${where} is open.` };
32890
+ const found = matchBy(among, bare, [(w) => w.branch, (w) => w.title]);
32891
+ if (found.length === 1) {
32892
+ const w = found[0];
32893
+ return { worktree: { id: w.id, branch: w.branch, title: w.title } };
32894
+ }
32895
+ if (found.length > 1) {
32896
+ return {
32897
+ refused: `"${q}" names more than one worktree: ${found.map((w) => w.branch).join(", ")}. Say which by its branch.`
32898
+ };
32899
+ }
32900
+ return { refused: `no open worktree of ${where} is called "${q}". Open: ${list}.` };
32901
+ }
32902
+ fate(worktreeId, id, pieceId, fate) {
32903
+ const wt = this.d.state.worktree(worktreeId);
32904
+ if (!wt)
32905
+ throw new UserError("that chat is gone");
32906
+ const agent = this.d.runtime.ensureAgent(wt).agent;
32907
+ const proposed = openPieces(agent.transcript()).find((p) => p.id === id);
32908
+ if (!proposed)
32909
+ throw new UserError("That list is not open in this chat");
32910
+ const piece = proposed.pieces.find((p) => p.id === pieceId);
32911
+ if (!piece)
32912
+ throw new UserError("That piece is not on the list");
32913
+ if (fate === "send" && !piece.to)
32914
+ throw new UserError("That piece has no worktree to send to");
32915
+ if (piece.fate === "send" && (fate === "here" || fate === "worktree")) {
32916
+ throw new UserError("A piece for an open worktree is sent there or skipped");
32917
+ }
32918
+ agent.note({ type: "pieces-fate", id, pieceId, fate, ts: Date.now() });
32919
+ }
32920
+ async answer(worktreeId, id, go, note) {
32921
+ if (!this.d.state.worktree(worktreeId))
32922
+ throw new UserError("that chat is gone");
32923
+ if (go)
32924
+ await this.act(worktreeId, id, note?.trim() || undefined);
32925
+ else
32926
+ await this.decline(worktreeId, id);
32927
+ }
32928
+ async decline(worktreeId, id, reason) {
32929
+ const wt = this.d.state.worktree(worktreeId);
32930
+ if (!wt)
32931
+ return;
32932
+ const agent = this.d.runtime.ensureAgent(wt).agent;
32933
+ if (!openPieces(agent.transcript()).some((p) => p.id === id)) {
32934
+ return log.debug(worktreeId, `decline for a pieces list that already closed (${id})`);
32935
+ }
32936
+ agent.note({
32937
+ type: "pieces-declined",
32938
+ id,
32939
+ ...reason ? { outcome: "failed", reason } : {},
32940
+ ts: Date.now()
32941
+ });
32942
+ }
32943
+ async act(worktreeId, id, note, tell = false) {
32944
+ const wt = this.d.state.worktree(worktreeId);
32945
+ if (!wt)
32946
+ return;
32947
+ const agent = this.d.runtime.ensureAgent(wt).agent;
32948
+ const entries = agent.transcript();
32949
+ const proposed = openPieces(entries).find((p) => p.id === id);
32950
+ if (!proposed)
32951
+ return log.debug(worktreeId, `go for a pieces list that already closed (${id})`);
32952
+ agent.note({ type: "pieces-go", id, ts: Date.now() });
32953
+ try {
32954
+ const repo = this.d.state.repo(wt.repoId);
32955
+ if (!repo)
32956
+ throw new UserError(`${wt.title}'s project is no longer open in Toyon`);
32957
+ const origin = this.originOf(wt, repo.name, listFile(entries, id));
32958
+ const outcomes = [];
32959
+ const became = new Map;
32960
+ for (const piece of proposed.pieces) {
32961
+ const fate = effectiveFate(entries, proposed, piece);
32962
+ try {
32963
+ outcomes.push(await this.actOn(wt, origin, proposed, piece, fate, note, became));
32964
+ } catch (e) {
32965
+ const error = e instanceof Error ? e.message : String(e);
32966
+ log.warn(worktreeId, `piece "${piece.title}" (${fate}) failed`, e);
32967
+ became.set(piece.id, { fate: "skip" });
32968
+ outcomes.push({ pieceId: piece.id, fate, error });
32969
+ }
32970
+ }
32971
+ agent.note({ type: "pieces", id, outcomes, ts: Date.now() });
32972
+ if (tell && outcomes.some((o) => o.fate !== "here")) {
32973
+ for (let i = 0;i < 30 && openPlanCard(agent.transcript()); i++)
32974
+ await Bun.sleep(100);
32975
+ await Bun.sleep(100);
32976
+ agent.send(piecesActedPrompt(proposed.pieces, outcomes), {
32977
+ asked: { kind: "pieces", why: "what the split started, so the agent builds only its own piece" },
32978
+ keepCards: true
32979
+ });
32980
+ }
32981
+ } catch (e) {
32982
+ const reason = e instanceof Error ? e.message : String(e);
32983
+ agent.note({ type: "pieces-declined", id, outcome: "failed", reason, ts: Date.now() });
32984
+ throw e;
32985
+ }
32986
+ }
32987
+ async read(worktreeId, markdown, planPath) {
32988
+ const wt = this.d.state.worktree(worktreeId);
32989
+ if (!wt)
32990
+ return;
32991
+ const agent = this.d.runtime.ensureAgent(wt).agent;
32992
+ if (openPieces(agent.transcript()).length > 0)
32993
+ return;
32994
+ const found = await this.d.reader(wt, markdown);
32995
+ if (!found || found.length < 2)
32996
+ return;
32997
+ const card = openPlanCard(agent.transcript());
32998
+ if (!card || card.plan !== planPath || openPieces(agent.transcript()).length > 0)
32999
+ return;
33000
+ await this.proposeRead(wt, markdown, found);
33001
+ }
33002
+ async proposeRead(wt, markdown, found) {
33003
+ const siblings = this.siblings(wt);
33004
+ const known = (q) => q === "this" || q === wt.branch || !("refused" in this.named(siblings, q.startsWith("@") ? q.slice(1) : q));
33005
+ const sections = new Map;
33006
+ for (const p of found)
33007
+ sections.set(p.section, (sections.get(p.section) ?? 0) + 1);
33008
+ const raw = found.map((p) => {
33009
+ const own = sections.get(p.section) === 1 ? sectionText(markdown, p.section) : null;
33010
+ const plan = own ?? p.plan ?? sectionText(markdown, p.section) ?? `${p.title}
33011
+
33012
+ ${p.section}`;
33013
+ const after = p.where === "worktree" && p.after && known(p.after) ? p.after : undefined;
33014
+ return { title: p.title, plan, where: p.where, ...after ? { after } : {} };
33015
+ });
33016
+ const r = await this.propose(wt.id, raw, "toyon", hashText(markdown));
33017
+ if (r.isError) {
33018
+ log.warn(wt.id, `the list read off the plan was refused: ${r.text}`);
33019
+ return null;
33020
+ }
33021
+ const agent = this.d.runtime.agentFor(wt.id);
33022
+ return agent ? openPieces(agent.transcript()).find((p) => p.by === "toyon") ?? null : null;
33023
+ }
33024
+ async stale(worktreeId) {
33025
+ const wt = this.d.state.worktree(worktreeId);
33026
+ if (wt?.mode !== "plan")
33027
+ return;
33028
+ const agent = this.d.runtime.agentFor(worktreeId);
33029
+ if (!agent)
33030
+ return;
33031
+ const entries = agent.transcript();
33032
+ const open = heldUnshown(entries);
33033
+ if (!open)
33034
+ return;
33035
+ if (entries.some(({ event }) => event.type === "pieces-stale" && event.id === open.id))
33036
+ return;
33037
+ const card = openPlanCard(entries);
33038
+ if (!card?.plan || !card.detail)
33039
+ return;
33040
+ let markdown;
33041
+ try {
33042
+ markdown = readFileSync20(join28(wt.path, card.plan), "utf8");
33043
+ } catch (e) {
33044
+ log.warn(worktreeId, `could not read ${card.plan} to see whether the plan moved`, e);
33045
+ return;
33046
+ }
33047
+ const moved = open.read ? hashText(markdown) !== open.read : planEdited(wt.path, card.plan, card.detail);
33048
+ if (!moved)
33049
+ return;
33050
+ agent.note({ type: "pieces-stale", id: open.id, ts: Date.now() });
33051
+ if (open.by !== "toyon")
33052
+ return;
33053
+ const found = await this.d.reader(wt, markdown);
33054
+ if (!found || found.length < 2)
33055
+ return;
33056
+ if (openPlanCard(agent.transcript())?.id !== card.id)
33057
+ return;
33058
+ const pressed = new Map;
33059
+ for (const { event } of agent.transcript()) {
33060
+ if (event.type === "pieces-fate" && event.id === open.id) {
33061
+ const title = open.pieces.find((p) => p.id === event.pieceId)?.title;
33062
+ if (title)
33063
+ pressed.set(title.toLowerCase(), event.fate);
33064
+ }
33065
+ }
33066
+ agent.note({ type: "pieces-declined", id: open.id, reason: "the plan changed; read again", ts: Date.now() });
33067
+ const fresh = await this.proposeRead(wt, markdown, found);
33068
+ if (!fresh)
33069
+ return;
33070
+ for (const piece of fresh.pieces) {
33071
+ const fate = pressed.get(piece.title.toLowerCase());
33072
+ if (fate && fate !== piece.fate && !(fate === "send" && !piece.to)) {
33073
+ agent.note({ type: "pieces-fate", id: fresh.id, pieceId: piece.id, fate, ts: Date.now() });
33074
+ }
33075
+ }
33076
+ }
33077
+ async ask(worktreeId, repoId, text) {
33078
+ const wt = this.d.state.requireWorktree(worktreeId);
33079
+ const repo = this.d.state.repo(repoId);
33080
+ if (!repo)
33081
+ throw new UserError("no such project");
33082
+ const here = repo.id === wt.repoId;
33083
+ const agent = this.d.runtime.agentFor(worktreeId);
33084
+ if (!agent)
33085
+ throw new UserError("worktree still starting; try again in a moment");
33086
+ if (agent.mcpTools === null)
33087
+ await agent.warm();
33088
+ if (agent.mcpTools === true) {
33089
+ agent.send(pieceAskPrompt(text, here ? undefined : repo), {
33090
+ asked: { kind: "pieces", why: here ? "a new worktree here" : `a new worktree in ${repo.name}` }
33091
+ });
33092
+ return;
33093
+ }
33094
+ const own = text?.trim();
33095
+ if (!own)
33096
+ throw new UserError(`Write what ${here ? "the new worktree" : repo.name} should do first`);
33097
+ const r = await this.propose(worktreeId, [{ title: titleFrom(own), plan: own, where: "worktree", ...here ? {} : { project: repo.path } }], "person");
33098
+ if (r.isError)
33099
+ throw new UserError(r.text);
33100
+ }
33101
+ originOf(wt, project, file) {
33102
+ return { path: wt.path, project, branch: wt.branch, ...file ? { plan: file } : {} };
33103
+ }
33104
+ async actOn(wt, origin, proposed, piece, fate, note, became) {
33105
+ const crossed = piece.project ? { repoName: piece.project.name } : {};
33106
+ if (fate === "here" || fate === "skip") {
33107
+ became.set(piece.id, { fate });
33108
+ return { pieceId: piece.id, fate };
33109
+ }
33110
+ if (fate === "send") {
33111
+ const target = piece.to ? this.d.state.worktree(piece.to.id) : undefined;
33112
+ const repoId = piece.project?.id ?? wt.repoId;
33113
+ if (target?.kind !== "worktree" || target.repoId !== repoId) {
33114
+ throw new UserError(`"${piece.to?.title ?? "the worktree"}" is no longer open`);
33115
+ }
33116
+ const attachments = await this.reattachAll(wt.id, piece.attachments);
33117
+ this.d.runtime.ensureAgent(target).agent.send(pieceSendPrompt(piece.plan, note, origin), {
33118
+ keepCards: true,
33119
+ ...attachments.length ? { attachments } : {},
33120
+ handed: {
33121
+ via: "message",
33122
+ body: piece.plan,
33123
+ origin: handedOrigin(wt, piece.project ? origin.project : undefined),
33124
+ ...note ? { note } : {}
33125
+ }
33126
+ });
33127
+ became.set(piece.id, { fate, worktree: { id: target.id, branch: target.branch, title: target.title } });
33128
+ return { pieceId: piece.id, fate, worktreeId: target.id, title: target.title, ...crossed };
33129
+ }
33130
+ const waits = this.waitsFor(wt, piece, became);
33131
+ if (waits) {
33132
+ const queued = {
33133
+ proposalId: proposed.id,
33134
+ pieceId: piece.id,
33135
+ title: piece.title,
33136
+ plan: piece.plan,
33137
+ after: waits,
33138
+ ...piece.project ? { project: piece.project } : {},
33139
+ attachments: piece.attachments,
33140
+ ...note ? { note } : {},
33141
+ queuedAt: Date.now()
33142
+ };
33143
+ const live = this.d.state.requireWorktree(wt.id);
33144
+ live.queued = [...live.queued ?? [], queued];
33145
+ this.d.state.save();
33146
+ this.d.hub.emit("worktreesChanged");
33147
+ became.set(piece.id, { fate, queued: true });
33148
+ const after = "piece" in waits ? proposed.pieces.find((p) => p.id === waits.piece)?.title ?? waits.piece : waits.branch;
33149
+ return { pieceId: piece.id, fate, after, ...crossed };
33150
+ }
33151
+ const made = await this.make(wt, origin, piece, note);
33152
+ became.set(piece.id, { fate, worktree: { id: made.id, branch: made.branch, title: made.title } });
33153
+ return { pieceId: piece.id, fate, worktreeId: made.id, title: made.title, ...crossed };
33154
+ }
33155
+ waitsFor(wt, piece, became) {
33156
+ const after = piece.after;
33157
+ if (!after)
33158
+ return null;
33159
+ if (!("piece" in after)) {
33160
+ const waits = this.d.state.worktree(after.id);
33161
+ return waits && waits.kind === "worktree" && !waits.landed ? after : null;
33162
+ }
33163
+ const on = became.get(after.piece);
33164
+ if (!on)
33165
+ return null;
33166
+ if (on.queued)
33167
+ return { piece: after.piece };
33168
+ if (on.worktree)
33169
+ return on.worktree;
33170
+ if (on.fate === "here")
33171
+ return { id: wt.id, branch: wt.branch, title: wt.title };
33172
+ return null;
33173
+ }
33174
+ async make(origin, at, piece, note, after) {
33175
+ const repoId = piece.project?.id ?? origin.repoId;
33176
+ if (piece.project && !this.d.state.repo(repoId)) {
33177
+ throw new UserError(`${piece.project.name} is no longer open in Toyon`);
33178
+ }
33179
+ const attachments = await this.reattachAll(origin.id, piece.attachments);
33180
+ const mode = origin.mode === "plan" ? "auto" : origin.mode;
33181
+ const via = piece.project ? "project" : "piece";
33182
+ return this.d.create(repoId, piece.title, {
33183
+ title: piece.title,
33184
+ ...mode ? { mode } : {},
33185
+ ...attachments.length ? { attachments } : {},
33186
+ from: {
33187
+ kind: "worktree",
33188
+ ref: origin.branch,
33189
+ origin: { id: origin.id, repoId: origin.repoId },
33190
+ ...at.plan ? { plan: at.plan } : {},
33191
+ ...after ? { after } : {}
33192
+ },
33193
+ plan: {
33194
+ markdown: pieceDoc(piece),
33195
+ handed: {
33196
+ via: "plan",
33197
+ body: piece.plan,
33198
+ origin: handedOrigin(origin, piece.project ? at.project : undefined),
33199
+ ...note ? { note } : {}
33200
+ },
33201
+ prompt: (path) => path ? handedPrompt(path, note, at, via) : `${pieceDoc(piece)}
33202
+ ${handedPrompt(null, note, at, via)}`
33203
+ },
33204
+ cold: true
33205
+ });
33206
+ }
33207
+ async reattachAll(worktreeId, refs) {
33208
+ const out = [];
33209
+ for (const ref of refs) {
33210
+ const again = await this.d.attachments.reattach(worktreeId, ref);
33211
+ if (again)
33212
+ out.push(again);
33213
+ else
33214
+ log.warn(worktreeId, `a piece's attachment is gone from the store (${ref.kind} ${ref.n})`);
33215
+ }
33216
+ return out;
33217
+ }
33218
+ async landed(landedId) {
33219
+ this.tellOrigin(landedId);
33220
+ const madeNow = new Set;
33221
+ for (const origin of this.d.state.worktrees) {
33222
+ const due = origin.queued?.filter((q) => !("piece" in q.after) && q.after.id === landedId) ?? [];
33223
+ if (due.length === 0)
33224
+ continue;
33225
+ const repo = this.d.state.repo(origin.repoId);
33226
+ origin.queued = origin.queued?.filter((q) => !due.includes(q));
33227
+ if (origin.queued?.length === 0)
33228
+ delete origin.queued;
33229
+ this.d.state.save();
33230
+ this.d.hub.emit("worktreesChanged");
33231
+ const agent = this.d.runtime.ensureAgent(origin).agent;
33232
+ for (const q of due) {
33233
+ try {
33234
+ if (!repo)
33235
+ throw new UserError("the project is no longer open in Toyon");
33236
+ const at = this.originOf(origin, repo.name, listFile(agent.transcript(), q.proposalId));
33237
+ const made = await this.make(origin, at, q, q.note, "piece" in q.after ? undefined : q.after);
33238
+ agent.note({
33239
+ type: "piece-made",
33240
+ id: q.proposalId,
33241
+ pieceId: q.pieceId,
33242
+ worktreeId: made.id,
33243
+ title: made.title,
33244
+ ts: Date.now()
33245
+ });
33246
+ madeNow.add(made.id);
33247
+ this.repoint(origin, q, { id: made.id, branch: made.branch, title: made.title });
33248
+ } catch (e) {
33249
+ const reason = e instanceof Error ? e.message : String(e);
33250
+ log.warn(origin.id, `queued piece "${q.title}" could not be made`, e);
33251
+ agent.note({ type: "piece-dropped", id: q.proposalId, pieceId: q.pieceId, reason, ts: Date.now() });
33252
+ this.dropDependents(origin, q);
33253
+ }
33254
+ }
33255
+ }
33256
+ this.tellWaiters(landedId, madeNow);
33257
+ }
33258
+ repoint(origin, made, worktree) {
33259
+ const live = this.d.state.worktree(origin.id);
33260
+ if (!live?.queued)
33261
+ return;
33262
+ let changed = false;
33263
+ for (const q of live.queued) {
33264
+ if (q.proposalId === made.proposalId && "piece" in q.after && q.after.piece === made.pieceId) {
33265
+ q.after = worktree;
33266
+ changed = true;
33267
+ }
33268
+ }
33269
+ if (changed) {
33270
+ this.d.state.save();
33271
+ this.d.hub.emit("worktreesChanged");
33272
+ }
33273
+ }
33274
+ dropDependents(origin, dropped) {
33275
+ const live = this.d.state.worktree(origin.id);
33276
+ if (!live?.queued)
33277
+ return;
33278
+ const gone = live.queued.filter((q) => q.proposalId === dropped.proposalId && ("piece" in q.after) && q.after.piece === dropped.pieceId);
33279
+ if (gone.length === 0)
33280
+ return;
33281
+ live.queued = live.queued.filter((q) => !gone.includes(q));
33282
+ if (live.queued.length === 0)
33283
+ delete live.queued;
33284
+ this.d.state.save();
33285
+ this.d.hub.emit("worktreesChanged");
33286
+ const agent = this.d.runtime.ensureAgent(live).agent;
33287
+ for (const q of gone) {
33288
+ agent.note({
33289
+ type: "piece-dropped",
33290
+ id: q.proposalId,
33291
+ pieceId: q.pieceId,
33292
+ reason: `the piece it waited for, "${dropped.title}", was dropped`,
33293
+ ts: Date.now()
33294
+ });
33295
+ this.dropDependents(live, q);
33296
+ }
33297
+ }
33298
+ tellOrigin(landedId) {
33299
+ const wt = this.d.state.worktree(landedId);
33300
+ if (wt?.from?.kind !== "worktree")
33301
+ return;
33302
+ const origin = this.d.state.worktree(wt.from.origin.id);
33303
+ if (origin?.kind !== "worktree")
33304
+ return;
33305
+ const repo = this.d.state.repo(wt.repoId);
33306
+ if (!repo)
33307
+ return;
33308
+ this.d.runtime.ensureAgent(origin).agent.note({
33309
+ type: "piece-landed",
33310
+ worktreeId: wt.id,
33311
+ repoName: repo.name,
33312
+ title: wt.title,
33313
+ ...wt.pr?.url ? { url: wt.pr.url } : {},
33314
+ ...wt.repoId === origin.repoId ? { same: true } : {},
33315
+ ts: Date.now()
33316
+ });
33317
+ }
33318
+ tellWaiters(landedId, madeNow) {
33319
+ const landed = this.d.state.worktree(landedId);
33320
+ if (!landed)
33321
+ return;
33322
+ const repo = this.d.state.repo(landed.repoId);
33323
+ if (!repo)
33324
+ return;
33325
+ for (const w of this.d.state.worktrees) {
33326
+ if (w.kind !== "worktree" || w.landed || w.id === landedId || madeNow.has(w.id))
33327
+ continue;
33328
+ if (w.from?.kind !== "worktree" || w.from.after?.id !== landedId)
33329
+ continue;
33330
+ this.d.runtime.ensureAgent(w).agent.note({
33331
+ type: "piece-landed",
33332
+ worktreeId: landed.id,
33333
+ repoName: repo.name,
33334
+ title: landed.title,
33335
+ ...landed.pr?.url ? { url: landed.pr.url } : {},
33336
+ ...w.repoId === landed.repoId ? { same: true } : {},
33337
+ again: true,
33338
+ ts: Date.now()
33339
+ });
33340
+ }
33341
+ }
33342
+ sweep() {
33343
+ let changed = false;
33344
+ for (const origin of this.d.state.worktrees) {
33345
+ if (!origin.queued?.length)
33346
+ continue;
33347
+ const live = (id) => this.d.state.worktree(id)?.kind === "worktree";
33348
+ const gone = origin.queued.filter((q) => !("piece" in q.after) && !live(q.after.id));
33349
+ if (gone.length === 0)
33350
+ continue;
33351
+ origin.queued = origin.queued.filter((q) => !gone.includes(q));
33352
+ if (origin.queued.length === 0)
33353
+ delete origin.queued;
33354
+ changed = true;
33355
+ const agent = this.d.runtime.ensureAgent(origin).agent;
33356
+ for (const q of gone) {
33357
+ const title = "piece" in q.after ? q.after.piece : q.after.title;
33358
+ agent.note({
33359
+ type: "piece-dropped",
33360
+ id: q.proposalId,
33361
+ pieceId: q.pieceId,
33362
+ reason: `the worktree it waited for, "${title}", is gone`,
33363
+ ts: Date.now()
33364
+ });
33365
+ this.dropDependents(origin, q);
33366
+ }
33367
+ }
33368
+ if (changed) {
33369
+ this.d.state.save();
33370
+ this.d.hub.emit("worktreesChanged");
33371
+ }
33372
+ }
33373
+ }
33374
+ function matchRepos(repos, q) {
33375
+ return matchBy(repos, q, [(r) => r.path, (r) => r.name]);
33376
+ }
33377
+ function attachmentNamed(entries, label) {
33378
+ const m = LABEL.exec(label.trim());
33379
+ if (!m)
33380
+ return null;
33381
+ const kind = KIND_OF[m[1].toLowerCase()];
33382
+ const n = Number(m[2]);
33383
+ for (const { event } of entries) {
33384
+ if (event.type !== "user-message" && event.type !== "handed")
33385
+ continue;
33386
+ const ref = event.attachments?.find((a) => a.kind === kind && a.n === n);
33387
+ if (ref)
33388
+ return ref;
33389
+ }
33390
+ return null;
33391
+ }
33392
+
32398
33393
  // ../daemon/src/worktrees/prs.ts
32399
33394
  var POLL_MS3 = 3 * 60000;
32400
33395
 
@@ -32503,7 +33498,7 @@ async function mergedBranches(repoPath, defaultBr) {
32503
33498
  }
32504
33499
 
32505
33500
  // ../daemon/src/worktrees/discover.ts
32506
- import { createHash as createHash5 } from "crypto";
33501
+ import { createHash as createHash6 } from "crypto";
32507
33502
  import { basename as basename10 } from "path";
32508
33503
 
32509
33504
  // ../daemon/src/git/worktrees.ts
@@ -32572,7 +33567,7 @@ async function listWorktrees(repoPath) {
32572
33567
 
32573
33568
  // ../daemon/src/worktrees/discover.ts
32574
33569
  function discoveredId(path) {
32575
- return `disc-${createHash5("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
33570
+ return `disc-${createHash6("sha1").update(canonical(path)).digest("hex").slice(0, 12)}`;
32576
33571
  }
32577
33572
  function knownPaths(known) {
32578
33573
  const paths = new Set;
@@ -32755,9 +33750,9 @@ function rankRefs({ refs, merged, prs, query, defaultBranch, rowIdForPath }) {
32755
33750
  }
32756
33751
 
32757
33752
  // ../daemon/src/worktrees/service.ts
32758
- import { cpSync, existsSync as existsSync36, readFileSync as readFileSync21, rmSync as rmSync8 } from "fs";
33753
+ import { cpSync, existsSync as existsSync36, readFileSync as readFileSync22, rmSync as rmSync8 } from "fs";
32759
33754
  import { rm as rm5 } from "fs/promises";
32760
- import { dirname as dirname13, join as join32 } from "path";
33755
+ import { dirname as dirname13, join as join33 } from "path";
32761
33756
 
32762
33757
  // ../daemon/src/git/archive.ts
32763
33758
  import { rmSync as rmSync5 } from "fs";
@@ -33366,7 +34361,7 @@ function compareUrl(remoteUrl, base, branch) {
33366
34361
 
33367
34362
  // ../daemon/src/git/turn.ts
33368
34363
  import { copyFileSync, existsSync as existsSync33, rmSync as rmSync6 } from "fs";
33369
- import { join as join28 } from "path";
34364
+ import { join as join29 } from "path";
33370
34365
  var turnRef = (worktreeId, which) => `refs/toyon/turns/${worktreeId}/${which}`;
33371
34366
  var SNAPSHOT_SUBJECT2 = "toyon: tree at a send";
33372
34367
  var queue = new Map;
@@ -33384,7 +34379,7 @@ async function treeNow(wtPath, head, dirty) {
33384
34379
  const dir = await git(wtPath, "rev-parse", "--absolute-git-dir");
33385
34380
  if (!dir.ok || !dir.out)
33386
34381
  return null;
33387
- index = join28(dir.out, "toyon-turn-index");
34382
+ index = join29(dir.out, "toyon-turn-index");
33388
34383
  }
33389
34384
  const env = { GIT_INDEX_FILE: index };
33390
34385
  const writeTree = async () => {
@@ -33407,7 +34402,7 @@ async function treeNow(wtPath, head, dirty) {
33407
34402
  }
33408
34403
  synced.delete(wtPath);
33409
34404
  if (!existsSync33(index)) {
33410
- const own = join28(index, "..", "index");
34405
+ const own = join29(index, "..", "index");
33411
34406
  if (existsSync33(own))
33412
34407
  copyFileSync(own, index);
33413
34408
  else if (!(await run(GIT, ["read-tree", "HEAD"], wtPath, env)).ok)
@@ -33489,8 +34484,8 @@ async function dropTurnRefs(repoPath, worktreeId) {
33489
34484
  }
33490
34485
 
33491
34486
  // ../daemon/src/worktrees/archive.ts
33492
- import { existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync20, renameSync as renameSync7, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
33493
- import { basename as basename11, dirname as dirname12, join as join29 } from "path";
34487
+ import { existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync9, readFileSync as readFileSync21, renameSync as renameSync7, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
34488
+ import { basename as basename11, dirname as dirname12, join as join30 } from "path";
33494
34489
  class WorktreeArchive {
33495
34490
  dir;
33496
34491
  records = new Map;
@@ -33502,7 +34497,7 @@ class WorktreeArchive {
33502
34497
  if (!existsSync34(file))
33503
34498
  continue;
33504
34499
  try {
33505
- this.records.set(id, JSON.parse(readFileSync20(file, "utf8")));
34500
+ this.records.set(id, JSON.parse(readFileSync21(file, "utf8")));
33506
34501
  } catch (e) {
33507
34502
  log.warn("archive", `skipped an unreadable record: ${file}`, e);
33508
34503
  }
@@ -33557,24 +34552,24 @@ class WorktreeArchive {
33557
34552
  planFile(id, rel) {
33558
34553
  if (!this.records.has(id) || !isPlanPath(rel))
33559
34554
  return null;
33560
- const file = join29(this.filesOf(id).plans, basename11(rel));
34555
+ const file = join30(this.filesOf(id).plans, basename11(rel));
33561
34556
  if (!existsSync34(file))
33562
34557
  return null;
33563
34558
  try {
33564
- return readFileSync20(file, "utf8");
34559
+ return readFileSync21(file, "utf8");
33565
34560
  } catch (e) {
33566
34561
  log.warn(id, `could not read its archived plan ${rel}`, e);
33567
34562
  return null;
33568
34563
  }
33569
34564
  }
33570
34565
  filesOf(id) {
33571
- const dir = join29(this.dir, id);
34566
+ const dir = join30(this.dir, id);
33572
34567
  return {
33573
34568
  dir,
33574
- record: join29(dir, "record.json"),
33575
- transcript: join29(dir, "transcript.jsonl"),
33576
- attachments: join29(dir, "attachments"),
33577
- plans: join29(dir, "plans")
34569
+ record: join30(dir, "record.json"),
34570
+ transcript: join30(dir, "transcript.jsonl"),
34571
+ attachments: join30(dir, "attachments"),
34572
+ plans: join30(dir, "plans")
33578
34573
  };
33579
34574
  }
33580
34575
  }
@@ -33601,7 +34596,7 @@ function summarize(r, repoId, transcript) {
33601
34596
  function lastUsage(transcript) {
33602
34597
  if (!existsSync34(transcript))
33603
34598
  return null;
33604
- const lines = readFileSync20(transcript, "utf8").split(`
34599
+ const lines = readFileSync21(transcript, "utf8").split(`
33605
34600
  `);
33606
34601
  for (let i = lines.length - 1;i >= 0; i--) {
33607
34602
  if (!lines[i]?.includes('"usage"'))
@@ -33617,14 +34612,16 @@ function lastUsage(transcript) {
33617
34612
  function firstPrompt(transcript) {
33618
34613
  if (!existsSync34(transcript))
33619
34614
  return;
33620
- for (const line of readFileSync20(transcript, "utf8").split(`
34615
+ for (const line of readFileSync21(transcript, "utf8").split(`
33621
34616
  `)) {
33622
- if (!line.includes('"user-message"'))
34617
+ if (!line.includes('"user-message"') && !line.includes('"handed"'))
33623
34618
  continue;
33624
34619
  try {
33625
34620
  const event = JSON.parse(line).event;
33626
34621
  if (event?.type === "user-message")
33627
34622
  return event.text.slice(0, 500);
34623
+ if (event?.type === "handed")
34624
+ return (event.body ?? event.text).slice(0, 500);
33628
34625
  } catch {}
33629
34626
  }
33630
34627
  return;
@@ -33713,7 +34710,7 @@ async function fillLandMarks(jobs, read, width = 8) {
33713
34710
 
33714
34711
  // ../daemon/src/worktrees/spare.ts
33715
34712
  import { existsSync as existsSync35 } from "fs";
33716
- import { basename as basename12, join as join30 } from "path";
34713
+ import { basename as basename12, join as join31 } from "path";
33717
34714
  class SparePool {
33718
34715
  d;
33719
34716
  inflight = new Map;
@@ -33781,11 +34778,11 @@ class SparePool {
33781
34778
  return this.record(repoId, repo).then((wt) => this.warmUp(wt, repo, {}));
33782
34779
  }
33783
34780
  async record(repoId, repo) {
33784
- const { id, dir } = freeSlot(join30(this.d.paths.worktreesDir, repo.name));
34781
+ const { id, dir } = freeSlot(join31(this.d.paths.worktreesDir, repo.name));
33785
34782
  const wt = {
33786
34783
  id,
33787
34784
  repoId,
33788
- path: join30(this.d.paths.worktreesDir, repo.name, dir),
34785
+ path: join31(this.d.paths.worktreesDir, repo.name, dir),
33789
34786
  branch: repo.defaultBranch,
33790
34787
  kind: "spare",
33791
34788
  phase: "reserved",
@@ -33975,7 +34972,7 @@ class SparePool {
33975
34972
  // ../daemon/src/worktrees/treeState.ts
33976
34973
  import { watch as watch2 } from "fs";
33977
34974
  import { stat as stat5 } from "fs/promises";
33978
- import { join as join31 } from "path";
34975
+ import { join as join32 } from "path";
33979
34976
  var EDIT_DELAY_MS = 150;
33980
34977
  var SHELL_QUIET_MS2 = 1500;
33981
34978
  var WATCH_DELAY_MS = 300;
@@ -34358,7 +35355,7 @@ class TreeState {
34358
35355
  xy: f.xy,
34359
35356
  ...entries.folded.has(f.path) ? { add: f.add, del: f.del } : {}
34360
35357
  }));
34361
- const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat5(join31(r.path, f.path)).then((s) => `${s.mtimeMs}:${s.size}`, () => "-")));
35358
+ const stats = await Promise.all(files.slice(0, SIGNATURE_STATS).map((f) => stat5(join32(r.path, f.path)).then((s) => `${s.mtimeMs}:${s.size}`, () => "-")));
34362
35359
  const t0 = performance.now();
34363
35360
  const counts = { ...ab, ...unpushed === null ? {} : { unpushed } };
34364
35361
  const signature = String(Bun.hash([
@@ -34692,7 +35689,7 @@ async function deleteTree(path) {
34692
35689
  }
34693
35690
  }
34694
35691
  function withoutAttachments(event) {
34695
- if (event.type !== "user-message")
35692
+ if (event.type !== "user-message" && event.type !== "handed")
34696
35693
  return event;
34697
35694
  const { attachments: _attachments, ...rest } = event;
34698
35695
  return rest;
@@ -34797,7 +35794,7 @@ class WorktreeService {
34797
35794
  const wt = d.state.worktree(worktreeId);
34798
35795
  const prompt = wt?.unnamed ? firstAskOf(d.runtime.agentFor(worktreeId)?.transcript() ?? []) : undefined;
34799
35796
  if (wt && prompt)
34800
- this.scheduleNaming(wt, prompt, wt.variant);
35797
+ this.scheduleNaming(wt, prompt);
34801
35798
  }
34802
35799
  if (turn.end !== "asking")
34803
35800
  this.syncAfterTurn(worktreeId);
@@ -34812,7 +35809,7 @@ class WorktreeService {
34812
35809
  });
34813
35810
  }
34814
35811
  async create(repoId, prompt, opts = {}) {
34815
- const { variant, attachments } = opts;
35812
+ const { attachments } = opts;
34816
35813
  const repo = this.d.state.requireRepo(repoId);
34817
35814
  const lead = opts.worktreeId ?? this.d.state.worktrees.find((w) => w.repoId === repoId && isMain(w))?.id;
34818
35815
  const context = (id) => {
@@ -34823,14 +35820,7 @@ class WorktreeService {
34823
35820
  const profile = this.checkProfile(repo, opts.profile);
34824
35821
  const task = taskText(prompt, attachments);
34825
35822
  const given = cleanTitle2(opts.title ?? "") || undefined;
34826
- const base = given ?? titleFrom(task);
34827
- const title = variant ? `${base} v${variant.index}` : base;
34828
- if (opts.carry && variant && variant.of > 1) {
34829
- throw new UserError(`only a single worktree can take the changes on ${repo.defaultBranch}`);
34830
- }
34831
- const agentPrompt = variant && variant.of >= 2 ? [prompt, variantLens(variant.index)].filter(Boolean).join(`
34832
-
34833
- `) : prompt;
35823
+ const title = given ?? titleFrom(task);
34834
35824
  const spare = this.d.state.worktrees.find((w) => w.repoId === repoId && w.kind === "spare");
34835
35825
  const restart = !!spare && profile !== undefined && profile !== resolveRun(repo, spare).profile;
34836
35826
  let carriedIn;
@@ -34841,7 +35831,6 @@ class WorktreeService {
34841
35831
  mode: opts.mode,
34842
35832
  model: opts.model,
34843
35833
  effort: opts.effort,
34844
- variant,
34845
35834
  from: opts.from,
34846
35835
  profile,
34847
35836
  unnamed: given ? undefined : true
@@ -34852,7 +35841,12 @@ class WorktreeService {
34852
35841
  if (rt.agent.runningAgent !== null && rt.agent.runningAgent !== agent)
34853
35842
  await rt.agent.restart();
34854
35843
  carriedIn = await this.carryMain(repo, wt, !!opts.carry);
34855
- rt.agent.send(agentPrompt, { context: withCarry(context(wt.id), carriedIn), attachments });
35844
+ const born = this.bornWith(wt, opts.plan);
35845
+ rt.agent.send(born?.text ?? prompt, {
35846
+ context: withCarry(context(wt.id), carriedIn),
35847
+ attachments,
35848
+ ...born ? { handed: born.handed } : {}
35849
+ });
34856
35850
  });
34857
35851
  if (claimed) {
34858
35852
  const { wt } = claimed;
@@ -34876,7 +35870,7 @@ class WorktreeService {
34876
35870
  release?.();
34877
35871
  if (restart)
34878
35872
  this.restartProcs(wt);
34879
- this.nameOrAsk(wt, task, given, variant);
35873
+ this.nameOrAsk(wt, task, given);
34880
35874
  if (carriedIn?.unmoved)
34881
35875
  throw new UserError(carriedIn.unmoved, { delivered: true });
34882
35876
  return wt;
@@ -34884,8 +35878,8 @@ class WorktreeService {
34884
35878
  if (!await isGitRepo(repo.path)) {
34885
35879
  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.`);
34886
35880
  }
34887
- const { id, dir } = freeSlot(join32(this.d.paths.worktreesDir, repo.name));
34888
- const wtPath = join32(this.d.paths.worktreesDir, repo.name, dir);
35881
+ const { id, dir } = freeSlot(join33(this.d.paths.worktreesDir, repo.name));
35882
+ const wtPath = join33(this.d.paths.worktreesDir, repo.name, dir);
34889
35883
  const branch = `toyon/${dir}`;
34890
35884
  await withRepoLock(repo.path, () => gitOrThrow(repo.path, "worktree", "add", "--no-track", "-b", branch, wtPath, baseOf(repo)));
34891
35885
  const wt = {
@@ -34900,7 +35894,6 @@ class WorktreeService {
34900
35894
  createdAt: Date.now(),
34901
35895
  promptedAt: Date.now(),
34902
35896
  agent,
34903
- ...variant ? { variant } : {},
34904
35897
  ...opts.from ? { from: opts.from } : {},
34905
35898
  ...opts.createdBy ? { createdBy: opts.createdBy } : {},
34906
35899
  ...profile !== undefined ? { profile } : {},
@@ -34909,16 +35902,34 @@ class WorktreeService {
34909
35902
  ...opts.effort ? { effort: opts.effort } : {}
34910
35903
  };
34911
35904
  const carried = await this.carryMain(repo, wt, !!opts.carry);
34912
- this.launch(wt, repo, repo.path);
34913
- this.d.runtime.ensureAgent(wt).agent.send(agentPrompt, { context: withCarry(context(id), carried), attachments });
34914
- this.nameOrAsk(wt, task, given, variant);
35905
+ this.launch(wt, repo, repo.path, opts.cold ? { start: false } : {});
35906
+ const born = this.bornWith(wt, opts.plan);
35907
+ this.d.runtime.ensureAgent(wt).agent.send(born?.text ?? prompt, {
35908
+ context: withCarry(context(id), carried),
35909
+ attachments,
35910
+ ...born ? { handed: born.handed } : {}
35911
+ });
35912
+ this.nameOrAsk(wt, task, given);
34915
35913
  if (carried.unmoved)
34916
35914
  throw new UserError(carried.unmoved, { delivered: true });
34917
35915
  return wt;
34918
35916
  }
34919
- nameOrAsk(wt, task, given, variant) {
35917
+ bornWith(wt, plan) {
35918
+ if (!plan)
35919
+ return null;
35920
+ const path = writePlanDocNow(wt.path, plan.markdown);
35921
+ if (path) {
35922
+ const live = this.d.state.requireWorktree(wt.id);
35923
+ live.plan = path;
35924
+ live.planned = true;
35925
+ this.d.state.save();
35926
+ this.d.hub.emit("worktreesChanged");
35927
+ }
35928
+ return { text: plan.prompt(path), handed: { ...plan.handed, ...path ? { path } : {} } };
35929
+ }
35930
+ nameOrAsk(wt, task, given) {
34920
35931
  if (given === undefined) {
34921
- this.scheduleNaming(wt, task, variant);
35932
+ this.scheduleNaming(wt, task);
34922
35933
  return;
34923
35934
  }
34924
35935
  fireAndForget(wt.id, this.rename(wt.id, given), "branch for the given title");
@@ -34959,7 +35970,7 @@ class WorktreeService {
34959
35970
  };
34960
35971
  const carried = await git(repo.path, "ls-tree", "-r", "--name-only", `${sha}^3`);
34961
35972
  const restored = (carried.ok ? carried.out.split(`
34962
- `).filter(Boolean) : []).filter((p) => !existsSync36(join32(wt.path, p)));
35973
+ `).filter(Boolean) : []).filter((p) => !existsSync36(join33(wt.path, p)));
34963
35974
  const applied = await git(wt.path, "stash", "apply", sha);
34964
35975
  if (applied.ok) {
34965
35976
  const ref = await entry();
@@ -34996,9 +36007,9 @@ class WorktreeService {
34996
36007
  const branch = kind === "pr" ? `pr/${number}` : ref;
34997
36008
  const title = kind === "pr" ? `pr-${number}` : cleanTitle2(ref) || "branch";
34998
36009
  let slug = branchSlug(title) || "branch";
34999
- if (existsSync36(join32(this.d.paths.worktreesDir, repo.name, slug)))
36010
+ if (existsSync36(join33(this.d.paths.worktreesDir, repo.name, slug)))
35000
36011
  slug = `${slug}-${shortId().slice(0, 4)}`;
35001
- const wtPath = join32(this.d.paths.worktreesDir, repo.name, slug);
36012
+ const wtPath = join33(this.d.paths.worktreesDir, repo.name, slug);
35002
36013
  await withRepoLock(repo.path, async () => {
35003
36014
  const out = (await listWorktrees(repo.path)).find((w) => w.branch === branch);
35004
36015
  if (out)
@@ -35167,24 +36178,14 @@ class WorktreeService {
35167
36178
  this.d.state.save();
35168
36179
  this.d.hub.emit("worktreesChanged");
35169
36180
  }
35170
- scheduleNaming(wt, prompt, variant) {
36181
+ scheduleNaming(wt, prompt) {
35171
36182
  if (!prompt.trim())
35172
36183
  return;
35173
- if (variant && variant.index !== 1)
35174
- return;
35175
36184
  if (this.naming.has(wt.id))
35176
36185
  return;
35177
36186
  this.naming.add(wt.id);
35178
36187
  const asked = (this.d.namer ?? makeNamer(this.d.runtime))(prompt, wt);
35179
- const applied = variant ? asked.then(async (name) => {
35180
- if (!name)
35181
- return;
35182
- for (const sibling of [wt, ...siblingsOf(wt, this.d.state.worktrees)]) {
35183
- await this.rename(sibling.id, `${name} v${sibling.variant.index}`).catch((e) => {
35184
- log.warn(sibling.id, "variant rename failed", e);
35185
- });
35186
- }
35187
- }) : asked.then((name) => {
36188
+ const applied = asked.then((name) => {
35188
36189
  if (name)
35189
36190
  return this.rename(wt.id, name);
35190
36191
  });
@@ -35274,7 +36275,7 @@ class WorktreeService {
35274
36275
  }
35275
36276
  }
35276
36277
  async keep(repo, wt) {
35277
- const index = join32(this.d.paths.archiveDir, `${wt.id}.index`);
36278
+ const index = join33(this.d.paths.archiveDir, `${wt.id}.index`);
35278
36279
  const kept = await keepState(repo.path, wt.path, archiveRef(wt.id), index) ?? await this.keepUnlinked(repo, wt);
35279
36280
  if (!kept)
35280
36281
  log.warn(wt.id, "could not keep its git state");
@@ -35311,10 +36312,10 @@ class WorktreeService {
35311
36312
  log.warn(wt.id, `its directory could not be deleted and stays at ${wt.path}`);
35312
36313
  }
35313
36314
  plansScratch(worktreeId) {
35314
- return join32(this.d.paths.archiveDir, `${worktreeId}.plans`);
36315
+ return join33(this.d.paths.archiveDir, `${worktreeId}.plans`);
35315
36316
  }
35316
36317
  keepPlans(wt) {
35317
- const from = join32(wt.path, PLANS_DIR);
36318
+ const from = join33(wt.path, PLANS_DIR);
35318
36319
  if (!existsSync36(from))
35319
36320
  return;
35320
36321
  try {
@@ -35459,7 +36460,7 @@ class WorktreeService {
35459
36460
  }
35460
36461
  archivedAttachment(archiveId, file) {
35461
36462
  const files = this.archive.chatFiles(archiveId);
35462
- return files && isAttachmentFile(file) ? join32(files.attachments, file) : null;
36463
+ return files && isAttachmentFile(file) ? join33(files.attachments, file) : null;
35463
36464
  }
35464
36465
  archivedGit(archiveId) {
35465
36466
  const rec = this.archive.get(archiveId);
@@ -35515,10 +36516,9 @@ class WorktreeService {
35515
36516
  throw new UserError(`${old.title} was archived without its commits, so there is nothing to restore`);
35516
36517
  if (rec.auto)
35517
36518
  log.info(old.id, `restoring a worktree that archived itself (${rec.auto})`);
35518
- const path = existsSync36(old.path) ? join32(dirname13(old.path), freeSlot(dirname13(old.path)).dir) : old.path;
35519
- const { variant: _variant, ...rest } = old;
36519
+ const path = existsSync36(old.path) ? join33(dirname13(old.path), freeSlot(dirname13(old.path)).dir) : old.path;
35520
36520
  const wt = {
35521
- ...rest,
36521
+ ...old,
35522
36522
  repoId: repo.id,
35523
36523
  path,
35524
36524
  branch: old.branch,
@@ -35554,7 +36554,7 @@ class WorktreeService {
35554
36554
  run.finish(0);
35555
36555
  this.arriving.delete(wt.id);
35556
36556
  wt.branch = branch;
35557
- move(chat.plans, join32(path, PLANS_DIR));
36557
+ move(chat.plans, join33(path, PLANS_DIR));
35558
36558
  const dropped = await git(repo.path, "update-ref", "-d", archiveRef(old.id));
35559
36559
  if (!dropped.ok)
35560
36560
  log.warn(old.id, `could not drop its archive ref: ${dropped.err}`);
@@ -35670,17 +36670,6 @@ class WorktreeService {
35670
36670
  this.d.state.save();
35671
36671
  this.d.hub.emit("worktreesChanged");
35672
36672
  }
35673
- async pickVariant(worktreeId) {
35674
- const wt = this.d.state.worktree(worktreeId);
35675
- if (!wt?.variant)
35676
- return;
35677
- for (const sibling of siblingsOf(wt, this.d.state.worktrees)) {
35678
- await this.archiveWorktree(sibling.id).catch((e) => log.warn(sibling.id, "could not archive variant sibling", e));
35679
- }
35680
- delete wt.variant;
35681
- this.d.state.save();
35682
- this.d.hub.emit("worktreesChanged");
35683
- }
35684
36673
  async graft(targetId, sourceIds) {
35685
36674
  const target = this.d.state.worktree(targetId);
35686
36675
  if (!target)
@@ -35733,6 +36722,24 @@ class WorktreeService {
35733
36722
  this.d.hub.emit("worktreesChanged");
35734
36723
  fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "start a parked preview");
35735
36724
  }
36725
+ retryPreview(worktreeId) {
36726
+ const wt = this.d.state.worktree(worktreeId);
36727
+ if (!wt)
36728
+ return;
36729
+ if (wt.parked) {
36730
+ this.startPreview(worktreeId);
36731
+ return;
36732
+ }
36733
+ const procs = this.d.runtime.get(worktreeId)?.procs;
36734
+ const down = procs && !procs.asleep ? procs.states().filter((p) => p.status === "crashed" || p.status === "unreachable" || p.status === "stopped") : [];
36735
+ if (down.length === 0) {
36736
+ fireAndForget(worktreeId, this.d.runtime.wake(worktreeId), "preview asked for");
36737
+ return;
36738
+ }
36739
+ for (const p of down) {
36740
+ fireAndForget(worktreeId, this.d.runtime.restartStream(worktreeId, p.name), "preview asked for");
36741
+ }
36742
+ }
35736
36743
  async setupAndStart(wt, repo, depsSource = repo.path, opts = {}) {
35737
36744
  this.d.runtime.markSetup(wt.id, true);
35738
36745
  try {
@@ -35742,14 +36749,14 @@ class WorktreeService {
35742
36749
  }
35743
36750
  }
35744
36751
  async mirrorBaseIgnore(repo) {
35745
- const src = join32(repo.path, ".gitignore");
36752
+ const src = join33(repo.path, ".gitignore");
35746
36753
  const tracked = existsSync36(src) && (await git(repo.path, "ls-files", "--", ".gitignore")).out !== "";
35747
36754
  const mirror = existsSync36(src) && !tracked;
35748
- const lines = mirror ? readFileSync21(src, "utf8").split(`
36755
+ const lines = mirror ? readFileSync22(src, "utf8").split(`
35749
36756
  `).filter((l) => l.trim() !== "") : [];
35750
36757
  await excludeBlock(repo.path, BASE_IGNORE, lines);
35751
36758
  }
35752
- async setupThenStart(wt, repo, depsSource, { setupCommands = true }) {
36759
+ async setupThenStart(wt, repo, depsSource, { setupCommands = true, start = true }) {
35753
36760
  await this.mirrorBaseIgnore(repo);
35754
36761
  const ceiling = timeoutFor(repo.config, "setup");
35755
36762
  if (this.d.cache && cachePolicy(repo.config)) {
@@ -35757,8 +36764,8 @@ class WorktreeService {
35757
36764
  await this.d.cache.restore(wt, repo, (line) => this.d.hub.emit("log", wt.id, "setup", line));
35758
36765
  run.finish(0);
35759
36766
  }
35760
- const srcNm = join32(depsSource, "node_modules");
35761
- const dstNm = join32(wt.path, "node_modules");
36767
+ const srcNm = join33(depsSource, "node_modules");
36768
+ const dstNm = join33(wt.path, "node_modules");
35762
36769
  if (existsSync36(srcNm) && !existsSync36(dstNm)) {
35763
36770
  const run = this.runs.begin(wt.id, "setup", { timeoutMs: ceiling, stage: "copying dependencies" });
35764
36771
  const r = await cloneTree(srcNm, dstNm);
@@ -35769,10 +36776,10 @@ class WorktreeService {
35769
36776
  log.warn(wt.id, "could not copy node_modules from the base checkout", r.error);
35770
36777
  }
35771
36778
  for (const f of LOCAL_CONFIG_FILES) {
35772
- const src = join32(depsSource, f);
35773
- if (!existsSync36(src) || existsSync36(join32(wt.path, f)))
36779
+ const src = join33(depsSource, f);
36780
+ if (!existsSync36(src) || existsSync36(join33(wt.path, f)))
35774
36781
  continue;
35775
- const r = await run("cp", [src, join32(wt.path, f)], wt.path);
36782
+ const r = await run("cp", [src, join33(wt.path, f)], wt.path);
35776
36783
  if (r.ok)
35777
36784
  this.d.hub.emit("log", wt.id, "setup", `copied ${f}`);
35778
36785
  else
@@ -35789,7 +36796,8 @@ class WorktreeService {
35789
36796
  } else if (code !== 0)
35790
36797
  this.d.hub.emit("log", wt.id, "setup", `setup failed (exit ${code}): ${cmd}`);
35791
36798
  }
35792
- await this.d.runtime.start(wt, repo);
36799
+ if (start)
36800
+ await this.d.runtime.start(wt, repo);
35793
36801
  }
35794
36802
  setPr(worktreeId, pr) {
35795
36803
  const wt = this.d.state.worktree(worktreeId);
@@ -35940,6 +36948,7 @@ ${wt.landing.body}` : s;
35940
36948
  await this.noteLand(repo, wt, mark);
35941
36949
  await this.restartFromMain(wt, baseOf(repo));
35942
36950
  await this.settleLanded(wt);
36951
+ this.d.hub.emit("landed", wt.id);
35943
36952
  }
35944
36953
  async land(worktreeId, message) {
35945
36954
  const { wt, repo } = this.landable(worktreeId, "land");
@@ -35950,12 +36959,9 @@ ${wt.landing.body}` : s;
35950
36959
  return { result: moved };
35951
36960
  return this.landOp(wt, repo, message);
35952
36961
  });
35953
- if (out.result.ok && out.archiveIds)
35954
- this.noteLanded(wt, out.result.message, out.archiveIds, out.mark);
35955
- return {
35956
- result: this.fixShip(wt, out.result, baseOf(repo)),
35957
- ...out.archiveIds ? { archiveIds: out.archiveIds } : {}
35958
- };
36962
+ if (out.result.ok && out.landed)
36963
+ this.noteLanded(wt, out.result.message, out.mark);
36964
+ return { result: this.fixShip(wt, out.result, baseOf(repo)) };
35959
36965
  }
35960
36966
  fixShip(wt, result, base) {
35961
36967
  if (result.ok)
@@ -35999,13 +37005,10 @@ ${wt.landing.body}` : s;
35999
37005
  return null;
36000
37006
  return { ok: false, message: "the agent has changed files since the check: land once its turn ends" };
36001
37007
  }
36002
- noteLanded(wt, message, archiveIds, mark) {
36003
- this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, archiveIds, ...mark ? { mark } : {}, ts: Date.now() });
37008
+ noteLanded(wt, message, mark) {
37009
+ this.d.runtime.ensureAgent(wt).agent.note({ type: "landed", message, ...mark ? { mark } : {}, ts: Date.now() });
36004
37010
  this.d.hub.emit("landed", wt.id);
36005
37011
  }
36006
- siblingIds(wt) {
36007
- return siblingsOf(wt, this.d.state.worktrees).map((s) => s.id);
36008
- }
36009
37012
  async followBranch(wt, current) {
36010
37013
  if (wt.kind === "main")
36011
37014
  return;
@@ -36064,7 +37067,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36064
37067
  if (policy.land === "pr") {
36065
37068
  if (wt.pr?.state === "merged" && !prTaken(wt)) {
36066
37069
  const { mark, ...result } = await this.prMergedOp(wt.id, undefined, w);
36067
- return result.ok ? { result, archiveIds: this.siblingIds(wt), mark } : { result };
37070
+ return result.ok ? { result, landed: true, mark } : { result };
36068
37071
  }
36069
37072
  const fetched = await this.fetchBase(wt, repo, w);
36070
37073
  if (!fetched.ok)
@@ -36104,7 +37107,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36104
37107
  const { mark: noted, ...r } = await this.prMergedOp(wt.id, mark, w);
36105
37108
  const on = `origin's ${repo.defaultBranch} has this work already`;
36106
37109
  const result = { ...r, message: r.ok ? `${on}; ${r.message}` : `${on}, but ${r.message}` };
36107
- return r.ok ? { result, archiveIds: this.siblingIds(wt), mark: noted } : { result };
37110
+ return r.ok ? { result, landed: true, mark: noted } : { result };
36108
37111
  }
36109
37112
  const named = !suggested ? undefined : splitMessage(policy.merge === "squash" ? await squashMessage(wt.path, base, suggested, !!typed) : suggested);
36110
37113
  const result = await openPr({
@@ -36160,7 +37163,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36160
37163
  const did = committedHere ? "committed and merged" : "merged";
36161
37164
  return {
36162
37165
  result: { ...result, message: `${did} into ${repo.defaultBranch}` },
36163
- archiveIds: this.siblingIds(wt),
37166
+ landed: true,
36164
37167
  mark: noted
36165
37168
  };
36166
37169
  }
@@ -36223,7 +37226,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36223
37226
  const here = followed.ok ? `${repo.defaultBranch} here ${followed.moved ? "pulled it" : "has it"}` : `${repo.defaultBranch} here was left where it is: ${followed.message}`;
36224
37227
  const how = { merge: "merged into", squash: "squashed onto", rebase: "rebased onto" }[method];
36225
37228
  const did = already ? `origin's ${repo.defaultBranch} has this work already` : `${committed ? `committed and ${how}` : how} ${repo.defaultBranch} and pushed`;
36226
- return { result: { ok: true, message: `${did}; ${here}` }, archiveIds: this.siblingIds(wt), mark: noted };
37229
+ return { result: { ok: true, message: `${did}; ${here}` }, landed: true, mark: noted };
36227
37230
  }
36228
37231
  async noteLand(repo, wt, mark, opts = {}) {
36229
37232
  if (!mark)
@@ -36293,7 +37296,7 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36293
37296
  else
36294
37297
  log.warn(worktreeId, `landing failed: ${result.message}`);
36295
37298
  if (result.ok && wt)
36296
- this.noteLanded(wt, result.message, this.siblingIds(wt), mark);
37299
+ this.noteLanded(wt, result.message, mark);
36297
37300
  return result;
36298
37301
  }
36299
37302
  async prMergedOp(worktreeId, carried, w = UNWATCHED) {
@@ -36550,6 +37553,37 @@ ${wt.landing.body}` : wt.landing.subject : undefined);
36550
37553
  base: baseOf(repo)
36551
37554
  };
36552
37555
  }
37556
+ async standing(worktreeId) {
37557
+ const wt = this.d.state.worktree(worktreeId);
37558
+ if (!wt || wt.kind === "spare" || this.going.has(worktreeId))
37559
+ return null;
37560
+ const repo = this.d.state.repo(wt.repoId);
37561
+ if (!repo)
37562
+ return null;
37563
+ const rec = await this.tree.refresh(worktreeId) ?? this.tree.get(worktreeId);
37564
+ const core = rec?.core;
37565
+ const last = wt.lands?.at(-1);
37566
+ const pr = prStanding(wt);
37567
+ return {
37568
+ main: wt.kind === "main",
37569
+ base: baseOf(repo),
37570
+ ...core ? { dirty: core.files.length } : {},
37571
+ ...core?.ahead === undefined ? {} : { ahead: core.ahead },
37572
+ ...core?.behind === undefined ? {} : { behind: core.behind },
37573
+ ...core?.unpushed === undefined ? {} : { unpushed: core.unpushed },
37574
+ ...wt.landed ? { landed: { subjects: last?.subjects ?? [], ...last?.pr === undefined ? {} : { pr: last.pr } } } : {},
37575
+ ...pr ? {
37576
+ pr: {
37577
+ number: pr.number,
37578
+ url: pr.url,
37579
+ state: pr.state,
37580
+ ...pr.checks ? { checks: pr.checks } : {},
37581
+ ...pr.review ? { review: pr.review } : {},
37582
+ ...pr.automerge ? { automerge: pr.automerge } : {}
37583
+ }
37584
+ } : {}
37585
+ };
37586
+ }
36553
37587
  async turnStarted(worktreeId) {
36554
37588
  const wt = this.d.state.worktree(worktreeId);
36555
37589
  if (wt && !this.going.has(worktreeId))
@@ -36809,11 +37843,8 @@ class ArchiveSweep {
36809
37843
  if (maybe.length === 0)
36810
37844
  continue;
36811
37845
  const fresh = new Map;
36812
- for (const w of beforeGit.rows) {
36813
- if (!maybe.some((m) => m.id === w.id || !!m.variant && m.variant.group === w.variant?.group))
36814
- continue;
37846
+ for (const w of maybe)
36815
37847
  fresh.set(w.id, await this.d.worktrees.freshCounts(w.id));
36816
- }
36817
37848
  const counts = (id) => fresh.get(id) ?? {};
36818
37849
  for (const w of maybe) {
36819
37850
  const reason = archiveReason(w, this.facts(repo.id, counts));
@@ -36861,17 +37892,17 @@ if (pruned > 0)
36861
37892
  var managed = await loadManaged();
36862
37893
  var paths = makePaths();
36863
37894
  ensureDirs(paths);
36864
- forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join33(paths.home, "transcripts-forwarded"));
37895
+ forwardTranscripts([paths.transcriptsDir, paths.archiveDir], join34(paths.home, "transcripts-forwarded"));
36865
37896
  var token = loadOrCreateToken(paths);
36866
37897
  var port = Number(process.env.TOYON_PORT ?? DAEMON_DEFAULT_PORT);
36867
- var helperApp = join33(paths.home, DAEMON_FILES.helper);
37898
+ var helperApp = join34(paths.home, DAEMON_FILES.helper);
36868
37899
  var helper = helperWanted({ platform: process.platform, cloud: cloud.enabled, port, home: process.env.TOYON_HOME }) ? new Helper({
36869
37900
  dir: helperApp,
36870
37901
  stub: HELPER_STUB,
36871
37902
  bun: process.execPath,
36872
37903
  cli: CLI_ENTRY,
36873
- log: join33(paths.home, DAEMON_FILES.launcherLog),
36874
- icon: join33(SHELL_DIST, "icon.svg"),
37904
+ log: join34(paths.home, DAEMON_FILES.launcherLog),
37905
+ icon: join34(SHELL_DIST, "icon.svg"),
36875
37906
  version: package_default.version
36876
37907
  }) : null;
36877
37908
  if (helper) {
@@ -36908,8 +37939,9 @@ var readiness2 = new AgentReadiness({
36908
37939
  connect: async (app, spec) => spawnAcp(app, agents.launch(spec, await prepareLaunch(paths.scratchDir, spec)), paths.scratchDir, `check:${spec.id}`),
36909
37940
  cwd: paths.scratchDir,
36910
37941
  options: (id, category) => state.cachedOptions(id, category),
36911
- learned: (id, category, choices) => {
36912
- if (state.setCachedOptions(id, category, choices))
37942
+ runs: (id, category) => state.cachedDefault(id, category),
37943
+ learned: (id, category, choices, runs) => {
37944
+ if (state.setCachedOptions(id, category, choices, runs))
36913
37945
  hub.emit("agentsChanged");
36914
37946
  },
36915
37947
  quick: (id) => {
@@ -36939,6 +37971,8 @@ var runtime = new RuntimeRegistry({
36939
37971
  bridgeScript: () => bridge.get(),
36940
37972
  pageErrors: (id) => pageErrors.ambient(id),
36941
37973
  turnStarting: (id) => worktrees.turnStarted(id),
37974
+ archived: (repoId) => worktrees.archived(repoId),
37975
+ standing: (id) => worktrees.standing(id),
36942
37976
  remote: () => setting.get(),
36943
37977
  gate: previewGate,
36944
37978
  viewed: (id) => idle.isViewed(id),
@@ -36975,13 +38009,15 @@ var worktrees = new WorktreeService({
36975
38009
  interest.onFirst((id) => worktrees.tree.watch(id));
36976
38010
  interest.onLast((id) => worktrees.tree.unwatch(id));
36977
38011
  new BackendShare({ state, hub, runtime, own: (id, names) => worktrees.ownProcs(id, names) });
36978
- var handoff2 = new HandoffService({
38012
+ var pieces = new PiecesService({
36979
38013
  state,
36980
38014
  hub,
36981
38015
  runtime,
36982
- create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts)
38016
+ attachments: attachments2,
38017
+ create: (repoId, prompt, opts) => worktrees.create(repoId, prompt, opts),
38018
+ reader: makePiecesReader(runtime, agents, state)
36983
38019
  });
36984
- tools.add(handoff2);
38020
+ tools.add(pieces);
36985
38021
  var turns = new TurnService({
36986
38022
  state,
36987
38023
  hub,
@@ -37149,7 +38185,7 @@ var {
37149
38185
  landing,
37150
38186
  fix,
37151
38187
  pageErrors,
37152
- handoff: handoff2,
38188
+ pieces,
37153
38189
  mcp,
37154
38190
  themes,
37155
38191
  agents,
@@ -37159,7 +38195,6 @@ var {
37159
38195
  afterLand,
37160
38196
  restarter,
37161
38197
  update,
37162
- planTasks: makePlanner(agents),
37163
38198
  folderDialog: folderDialog()
37164
38199
  }
37165
38200
  });