agentbox-app 0.1.7 → 0.1.12

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 (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +11 -11
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +116 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
@@ -26,10 +26,10 @@ const exec = promisify(execFile);
26
26
  * a chat of its own, that takes over and nothing forks twice.
27
27
  */
28
28
  /**
29
- * A row name out of the sentence she typed after `/fork`: her first sentence,
29
+ * A row name out of the sentence typed after `/fork`: its first sentence,
30
30
  * or the first sixty characters of it, so the inbox line says which fork this
31
31
  * is rather than which row it came from. Whitespace flattened because a title
32
- * is one line and she can paste a paragraph.
32
+ * is one line and a paragraph can be pasted.
33
33
  */
34
34
  export function forkTitle(words, cap = 60) {
35
35
  const flat = String(words ?? '').replace(/\s+/g, ' ').trim();
@@ -45,18 +45,16 @@ function forkTask(sup, productSlug, id, item, engine, op, publish) {
45
45
  }
46
46
  // NAMING THE KEY, because the menu closes at the space and plain return then
47
47
  // writes a newline, the way it does on every other reply here. Without that
48
- // sentence she types the words, presses return, and is back to a key that
49
- // does nothing (hers, 2026-09-23: "Fork does nothing when I type /fork then
50
- // hit return").
48
+ // sentence the user types the words, presses return, and is back to a key
49
+ // that does nothing, so a bare /fork looks as if it did nothing.
51
50
  if (!op.args) return publish('failed', 'Say what the fork should try, as in /fork try it without the modal, then press command and return.', op.name);
52
51
  const chat = sup.rowSessionFor(item, sup.store.listProducts().find((p) => p.slug === productSlug));
53
52
  if (!chat?.sessionId) return publish('failed', 'This task has no saved conversation to fork yet. Send a message first.', op.name);
54
53
  const made = sup.store.composeItem(productSlug, {
55
- // THE FORK IS NAMED AFTER WHAT SHE ASKED IT TO TRY, not after the row it
56
- // came from. Naming it `<parent title> · fork` put two rows in her inbox
57
- // whose names differ by one word at the end, and the first thing she said
58
- // after using it was that she could not tell which agent she was talking to
59
- // (2026-09-23). The instruction is the only thing that distinguishes them,
54
+ // THE FORK IS NAMED AFTER WHAT IT WAS ASKED TO TRY, not after the row it
55
+ // came from. Naming it `<parent title> · fork` put two rows in the inbox
56
+ // whose names differ by one word at the end, so there was no telling which
57
+ // agent was which. The instruction is the only thing that distinguishes them,
60
58
  // so it is what the row is called; the parent is still on the row as the
61
59
  // `fork:` label, which is where the machinery reads it anyway.
62
60
  title: forkTitle(op.args),
@@ -1,8 +1,7 @@
1
1
  // ONE FOLDER PER TASK, OWNED BY THE APP RATHER THAN BY THE AGENT IN IT.
2
2
  //
3
- // the founder, 2026-09-22, choosing between letting Claude Code make its own
4
- // worktree and doing it ourselves: "the app makes the folder for both agents".
5
- // The two engines are not equal here. Claude Code has isolation built in
3
+ // Between letting Claude Code make its own worktree and doing it ourselves, the
4
+ // app makes the folder for both agents. The two engines are not equal here. Claude Code has isolation built in
6
5
  // (`--worktree`), makes the folder under `.claude/worktrees/`, locks it, reuses
7
6
  // it by name and deletes it when it leaves nothing behind. Codex has none of
8
7
  // that, only `--cd` for choosing where to work. So the rules have to live on
@@ -11,7 +10,7 @@
11
10
  // exactly where Claude Code would, so a person who opens the repository in a
12
11
  // terminal sees the layout they already know.
13
12
  //
14
- // WHAT WAS MEASURED, ON HER MAC, 2026-09-22. This repository had 25 leftover
13
+ // WHAT WAS MEASURED, 2026-09-22. This repository had 25 leftover
15
14
  // agent copies holding 26 GB: twelve with edits committed nowhere, twelve on
16
15
  // branches never merged, one safe to delete. Isolation was a rule in the worker
17
16
  // brief; the rule's first half (make a copy) was kept and its second half (fold
@@ -51,6 +50,9 @@ const BRANCH_PREFIX = `${nameSlug}/`;
51
50
  /** Ids are `w-` plus hex today. This is the guard for the day they are not. */
52
51
  const SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
53
52
 
53
+ /** What an unfinished folder is called. A task name may never start with a dot. */
54
+ const BUILDING = '.building-';
55
+
54
56
  const git = (cwd, args) => execFileSync('git', args, {
55
57
  cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
56
58
  }).trim();
@@ -81,6 +83,32 @@ export function taskFolderPath(repoPath, id) {
81
83
  return path.join(real(repoPath), ...WORKTREES, safeTaskName(id));
82
84
  }
83
85
 
86
+ /**
87
+ * WHERE A FOLDER IS BUILT, WHICH IS NEVER WHERE IT IS RUN IN.
88
+ *
89
+ * Making one is a worktree plus a block clone of the checkout. It used to be
90
+ * built AT the path a session runs in, so for the whole of that a directory sat
91
+ * there, registered with git and holding almost none of the code. MEASURED on
92
+ * astral-video, the repository this happened in, 2026-10-01: the build takes 115
93
+ * seconds now that the checkout is 14 GB, and the path a session runs in
94
+ * appeared 2.7 seconds in. That is 112 seconds of a folder that looked ready and
95
+ * was not, against a supervisor tick of fifteen seconds, so seven ticks could
96
+ * read it. Everything that asks whether a row's folder is ready asks the disk,
97
+ * which means for those 112 seconds every one of them said yes:
98
+ * the supervisor started a session in a half written folder, and when the build
99
+ * finished it started a second session on the same row (2026-09-30,
100
+ * w-4d722ecf92, where the second one's claim came back refused and the first
101
+ * one's folder was deleted under it a minute in).
102
+ *
103
+ * Built here and MOVED into place once it is finished and checked, so a
104
+ * directory at the real path means a folder that is complete. That makes every
105
+ * existing question correct without any of them learning a new one, and it
106
+ * leaves the rollback below nothing to delete but its own unfinished copy.
107
+ */
108
+ export function buildingFolderPath(repoPath, id) {
109
+ return path.join(real(repoPath), ...WORKTREES, `${BUILDING}${safeTaskName(id)}`);
110
+ }
111
+
84
112
  export function taskBranch(id) { return `${BRANCH_PREFIX}${safeTaskName(id)}`; }
85
113
 
86
114
  /** The repository root, or null when this is not a checkout at all. */
@@ -156,20 +184,52 @@ export function ensureTaskFolder(dir, id, { pid = process.pid, dependencies = tr
156
184
 
157
185
  hideFromTheCheckout(root);
158
186
 
187
+ // EVERY STEP BELOW HAPPENS SOMEWHERE ELSE, and the folder appears at its own
188
+ // path in one move at the end. `buildingFolderPath` is why. A build the app
189
+ // died in the middle of leaves an unfinished copy there, which nobody wants
190
+ // and which would refuse the next attempt, so it goes first.
191
+ const staging = buildingFolderPath(root, name);
192
+ clearUnfinished(root, staging);
193
+
159
194
  const branchExists = tryGit(root, ['rev-parse', '--verify', '-q', `refs/heads/${branch}`]).ok;
160
195
  const from = branchExists ? branch : baseRef(root);
161
- if (!cloneCheckout(root, folder, branch, from, branchExists)) {
162
- const add = branchExists
163
- ? tryGit(root, ['worktree', 'add', folder, branch])
164
- : tryGit(root, ['worktree', 'add', '-b', branch, folder, baseRef(root)]);
196
+ if (!cloneCheckout(root, staging, branch, from, branchExists)) {
197
+ // The fast path may have created the branch and then failed the copy.
198
+ // The branch stays: this file never deletes one. The slow path uses it
199
+ // when it is already there, which is what `worktree add -b` would refuse.
200
+ const existsNow = tryGit(root, ['rev-parse', '--verify', '-q', `refs/heads/${branch}`]).ok;
201
+ const add = existsNow
202
+ ? tryGit(root, ['worktree', 'add', staging, branch])
203
+ : tryGit(root, ['worktree', 'add', '-b', branch, staging, baseRef(root)]);
165
204
  if (!add.ok) throw Error(`Could not make a folder for ${name}: ${add.out}`);
166
- if (dependencies) cloneDependencies(root, folder);
205
+ if (dependencies) cloneDependencies(root, staging);
206
+ }
207
+
208
+ // `worktree move` rather than a rename, so git's own record of where this
209
+ // worktree lives moves with it. It refuses if anything is already at the
210
+ // destination, which is the last guard against walking over a live folder.
211
+ const moved = tryGit(root, ['worktree', 'move', staging, folder]);
212
+ if (!moved.ok) {
213
+ clearUnfinished(root, staging);
214
+ throw Error(`Could not make a folder for ${name}: ${moved.out}`);
167
215
  }
168
216
 
169
217
  lock(root, folder, pid);
170
218
  return { path: folder, branch, created: true };
171
219
  }
172
220
 
221
+ /**
222
+ * Throw away an unfinished copy. Nothing is ever lost here: a staging folder
223
+ * has never been handed to anybody, so the most it can hold is a checkout that
224
+ * did not finish. The branch is left alone, as everywhere else in this file.
225
+ */
226
+ function clearUnfinished(root, staging) {
227
+ if (!fs.existsSync(staging) && !record(root, staging)) return;
228
+ tryGit(root, ['worktree', 'remove', '--force', staging]);
229
+ try { fs.rmSync(staging, { recursive: true, force: true }); } catch { /* already gone */ }
230
+ tryGit(root, ['worktree', 'prune']);
231
+ }
232
+
173
233
  /**
174
234
  * MAKING THE FOLDER WITHOUT PAYING FOR IT TWICE.
175
235
  *
@@ -188,7 +248,20 @@ export function ensureTaskFolder(dir, id, { pid = process.pid, dependencies = tr
188
248
  * commit, down to an empty `git status`, the folder is thrown away and the
189
249
  * ordinary checkout runs. A fast folder that is subtly not the code would be
190
250
  * far worse than a slow one.
251
+ *
252
+ * THE COPY IS THE PLATFORM'S OWN CLONE. macOS `cp -c` is APFS clonefile.
253
+ * GNU `cp -a --reflink=auto` shares blocks where the filesystem can and
254
+ * copies where it cannot. `-c` is not an option there, and a copy that
255
+ * failed used to leave the branch the fast path had just created, so the
256
+ * ordinary checkout then died with "a branch named … already exists".
191
257
  */
258
+ function copyTree(from, to) {
259
+ const args = process.platform === 'darwin'
260
+ ? ['-c', '-R', from, to]
261
+ : ['-a', '--reflink=auto', from, to];
262
+ execFileSync('cp', args, { stdio: ['ignore', 'ignore', 'pipe'] });
263
+ }
264
+
192
265
  function cloneCheckout(root, folder, branch, from, branchExists) {
193
266
  // TRACKED CONTENT ONLY, on both sides of this. Her checkout permanently
194
267
  // carries a couple of untracked files (a scratch `:memory:.ses`, a folder of
@@ -210,17 +283,19 @@ function cloneCheckout(root, folder, branch, from, branchExists) {
210
283
  try {
211
284
  for (const entry of fs.readdirSync(root)) {
212
285
  if (entry === '.git' || entry === WORKTREES[0]) continue;
213
- execFileSync('cp', ['-c', '-R', path.join(root, entry), folder], { stdio: ['ignore', 'ignore', 'pipe'] });
286
+ copyTree(path.join(root, entry), folder);
214
287
  }
215
288
  if (!tryGit(folder, ['reset', '-q', 'HEAD']).ok) throw Error('the index would not fill in');
216
289
  const after = tryGit(folder, ['status', '--porcelain', '--untracked-files=no']);
217
290
  if (!after.ok || after.out.length > 0) throw Error('the clone did not match the commit');
218
291
  return true;
219
292
  } catch {
220
- // Back to nothing, so the ordinary path starts from a clean sheet.
221
- tryGit(root, ['worktree', 'remove', '--force', folder]);
222
- try { fs.rmSync(folder, { recursive: true, force: true }); } catch { /* already gone */ }
223
- tryGit(root, ['worktree', 'prune']);
293
+ // Back to nothing, so the ordinary path starts from a clean sheet. What is
294
+ // thrown away here is the UNFINISHED COPY and never a folder anybody is in:
295
+ // `folder` is the staging path, which no session has ever been given. It
296
+ // used to be the real one, and a `remove --force` of that is how a running
297
+ // agent lost the ground under it.
298
+ clearUnfinished(root, folder);
224
299
  return false;
225
300
  }
226
301
  }
@@ -257,7 +332,7 @@ function cloneDependencies(root, folder) {
257
332
  const to = path.join(folder, 'node_modules');
258
333
  if (!fs.existsSync(from) || fs.existsSync(to)) return false;
259
334
  try {
260
- execFileSync('cp', ['-c', '-R', from, to], { stdio: ['ignore', 'ignore', 'pipe'] });
335
+ copyTree(from, to);
261
336
  return true;
262
337
  } catch {
263
338
  try { fs.rmSync(to, { recursive: true, force: true }); } catch { /* nothing to undo */ }
@@ -390,7 +465,10 @@ export function listTaskFolders(dir) {
390
465
  if (!root) return [];
391
466
  const home = path.join(root, ...WORKTREES);
392
467
  return registered(root)
393
- .filter((w) => path.dirname(w.path) === home && !w.prunable)
468
+ // A folder still being built is not one of hers: it has no row standing in
469
+ // it and nothing in it is wanted, so it may never be listed, shown or swept.
470
+ .filter((w) => path.dirname(w.path) === home && !w.prunable
471
+ && !path.basename(w.path).startsWith(BUILDING))
394
472
  .map((w) => {
395
473
  const { dirty, merged } = folderState(root, w.path);
396
474
  let touchedAt = null;
@@ -18,9 +18,9 @@ export async function taskRemoteControl(sup,product,id,action='status') {
18
18
  if(sup._engineFor(item)==='codex')throw Error('Remote Control is currently available for Claude Code conversations only.');
19
19
  if(action==='dismiss')return current()?.state==='failed'&&!current()?.mayBeActive?publish('off'):current();
20
20
  if(current()?.state==='connecting'||current()?.state==='disconnecting')return current();
21
- // Typing the command IS the decision, so it never stops at a card asking again.
22
- // Her words, w-79bffd7ec3: "should trigger it to turn on or off, depending on
23
- // its current state, without multiple confirmations."
21
+ // Typing the command IS the decision, so it never stops at a card asking again
22
+ // (w-79bffd7ec3): it turns remote control on or off depending on its current
23
+ // state, with no further confirmation.
24
24
  if(action==='toggle')action=['connected','ready','reconnecting'].includes(current()?.state)||current()?.mayBeActive?'disable':'enable';
25
25
  if(action==='enable'&&['connected','ready','reconnecting'].includes(current()?.state))return current();
26
26
  let session=sup.sessions.get(id);
@@ -32,6 +32,17 @@ export function terminalPlace({store,supervisor,product,id,agent}){
32
32
  return {cwd,notice};
33
33
  }
34
34
  export function terminalFolder(args){return terminalPlace(args).cwd;}
35
+ // The shell a person actually has. `$SHELL` when that file exists, then
36
+ // zsh, bash, and sh. A machine with no zsh used to spawn `/bin/zsh` and
37
+ // the terminal never came up.
38
+ export function loginShell(env=process.env,exists=fs.existsSync){
39
+ const preferred=env.SHELL;
40
+ if(preferred&&exists(preferred))return preferred;
41
+ for(const candidate of ['/bin/zsh','/bin/bash','/bin/sh']){
42
+ if(exists(candidate))return candidate;
43
+ }
44
+ return preferred||'/bin/sh';
45
+ }
35
46
  // Enumerate only descendants of our own shell, including background process
36
47
  // groups. Kill children before shell; closing a pane never calls this.
37
48
  export function disposeTerminalProcess(pty){
@@ -69,7 +80,7 @@ const IDLE_MS=600000;
69
80
  const SWEEP_MS=60000;
70
81
  const MAX_TERMINALS=20;
71
82
  export class TaskTerminals {
72
- constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=process.env.SHELL||'/bin/zsh',idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now()}={}){
83
+ constructor({spawn,resolve,disposeProcess=disposeTerminalProcess,env=terminalEnv(),shell=loginShell(),idleMs=IDLE_MS,sweepMs=SWEEP_MS,now=()=>Date.now()}={}){
73
84
  this.spawn=spawn??((...args)=>require('node-pty').spawn(...args));this.resolve=resolve;this.disposeProcess=disposeProcess;this.env=env;this.shell=shell;this.sessions=new Map();
74
85
  this.idleMs=idleMs;this.now=now;
75
86
  // Keys whose shell this class ended on its own, and WHY. The next open on
@@ -133,7 +144,8 @@ export class TaskTerminals {
133
144
  }
134
145
  const place=this.resolve(key);
135
146
  const cwd=typeof place==='string'?place:place.cwd;
136
- const pty=this.spawn(this.shell,['-l'],{cwd,env:this.env,cols:80,rows:24,name:'xterm-256color'});
147
+ const command=place?.command;
148
+ const pty=this.spawn(command?.file??this.shell,command?.args??['-l'],{cwd,env:{...this.env,...command?.env},cols:80,rows:24,name:'xterm-256color'});
137
149
  let resolveExit;const exitPromise=new Promise(resolve=>{resolveExit=resolve;});
138
150
  const session={token:randomUUID(),pty,cwd,buffer:'',offset:0,exitCode:null,exited:false,exitPromise,lastReadAt:this.now()};this.sessions.set(key,session);
139
151
  /*