agentbox-app 0.1.13 → 0.1.16

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 (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -1,60 +1,32 @@
1
- // THE LOCAL FILES A NEW TASK FOLDER IS GIVEN, AND THE ONES IT IS REFUSED.
1
+ // The untracked local files a new task folder is given: dependencies, a `.env`,
2
+ // a machine's own config. A worktree holds only the commit, so without this
3
+ // they are missing.
2
4
  //
3
- // A worktree is a fresh checkout, so it holds the commit and nothing else. Every
4
- // file a repository needs that git does not track -- the dependencies, a `.env`,
5
- // a key, a machine's own config -- is simply absent, and that is what people
6
- // mean when they say agent worktrees are brittle. Measured on one Mac on
7
- // 2026-10-07 (w-5952e6de3e): of the seven task folders then live, seven had
8
- // node_modules because exactly one ignored path was ever carried, and five had
9
- // no `zero.config.json`, so the app ran in them on fallback defaults.
5
+ // `.worktreeinclude` is the file Conductor and Claude Code already read: a file
6
+ // at the repository root in .gitignore syntax naming the ignored files to carry.
7
+ // Git matches the patterns (`git ls-files --others --ignored --exclude-from`),
8
+ // so negation, anchoring and globs behave exactly as git defines them.
10
9
  //
11
- // `.worktreeinclude` IS NOT OURS, AND THAT IS WHY IT WAS CHOSEN. It is a file at
12
- // the repository root in .gitignore pattern syntax, naming the IGNORED files to
13
- // carry into a new worktree. Conductor reads it; so does Claude Code. A
14
- // repository already set up for either works here with nothing new to write.
10
+ // That command does not consult the repository's own .gitignore. A path it
11
+ // selects that the repository does not ignore would stay untracked in the
12
+ // folder, and `parkTaskFolder` runs `git add -A`, so closing the task would
13
+ // commit it, credentials included. Every selected path is therefore checked
14
+ // with `git check-ignore` in both checkouts.
15
15
  //
16
- // THE PATTERNS ARE MATCHED BY GIT, NEVER BY US. `git ls-files --others --ignored
17
- // --exclude-from=<list>` answers with the files those patterns select, which gets
18
- // comments, blank lines, anchoring, nested globs, directory patterns and `!`
19
- // negation right by construction rather than by a matcher of ours that would
20
- // drift. `--others` never lists tracked content, so a pattern naming a file the
21
- // commit carries selects nothing.
22
- //
23
- // AND SELECTING IS NOT THE SAME AS BEING IGNORED, which cost a review to notice.
24
- // That command means "untracked files matching THESE rules"; it does not consult
25
- // the repository's own .gitignore at all. So a `.worktreeinclude` naming an
26
- // untracked file that the repository does NOT ignore would have carried it in,
27
- // where it stays untracked -- and `parkTaskFolder` runs `git add -A`, so closing
28
- // the task would commit it. A credential named in this file would have been
29
- // committed onto a branch. Every selected path is therefore put through
30
- // `git check-ignore` IN BOTH CHECKOUTS, and anything the repository does not
31
- // actually ignore is refused rather than carried (Codex's review, 2026-10-07).
32
- //
33
- // NOTHING IS DROPPED QUIETLY. Every path this module will not carry, and every
34
- // link it cannot vouch for, refuses the folder and names itself. A folder
35
- // quietly missing a file somebody asked for by name, or quietly sharing its
36
- // dependencies with the checkout, is the exact fault this module exists to end,
37
- // so it may not be the way this module fails.
16
+ // Every path that is not carried, and every link that cannot be vouched for,
17
+ // refuses the whole folder by name. Nothing is dropped quietly.
38
18
  import { execFileSync } from 'node:child_process';
39
19
  import fs from 'node:fs';
40
20
  import os from 'node:os';
41
21
  import path from 'node:path';
42
22
 
43
- /** The file a repository says what it needs in, at its root. */
44
23
  export const INCLUDE_FILE = '.worktreeinclude';
45
24
 
46
25
  /**
47
- * ONE COPY, THE PLATFORM'S OWN, FOR EVERY TREE EITHER FILE MOVES.
48
- *
49
- * macOS `cp -c` asks APFS for a block-sharing clone. GNU `cp` has no `-c` at
50
- * all and would simply fail; `-a --reflink=auto` shares blocks where the
51
- * filesystem can do it and copies where it cannot, which is the same bargain.
52
- * The inbox runs on Linux too (origin/main, bec6d92), so a hardcoded `-c` is a
53
- * copy that cannot work there.
54
- *
55
- * It lives in this file rather than in `main/task-folders.mjs`, which is the
56
- * other caller, because that file imports this one and the reverse would be a
57
- * circle.
26
+ * macOS `cp -c` asks APFS for a block-sharing clone. GNU `cp` has no `-c`;
27
+ * `-a --reflink=auto` shares blocks where the filesystem can and copies where
28
+ * it cannot. Lives here rather than in `main/task-folders.mjs`, the other
29
+ * caller, because that file imports this one.
58
30
  */
59
31
  export function copyTree(from, to) {
60
32
  const args = process.platform === 'darwin'
@@ -64,46 +36,29 @@ export function copyTree(from, to) {
64
36
  }
65
37
 
66
38
  /**
67
- * What a repository that has never heard of this gets: its dependencies, which
68
- * is exactly what was carried before, so nothing is worse off for not asking.
69
- *
70
- * Anchored, because the old behaviour was the root's `node_modules` and not every
71
- * `node_modules` anywhere in the tree. And WITHOUT the trailing slash, which is
72
- * not cosmetic: measured 2026-10-07, `/node_modules/` matches a directory and
73
- * nothing else, so a `node_modules` that is really a SYMLINK to a shared one is
74
- * not selected, not carried, and -- the part that matters -- not reported either.
75
- * Without the slash it is selected, and then refused by name.
39
+ * For a repository with no `.worktreeinclude`: the root's dependencies. No
40
+ * trailing slash on purpose. `/node_modules/` matches directories only, so a
41
+ * `node_modules` that is a symlink to a shared one would be skipped without a
42
+ * word. Without the slash it is selected and then refused by name.
76
43
  */
77
44
  export const DEFAULT_PATTERNS = ['/node_modules'];
78
45
 
79
- /**
80
- * Where the app's own task folders live, which may never be carried into one.
81
- * It is ignored (the app ignores it itself), so a pattern as broad as `**`
82
- * selects it, and carrying it would copy every other task's folder into this
83
- * one. Said as a constant because it is the single unconditional exclusion.
84
- */
46
+ /** The app's own task folders. They are ignored, so a pattern like `**` selects
47
+ * them, and carrying them would copy every other task into this one. */
85
48
  const MANAGED = '.claude/worktrees';
86
49
 
87
50
  const git = (cwd, args, input) => execFileSync('git', args, {
88
51
  cwd, input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
89
52
  });
90
53
 
91
- /** Lines that are a pattern: not blank, not a comment. */
92
54
  const patternsIn = (text) => text.split('\n')
93
55
  .map((line) => line.replace(/\r$/, ''))
94
56
  .filter((line) => line.trim() !== '' && !line.trimStart().startsWith('#'));
95
57
 
96
58
  /**
97
- * What this repository asks for.
98
- *
99
- * A FILE THAT EXISTS AND SELECTS NOTHING CARRIES NOTHING. Present and empty is a
100
- * decision, not an absence: without that there is no way to say "give me a bare
101
- * folder", and a repository that deliberately wants its dependencies installed
102
- * fresh would be overruled by our default.
103
- *
104
- * A file that exists and cannot be READ is an error and not a default. Falling
105
- * back there would answer "we could not find out what you need" with "here is
106
- * what we guessed", which is the shape of every bug in this module's history.
59
+ * An empty `.worktreeinclude` carries nothing rather than the default, so a
60
+ * repository can ask for a bare folder. One that exists and cannot be read
61
+ * throws rather than falling back to a guess.
107
62
  */
108
63
  export function askedFor(root) {
109
64
  const file = path.join(root, INCLUDE_FILE);
@@ -113,12 +68,8 @@ export function askedFor(root) {
113
68
  }
114
69
 
115
70
  /**
116
- * The paths those patterns select in this checkout, as git sees them.
117
- *
118
- * `--directory` collapses a wholly untracked directory to one entry, which turns
119
- * a node_modules of sixty thousand files into a single `cp`. `-z` because a
120
- * newline is a legal character in a filename and this list decides what is
121
- * copied.
71
+ * `--directory` collapses a wholly untracked directory to one entry, so
72
+ * node_modules is one `cp`. `-z` because a filename may contain a newline.
122
73
  */
123
74
  export function localFilesIn(root) {
124
75
  const { patterns, source } = askedFor(root);
@@ -141,14 +92,10 @@ export function localFilesIn(root) {
141
92
  }
142
93
 
143
94
  /**
144
- * THE EXCLUSIONS NO PATTERN CAN OVERRIDE.
145
- *
146
- * `.git` is the repository, and the folder other tasks live in is other tasks'
147
- * work. A selected path that CONTAINS the managed folder is dropped whole rather
148
- * than picked apart: `**` collapses to `.claude`, and narrowing that by hand
149
- * would be a second matcher to get wrong. A repository that needs one file from
150
- * in there names it exactly, and an exact name is not an ancestor of the managed
151
- * folder, so it still comes through.
95
+ * Exclusions no pattern can override: `.git` and the task folders. A path that
96
+ * contains the task folders (`**` collapses to `.claude`) is dropped whole
97
+ * rather than narrowed by a matcher of ours. A repository that needs one file
98
+ * from `.claude` names it exactly, and that still comes through.
152
99
  */
153
100
  function keepable(rel) {
154
101
  if (rel.split('/')[0] === '.git') return false;
@@ -158,10 +105,9 @@ function keepable(rel) {
158
105
  }
159
106
 
160
107
  /**
161
- * Which of these paths the repository's OWN rules ignore, asked in whichever
162
- * checkout is passed. Exit 1 means "none of them", which is an answer and not a
163
- * failure; anything else is a failure and is thrown, because treating a broken
164
- * git call as "not ignored" silently stops carrying everything.
108
+ * Which of these paths the checkout's own rules ignore. `git check-ignore`
109
+ * exits 1 for "none of them"; any other failure throws rather than reading as
110
+ * "not ignored".
165
111
  */
166
112
  function ignoredIn(cwd, paths) {
167
113
  if (!paths.length) return new Set();
@@ -175,16 +121,10 @@ function ignoredIn(cwd, paths) {
175
121
  }
176
122
 
177
123
  /**
178
- * WHAT IS AT THIS PATH, AND "WE COULD NOT LOOK" IS NOT "NOTHING IS THERE".
179
- *
180
- * Every `lstat` here used to be wrapped in a bare `catch` that answered false,
181
- * so a path the app has no permission to look at read exactly like a path with
182
- * nothing at it. Both of the questions below are asked to decide whether
183
- * something may be written, and answering them from an error nobody saw is how
184
- * a folder gets built through a link that was never inspected. ENOENT and
185
- * ENOTDIR are real absences: the name is not there, or a name on the way to it
186
- * is a file, and either way nothing can be. Anything else is this folder
187
- * failing to be made, which `carryLocalFiles` reports on the row.
124
+ * A path that cannot be looked at is not a path with nothing at it. ENOENT and
125
+ * ENOTDIR are real absences; any other error throws, because these answers
126
+ * decide whether something may be written through a link nobody inspected.
127
+ * Pinned by tests/a-file-the-app-cannot-look-at-is-not-a-file-that-is-missing.test.mjs.
188
128
  */
189
129
  function lookAt(p) {
190
130
  try { return fs.lstatSync(p); }
@@ -199,10 +139,8 @@ const isLink = (p) => lookAt(p)?.isSymbolicLink() === true;
199
139
  const stillThere = (p) => lookAt(p) !== null;
200
140
 
201
141
  /**
202
- * Where a link points, with the same division. ENOENT is the link going away
203
- * under us and EINVAL is the path not being a link at all, which are both
204
- * ordinary; a link we are not allowed to read is a link we cannot judge, and
205
- * carrying one of those in unexamined is the whole risk this module manages.
142
+ * Where a link points, or null if it went away (ENOENT) or is no longer a link
143
+ * (EINVAL). A link that cannot be read cannot be judged, so that throws.
206
144
  */
207
145
  function linkTarget(p) {
208
146
  try { return fs.readlinkSync(p); }
@@ -212,7 +150,6 @@ function linkTarget(p) {
212
150
  }
213
151
  }
214
152
 
215
- /** Is any directory on the way to this path a link? Then nothing may be written through it. */
216
153
  function reachedThroughALink(folder, rel) {
217
154
  let at = folder;
218
155
  for (const part of rel.split('/').slice(0, -1)) {
@@ -223,11 +160,8 @@ function reachedThroughALink(folder, rel) {
223
160
  }
224
161
 
225
162
  /**
226
- * Is this path under that folder, as a path, saying nothing about what is there?
227
- *
228
- * `..` IS A COMPONENT AND NOT A PREFIX. `startsWith('..')` reads a directory
229
- * honestly named `..deps` as a climb out of the folder, and refused the whole
230
- * task over it (Codex's review, 2026-10-07).
163
+ * Whether a path is under a folder, by its text only. `..` is checked as a
164
+ * whole component so a directory named `..deps` is not read as a climb out.
231
165
  */
232
166
  const climbsOut = (rel) => rel === '..' || rel.startsWith(`..${path.sep}`);
233
167
  const within = (folder, abs) => {
@@ -240,16 +174,9 @@ const withinOrIs = (folder, abs) => {
240
174
  };
241
175
 
242
176
  /**
243
- * The nearest ancestor of this path that is really on disk, with every link on
244
- * the way to it followed.
245
- *
246
- * THIS IS WHAT MAKES A DANGLING LINK SAFE TO JUDGE. A target that does not exist
247
- * cannot be resolved, and skipping the check for those left a hole: with a
248
- * TRACKED `bridge -> /somewhere/shared` in the commit, a carried
249
- * `node_modules/pkg/cache -> ../../bridge/newfile` looks like a path inside the
250
- * folder, dangles, and the first write through it creates a file in the shared
251
- * directory. The last component being absent says nothing about the ones before
252
- * it, so the ones before it are the ones resolved.
177
+ * The real path of the nearest ancestor on disk. This lets a dangling link be
178
+ * judged: with a tracked `bridge -> /shared`, a carried link to
179
+ * `../../bridge/newfile` dangles but would write into `/shared`.
253
180
  */
254
181
  function nearestReal(abs) {
255
182
  let at = abs;
@@ -257,10 +184,8 @@ function nearestReal(abs) {
257
184
  if (stillThere(at)) {
258
185
  try { return fs.realpathSync(at); }
259
186
  catch (error) {
260
- // It was there a moment ago, so the only ordinary answer left is that
261
- // it went away between the two calls. Anything else is a path this
262
- // process cannot follow, and a containment check skipped because the
263
- // answer was unreadable is a containment check that did not happen.
187
+ // ENOENT means it went away between the two calls. Any other error
188
+ // would skip the containment check, so it throws.
264
189
  if (error?.code === 'ENOENT') return null;
265
190
  throw Error(`${at} could not be followed: ${error.message}`);
266
191
  }
@@ -272,34 +197,22 @@ function nearestReal(abs) {
272
197
  }
273
198
 
274
199
  /**
275
- * A LINK OUT OF THE FOLDER IS NOT ISOLATION, IT IS THE OPPOSITE.
276
- *
277
- * Measured on one Mac, 2026-10-07: a hand-made worktree beside the checkout had
278
- * `node_modules -> ../agentbox-team/node_modules`, so an install in it rewrote
279
- * the dependencies of every other folder pointing at that target. Carrying such
280
- * a link forward recreates exactly that, in a folder whose whole purpose is that
281
- * nothing it does reaches anybody else.
200
+ * Links in a carried entry that would let the folder write somewhere else, such
201
+ * as `node_modules -> ../other-checkout/node_modules`, where an install rewrites
202
+ * another folder's dependencies.
282
203
  *
283
- * An ABSOLUTE link is never carried, even one pointing inside this very
284
- * checkout, because absolute is precisely what survives the move into place and
285
- * keeps pointing at the original. A RELATIVE one is carried when it still lands
286
- * inside the folder, which is what a workspace link is for: after the move it
287
- * points at the folder's own copy.
288
- *
289
- * EVERY LINK IN THE SUBTREE IS LOOKED AT, not just the top ones. One level of
290
- * children was tried first and is not enough: `node_modules/@scope/package` is
291
- * an ordinary layout, and a scope directory hides every link under it. `find`
292
- * does not follow links while walking, so a link cannot be used to escape the
293
- * walk either.
204
+ * An absolute link is never carried, even into this checkout, because after the
205
+ * copy it still points at the original. A relative one is carried when it lands
206
+ * inside the folder, as workspace links do. Every link in the subtree is checked,
207
+ * because `node_modules/@scope/` hides links a level down. `find` does not follow
208
+ * links while walking, so a link cannot lead the walk out.
294
209
  */
295
210
  function linksThatDoNotHold(root, folder, rel) {
296
211
  const to = path.join(folder, rel);
297
212
  let found = [];
298
213
  try {
299
214
  // `find <path>` on a path that is itself a link reports that link, so an
300
- // entry which IS a link is checked by the same walk as the links inside a
301
- // directory. That is not incidental: an entry that was a link got no
302
- // destination check at all while this gated on the entry being a directory.
215
+ // entry that is a link gets the same check as the links inside a directory.
303
216
  found = execFileSync('find', [to, '-type', 'l', '-print0'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
304
217
  .split('\0').filter(Boolean);
305
218
  } catch (error) {
@@ -312,16 +225,11 @@ function linksThatDoNotHold(root, folder, rel) {
312
225
  if (target === null) continue; // gone from under us, or no longer a link
313
226
  if (path.isAbsolute(target)) { bad.push({ path: name, why: 'it is a link to somewhere outside this folder' }); continue; }
314
227
  const resolved = path.resolve(path.dirname(link), target);
315
- // CONTAINMENT IS CHECKED WHETHER OR NOT ANYTHING IS THERE. A link pointing
316
- // out of the folder is a way out of the folder even while it dangles:
317
- // `node_modules/pkg/out -> ../../../w-neighbour/newfile` points into the
318
- // folder the NEXT TASK is working in, and the first thing written through it
319
- // lands in that task's work. Checking only when the target exists let that
320
- // through (Codex's review, 2026-10-07).
228
+ // Checked even when the link dangles: the first write through a dangling
229
+ // link to `../../../w-neighbour/newfile` lands in another task's folder.
321
230
  if (!within(folder, resolved)) { bad.push({ path: name, why: 'it is a link that leads out of this folder' }); continue; }
322
- // AND THE LINKS ON THE WAY TO IT, WHICH DANGLING DOES NOT EXCUSE. A path
323
- // inside the folder can still arrive outside it through a link on the way,
324
- // and the links on the way exist even when the last name does not.
231
+ // A path inside the folder can still arrive outside it through a link on
232
+ // the way, and those links exist even when the last name does not.
325
233
  const home = nearestReal(folder);
326
234
  const reached = nearestReal(resolved);
327
235
  if (home && reached && !withinOrIs(home, reached)) {
@@ -329,30 +237,20 @@ function linksThatDoNotHold(root, folder, rel) {
329
237
  continue;
330
238
  }
331
239
  if (fs.existsSync(resolved)) continue;
332
- // Dangling, and everything on the way to it is this folder's, so nothing it
333
- // reaches can be anybody else's. Only our business if the copy BROKE it,
334
- // which means the target was there in the checkout and was left behind.
240
+ // Dangling but contained. It is refused only if the copy broke it, meaning
241
+ // the target exists in the checkout and was not carried. A link already
242
+ // dangling in the checkout is left as it was.
335
243
  const sourceSide = path.resolve(path.dirname(path.join(root, name)), target);
336
244
  if (fs.existsSync(sourceSide)) bad.push({ path: name, why: 'it points at something that was not carried in with it' });
337
- // A link that was already dangling where it came from is left exactly as it
338
- // was found. It is the checkout's own state, not something this made.
339
245
  }
340
246
  return bad;
341
247
  }
342
248
 
343
249
  /**
344
- * Carry the selected local files into a folder that has just been made.
345
- *
346
- * `cp -c` asks APFS for a block-sharing clone: measured 2026-09-22, this
347
- * repository's 929 MB of dependencies took 2.9 seconds and moved the volume's
348
- * free space by nothing at all, because the blocks are shared until something
349
- * writes.
350
- *
351
- * A PATTERN THAT MATCHES NOTHING IS SILENT, and it is the only silence here: a
352
- * repository may name a `.env` that this machine does not have. Everything else
353
- * that will not or cannot be carried THROWS, with every path and reason in the
354
- * message, because the caller's only honest options are a complete folder or no
355
- * folder, and a list of reasons is what makes the second one actionable.
250
+ * Carries the selected local files into a folder that has just been made.
251
+ * A pattern that matches nothing is fine, since a repository may name a `.env`
252
+ * this machine lacks. Anything else that cannot be carried throws, with every
253
+ * path and reason, so the caller gets a complete folder or none.
356
254
  */
357
255
  export function carryLocalFiles(root, folder) {
358
256
  const { paths, source } = localFilesIn(root);
@@ -365,13 +263,9 @@ export function carryLocalFiles(root, folder) {
365
263
  const from = path.join(root, rel);
366
264
  const to = path.join(folder, rel);
367
265
  if (!fs.existsSync(from) && !isLink(from)) continue; // it went away; silent
368
- // IGNORED BY THE REPOSITORY ITSELF, on both sides. Being named in
369
- // `.worktreeinclude` is not enough: an untracked file the repository does not
370
- // ignore stays untracked in the folder, and closing the task commits it.
371
266
  if (!theirs.has(rel)) { no(rel, 'the repository does not ignore it, so carrying it in could commit it'); continue; }
372
- // NEVER OVER THE COMMIT. The patterns cannot select tracked content in the
373
- // checkout beside us, but the folder may stand on a DIFFERENT commit, and a
374
- // path this branch tracks is content rather than a local file.
267
+ // The folder may be on a different commit from the checkout, where this
268
+ // path is tracked.
375
269
  if (tracksIt(folder, rel)) { no(rel, 'this branch tracks it'); continue; }
376
270
  if (reachedThroughALink(folder, rel)) { no(rel, 'a link stands where it would have to be written'); continue; }
377
271
  if (fs.existsSync(to)) continue; // already there; silent
@@ -383,13 +277,10 @@ export function carryLocalFiles(root, folder) {
383
277
  no(rel, `it could not be copied: ${String(error?.stderr ?? error?.message ?? '').trim()}`);
384
278
  continue;
385
279
  }
386
- // AND IGNORED HERE TOO, ASKED NOW IT IS HERE. This cannot be asked before
387
- // the copy: `node_modules/` with a trailing slash is the way nearly every
388
- // repository writes it, and a trailing slash matches directories only, so
389
- // git cannot match a path that is not on disk yet and answers "not ignored"
390
- // for every one of them. Asked after, the answer is the real one. A file the
391
- // folder's own branch would not ignore is taken straight back out, because
392
- // closing the task runs `git add -A` and would commit it.
280
+ // Asked only after the copy: a trailing-slash pattern like `node_modules/`
281
+ // matches directories only, so git answers "not ignored" for a path not on
282
+ // disk yet. What the folder's branch does not ignore is removed again,
283
+ // because closing the task runs `git add -A`.
393
284
  if (!ignoredIn(folder, [rel]).has(rel)) {
394
285
  try { fs.rmSync(to, { recursive: true, force: true }); } catch { /* best effort */ }
395
286
  no(rel, 'the branch in this folder does not ignore it, so closing the task would commit it');
@@ -398,14 +289,8 @@ export function carryLocalFiles(root, folder) {
398
289
  carried.push(rel);
399
290
  }
400
291
 
401
- // EVERY COPY FIRST, AND ONLY THEN THE LINKS, because a link is a statement
402
- // about the finished folder. Checking each entry as it landed refused a
403
- // perfectly good repository: with `node_modules` and `packages` both asked for
404
- // and `node_modules/thing -> ../packages/thing`, git lists `node_modules`
405
- // first, so the link was judged against a folder that did not have `packages`
406
- // in it yet, found dangling, and refused -- and nothing ever cleared that when
407
- // `packages` arrived a moment later. No task in such a repository could have
408
- // started (Codex's review, 2026-10-07).
292
+ // Links are judged only after every copy. `node_modules/thing ->
293
+ // ../packages/thing` dangles until `packages`, listed later, is copied.
409
294
  for (const rel of carried) refused.push(...linksThatDoNotHold(root, folder, rel));
410
295
 
411
296
  if (refused.length) {
@@ -416,10 +301,9 @@ export function carryLocalFiles(root, folder) {
416
301
  }
417
302
 
418
303
  /**
419
- * An entry that is itself a link, judged where it came FROM: relative, and
420
- * landing back inside the repository. The same link is judged again in the
421
- * folder once everything has been copied, which is the check that matters;
422
- * this one keeps an obviously shared one from being copied at all.
304
+ * An entry that is itself a link, judged in the checkout before copying. The
305
+ * check that matters runs in the folder after every copy; this one keeps an
306
+ * obviously shared link from being copied at all.
423
307
  */
424
308
  function carriableLink(root, abs) {
425
309
  const target = linkTarget(abs);
@@ -438,18 +322,9 @@ const tracksIt = (folder, rel) => {
438
322
  };
439
323
 
440
324
  /**
441
- * THE FOLDER IS THE COMMIT, PLUS WHAT WAS ASKED FOR, AND NOTHING ELSE.
442
- *
443
- * The clone copies whole top-level directories, so an ignored build output
444
- * living inside a tracked one (`renderer/dist`) rides along. That used to be the
445
- * difference between the two ways of making a folder: the clone carried every
446
- * untracked file in the checkout and the ordinary checkout carried one. Two
447
- * folders that are not the same thing is the bug underneath "worktrees don't
448
- * work", so the extras are cleared and the asked-for files are put back by name.
449
- *
450
- * Run BEFORE anything is carried in, so it can never remove what was asked for.
451
- * A failure throws: leaving an extra behind would quietly hand one folder
452
- * something another did not get, which is the difference being removed.
325
+ * Clears untracked files a clone brought along, such as `renderer/dist` inside
326
+ * a tracked top-level directory, so a cloned folder matches a checked-out one.
327
+ * Runs before anything is carried in, so it never removes what was asked for.
453
328
  */
454
329
  export function onlyTrackedContent(folder) {
455
330
  let found;
@@ -460,9 +335,8 @@ export function onlyTrackedContent(folder) {
460
335
  if (!keepable(rel)) continue;
461
336
  const at = path.join(folder, rel);
462
337
  try { fs.rmSync(at, { recursive: true, force: true }); }
463
- // `lstat` and not `existsSync`: existsSync follows a link, so a DANGLING
464
- // link still sitting there reads as absent and the failure to remove it
465
- // would be reported as a success.
338
+ // `lstat`, because existsSync follows links and would report a dangling
339
+ // link that failed to be removed as gone.
466
340
  catch (error) { if (stillThere(at)) throw Error(`${rel} came with the clone and could not be cleared: ${error.message}`); }
467
341
  gone.push(rel);
468
342
  }
@@ -470,15 +344,12 @@ export function onlyTrackedContent(folder) {
470
344
  }
471
345
 
472
346
  /**
473
- * The top-level entries the clone has to copy: the ones holding tracked content.
474
- * Everything else in the checkout is either ignored or somebody's scratch, and
475
- * whichever of it is wanted arrives by name through `.worktreeinclude`.
347
+ * The top-level entries holding tracked content, which the clone copies.
348
+ * Anything else wanted arrives by name through `.worktreeinclude`.
476
349
  *
477
- * A SUBMODULE STOPS THE CLONE ALTOGETHER. `ls-files` reports one as mode 160000,
478
- * a pointer rather than a directory of files, and copying whatever is on disk
479
- * there hands the folder another repository's administration rather than its own.
480
- * `git worktree move` also refuses a worktree holding one. The ordinary checkout
481
- * knows what a submodule is, so a repository with any is left to it.
350
+ * A submodule (mode 160000) stops the clone: copying it would hand the folder
351
+ * another repository's git data, and `git worktree move` refuses a worktree
352
+ * holding one. A plain `git worktree add` handles submodules.
482
353
  */
483
354
  export function trackedTopLevel(root) {
484
355
  let listed;
@@ -12,7 +12,7 @@ import path from 'node:path';
12
12
  import crypto from 'node:crypto';
13
13
  import { readIndex, readCreationContent, saveCreation, setCreationStatus, CREATION_STATUSES } from '../../main/store/project.mjs';
14
14
  import { runForProject } from '../../main/store/queues.mjs';
15
- import { assertInside, deniesGitOrEnvPath } from '../../main/store/paths.mjs';
15
+ import { assertInside, deniesGitOrEnvPath, deniesMachineryPath } from '../../main/store/paths.mjs';
16
16
  import { AGENT_CREATION_KINDS, SCHEMA_VERSION } from '../../shared/contracts.mjs';
17
17
  import { libraryFolderOf } from '../../shared/library-folder.mjs';
18
18
  import { resolveProduct } from './account.mjs';
@@ -142,6 +142,7 @@ export function readDocument(ref, { id = null, path: relPath = null } = {}) {
142
142
  // are what make an agent-supplied path safe to open at all.
143
143
  if (id) throw new Error(`no document with id ${id} in ${project.name}`);
144
144
  if (deniesGitOrEnvPath(relPath)) throw new Error(`refusing to read a .git/.env path: ${relPath}`);
145
+ if (deniesMachineryPath(relPath)) throw new Error(`refusing to read a reserved store path: ${relPath}`);
145
146
  let content;
146
147
  try {
147
148
  content = fs.readFileSync(assertInside(project.dir, relPath), 'utf8');
@@ -181,6 +182,7 @@ export async function writeDocument(ref, { id = null, path: relPath = null, titl
181
182
  const nextPath = relPath ?? existing?.path;
182
183
  if (!nextPath) throw new Error('a new document needs a path, e.g. "reports/first-run.md" or "personas/marcus.md"');
183
184
  if (deniesGitOrEnvPath(nextPath)) throw new Error(`refusing to write a .git/.env path: ${nextPath}`);
185
+ if (deniesMachineryPath(nextPath)) throw new Error(`refusing to write a reserved store path: ${nextPath}`);
184
186
 
185
187
  // THE PATH ALREADY SAYS WHAT THIS IS, so a caller that did not name a kind
186
188
  // gets the one its extension implies rather than a blanket 'markdown'. Two
@@ -0,0 +1,88 @@
1
+ // The standing instruction behind a repeating task, for the agent on one of its
2
+ // runs.
3
+ //
4
+ // WHICH RUN THE SESSION IS ON COMES FROM THE APP, NOT FROM THE MODEL.
5
+ // `ZERO_ITEM` and `ZERO_PRODUCT` are written into a worker's environment when
6
+ // the app spawns it (main/supervisor.mjs), which is why no argument below names
7
+ // the task: a session reaches the rule it is standing on or no rule at all.
8
+ //
9
+ // AND ONLY THE WORDS, THOUGH `patchRule` TAKES ANYTHING. The app gates `engine`
10
+ // and `model` at the door (`engineOffered`, `modelOffered` in main/ipc.mjs) so a
11
+ // rule cannot be marked for a coding agent the machine never offered; this
12
+ // server cannot ask that question, so it does not accept the answer. The hour
13
+ // and the day stay on the person's own screen.
14
+
15
+ import { Repeats } from '../../main/repeats.mjs';
16
+ import { readWorkItem } from '../../main/store/work-items.mjs';
17
+ import { listProjects } from '../../main/store/project.mjs';
18
+ import { nextRunAt, ruleIdOf, ruleLabel, standingInstruction } from '../../shared/repeats.mjs';
19
+ import { resolveProduct } from './account.mjs';
20
+
21
+ const NO_THREAD = 'this session is not on a thread, so there is no repeating task behind it';
22
+
23
+ // Read and patch only, so the store modules `serve` needs are never built here:
24
+ // nothing below creates a work item or takes the project lock.
25
+ const rules = new Repeats(null);
26
+
27
+ function projectOf(itemId, product) {
28
+ if (product) return resolveProduct(product);
29
+ for (const p of listProjects()) {
30
+ try { if (readWorkItem(p.dir, itemId)) return p; } catch { /* a product with no ledger */ }
31
+ }
32
+ throw new Error(`no thread ${itemId} in any product`);
33
+ }
34
+
35
+ /** The rule this session's run belongs to, or a sentence saying why there is none. */
36
+ function ruleBehindThisRun({ env = process.env, now = Date.now() } = {}) {
37
+ const itemId = String(env.ZERO_ITEM ?? '').trim();
38
+ if (!itemId) throw new Error(NO_THREAD);
39
+
40
+ const project = projectOf(itemId, String(env.ZERO_PRODUCT ?? '').trim() || null);
41
+ const item = readWorkItem(project.dir, itemId, now);
42
+ if (!item) throw new Error(`no thread ${itemId} in ${project.name}`);
43
+
44
+ const ruleId = ruleIdOf(item);
45
+ if (!ruleId) {
46
+ throw new Error(`${itemId} is not a run of a repeating task, so it has no standing instruction to change`);
47
+ }
48
+ const rule = rules.get(project.dir, ruleId, now);
49
+ if (!rule) throw new Error(`${itemId} names the repeating task ${ruleId}, which is no longer on file`);
50
+ return { project, item, ruleId, rule };
51
+ }
52
+
53
+ function asAnswer({ project, item, ruleId, rule }, now) {
54
+ return {
55
+ repeat: ruleId,
56
+ product: project.id,
57
+ run: item.id,
58
+ label: rule.title,
59
+ instruction: standingInstruction(rule),
60
+ schedule: ruleLabel(rule),
61
+ nextRun: nextRunAt(rule, now),
62
+ ended: !!rule.endedAt,
63
+ };
64
+ }
65
+
66
+ /** What every run after this one is told, as it stands. */
67
+ export function readStandingInstruction({ env = process.env, now = Date.now() } = {}) {
68
+ return asAnswer(ruleBehindThisRun({ env, now }), now);
69
+ }
70
+
71
+ /**
72
+ * Change it. The whole instruction, replaced, because that is the operation the
73
+ * person's own screen performs and the only one that can also take a sentence
74
+ * back out.
75
+ */
76
+ export function setStandingInstruction({ instruction }, { env = process.env, now = Date.now() } = {}) {
77
+ const words = typeof instruction === 'string' ? instruction.trim() : '';
78
+ if (!words) throw new Error('a repeating task needs an instruction; send the whole of it, not an empty one');
79
+
80
+ const found = ruleBehindThisRun({ env, now });
81
+ if (found.rule.endedAt) throw new Error(`the repeating task ${found.ruleId} was ended, so it has no next run to change`);
82
+
83
+ // Only the body. The title is a label the app derived from what the person
84
+ // typed, and a line added to a standing instruction is not a new name for the
85
+ // task.
86
+ const rule = rules.patchRule(found.project.dir, found.ruleId, { body: words }, now);
87
+ return asAnswer({ ...found, rule }, now);
88
+ }