agentbox-app 0.1.11 → 0.1.13

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 (239) hide show
  1. package/README.md +24 -16
  2. package/bin/agentbox.mjs +18 -9
  3. package/briefs/message-rules.md +15 -4
  4. package/main/account-tooling.mjs +2 -1
  5. package/main/acp.mjs +296 -0
  6. package/main/agent-homes.mjs +158 -0
  7. package/main/agent-sessions.mjs +57 -20
  8. package/main/agent-updates.mjs +9 -20
  9. package/main/analytics.mjs +30 -0
  10. package/main/approvals.mjs +14 -5
  11. package/main/claude-bin.mjs +45 -310
  12. package/main/claude-input.mjs +9 -3
  13. package/main/claude-launch-env.mjs +54 -0
  14. package/main/claude-models.mjs +39 -7
  15. package/main/claude-plan.mjs +14 -2
  16. package/main/claude-usage.mjs +82 -25
  17. package/main/codex-bin.mjs +51 -248
  18. package/main/codex-launch-env.mjs +31 -0
  19. package/main/codex-session.mjs +38 -3
  20. package/main/codex-threads.mjs +41 -8
  21. package/main/codex-watch.mjs +67 -3
  22. package/main/config.mjs +40 -13
  23. package/main/corner-tag.mjs +489 -0
  24. package/main/crash-report.mjs +26 -3
  25. package/main/demo.mjs +17 -14
  26. package/main/engine-bin.mjs +595 -0
  27. package/main/engine-setup.mjs +27 -27
  28. package/main/feedback.mjs +11 -6
  29. package/main/fresh-user.mjs +1 -1
  30. package/main/git-change.mjs +8 -2
  31. package/main/harnesses/claude-stream.mjs +208 -0
  32. package/main/harnesses/claude.mjs +84 -0
  33. package/main/harnesses/codex.mjs +65 -0
  34. package/main/harnesses/common.mjs +78 -0
  35. package/main/harnesses/instructions-file.mjs +116 -0
  36. package/main/harnesses/opencode.mjs +234 -0
  37. package/main/harnesses.mjs +65 -0
  38. package/main/importable-threads.mjs +38 -0
  39. package/main/instruction-settings.mjs +47 -11
  40. package/main/ipc.mjs +175 -22
  41. package/main/local-app-frames.mjs +82 -0
  42. package/main/machine.mjs +1 -1
  43. package/main/main.mjs +120 -5
  44. package/main/memory-gate-history.mjs +51 -12
  45. package/main/memory-gate-server.mjs +67 -11
  46. package/main/memory-gate.mjs +49 -8
  47. package/main/memory-log.mjs +124 -0
  48. package/main/notify.mjs +48 -5
  49. package/main/opencode-server.mjs +271 -0
  50. package/main/opencode.mjs +287 -0
  51. package/main/phone-link.mjs +551 -0
  52. package/main/refresh-agent-models.mjs +2 -1
  53. package/main/relay-address.mjs +28 -0
  54. package/main/relay-client.mjs +246 -0
  55. package/main/row-label.mjs +8 -7
  56. package/main/runs-on.mjs +90 -0
  57. package/main/serve.mjs +124 -10
  58. package/main/settings.mjs +171 -11
  59. package/main/shell-path.mjs +86 -0
  60. package/main/ship-queue.mjs +25 -7
  61. package/main/sign-in-files.mjs +3 -7
  62. package/main/source-updater.mjs +27 -1
  63. package/main/start-hold.mjs +73 -0
  64. package/main/store/home.mjs +37 -5
  65. package/main/store/project.mjs +15 -19
  66. package/main/store.mjs +10 -4
  67. package/main/supervisor.mjs +1030 -447
  68. package/main/task-commands.mjs +13 -17
  69. package/main/task-folders-offthread.mjs +6 -1
  70. package/main/task-folders.mjs +491 -82
  71. package/main/task-terminals.mjs +49 -8
  72. package/main/team/index.mjs +2 -2
  73. package/main/team/projects.mjs +1 -1
  74. package/main/updater.mjs +71 -9
  75. package/main/worktree-include.mjs +494 -0
  76. package/mcp/core/page.mjs +1 -1
  77. package/mcp/core/work.mjs +34 -3
  78. package/mcp/tools.mjs +2 -1
  79. package/package.json +10 -1
  80. package/renderer/dist/assets/abnfDiagram-YUKMZFIV-DPPuMWt-.js +1 -0
  81. package/renderer/dist/assets/arc-HmFfdseH.js +1 -0
  82. package/renderer/dist/assets/architectureDiagram-47P4ROYG-DxJ3zHrH.js +36 -0
  83. package/renderer/dist/assets/blockDiagram-E7TT5TSB-CbTXzOqQ.js +140 -0
  84. package/renderer/dist/assets/c4Diagram-CRA5TL53-X5ItFyLn.js +38 -0
  85. package/renderer/dist/assets/channel-BlT2g9Sz.js +1 -0
  86. package/renderer/dist/assets/chunk-2Q5K7J3B-DC12Ho_L.js +1 -0
  87. package/renderer/dist/assets/chunk-5VM5RSS4-ClVtfPd0.js +15 -0
  88. package/renderer/dist/assets/chunk-7TKQ45FW-DgcOm0HT.js +1 -0
  89. package/renderer/dist/assets/chunk-AW2ZBBNX-C01s4yoF.js +88 -0
  90. package/renderer/dist/assets/chunk-FVRAUYC3-Bj0zxhR4.js +2 -0
  91. package/renderer/dist/assets/chunk-HTAEGDNF-DWDeEhLU.js +1 -0
  92. package/renderer/dist/assets/chunk-JWPE2WC7-Dsqwv3eW.js +1 -0
  93. package/renderer/dist/assets/chunk-KQW6MTUR-CE86ANL2.js +1 -0
  94. package/renderer/dist/assets/chunk-XXDRQBXY-CoNTASfs.js +1 -0
  95. package/renderer/dist/assets/classDiagram-v2-K4WV4PDN-2wprLqtw.js +217 -0
  96. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +1 -0
  97. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +1 -0
  98. package/renderer/dist/assets/cose-bilkent-JH36ORCC-enqMiYbn.js +1 -0
  99. package/renderer/dist/assets/cynefin-EF2NZ3EQ-DHngLxvQ.js +141 -0
  100. package/renderer/dist/assets/cynefinDiagram-3GCD6N5R-C0rZZIl3.js +62 -0
  101. package/renderer/dist/assets/cytoscape.esm-C5JX3QLV.js +331 -0
  102. package/renderer/dist/assets/dagre-W4DXFKR2-DQnaEWO7.js +4 -0
  103. package/renderer/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  104. package/renderer/dist/assets/diagram-2UJZ2QOL-CS8AlGPx.js +200 -0
  105. package/renderer/dist/assets/diagram-OVF4WLC6-yiDMHIkY.js +24 -0
  106. package/renderer/dist/assets/diagram-PFPMY2P6-XbQKzbBA.js +30 -0
  107. package/renderer/dist/assets/diagram-UMYRVEAY-dhVvlfl_.js +24 -0
  108. package/renderer/dist/assets/diagram-YEKJPTXX-DZHe8SCf.js +41 -0
  109. package/renderer/dist/assets/diagram-ZIFT7M5P-B5h6aXVR.js +3 -0
  110. package/renderer/dist/assets/ebnfDiagram-VR2GEFS7-PR9ScDco.js +1 -0
  111. package/renderer/dist/assets/elk-IJKZMXRS-Cg-yQXPd.js +27 -0
  112. package/renderer/dist/assets/erDiagram-O2IAPWRE-omOrJMxu.js +99 -0
  113. package/renderer/dist/assets/flowDiagram-OXPTDLAJ-aOX2hzvT.js +213 -0
  114. package/renderer/dist/assets/ganttDiagram-R7TSEDQI-B1H3pWyI.js +292 -0
  115. package/renderer/dist/assets/gitGraphDiagram-XJZIOB7I-Cvw304HC.js +106 -0
  116. package/renderer/dist/assets/graph-DGbsTEsI.js +1 -0
  117. package/renderer/dist/assets/infoDiagram-5W2HQ5XZ-_ZXcNViH.js +2 -0
  118. package/renderer/dist/assets/init-Gi6I4Gst.js +1 -0
  119. package/renderer/dist/assets/ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js +70 -0
  120. package/renderer/dist/assets/journeyDiagram-COXZFDF6-fr62QoAt.js +139 -0
  121. package/renderer/dist/assets/kanban-definition-P3RFRI5V-J8zsabCe.js +89 -0
  122. package/renderer/dist/assets/katex-C5jXJg4s.js +257 -0
  123. package/renderer/dist/assets/linear-BmNdEtIi.js +1 -0
  124. package/renderer/dist/assets/main-ChNdnUvt.js +1132 -0
  125. package/renderer/dist/assets/main-nj0QR5kg.css +32 -0
  126. package/renderer/dist/assets/mermaid.core-CnYzvFkz.js +314 -0
  127. package/renderer/dist/assets/mindmap-definition-LZFPQGKD-Bfcxc7PM.js +96 -0
  128. package/renderer/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  129. package/renderer/dist/assets/pegDiagram-BGZESJAR-CwUJOq6X.js +1 -0
  130. package/renderer/dist/assets/pieDiagram-CAPJLFHJ-BrwMmqfC.js +39 -0
  131. package/renderer/dist/assets/product-name-CTKLYNWY.js +40 -0
  132. package/renderer/dist/assets/quadrantDiagram-GDMTTRAM-BN4Stoj-.js +7 -0
  133. package/renderer/dist/assets/railroadDiagram-SM67HX2B-CPZ5bbeR.js +1 -0
  134. package/renderer/dist/assets/requirementDiagram-X7JNWC4A-CgRnimVe.js +84 -0
  135. package/renderer/dist/assets/sankeyDiagram-UM26HJRW-Bt2rej__.js +40 -0
  136. package/renderer/dist/assets/sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js +169 -0
  137. package/renderer/dist/assets/sizeCapture-INFHLROL-usyNyXyh.js +1 -0
  138. package/renderer/dist/assets/stateDiagram-v2-TBUQTH76-CRzUBb0u.js +291 -0
  139. package/renderer/dist/assets/swimlanes-N4OXWK64-C1GJvMS0.js +1 -0
  140. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +8 -0
  141. package/renderer/dist/assets/timeline-definition-YOQAKGHF-caeyPlcc.js +120 -0
  142. package/renderer/dist/assets/usecaseDiagram-VIAY4XPW-BN_SV6PC.js +370 -0
  143. package/renderer/dist/assets/vennDiagram-BLWOH2XV-BLGuaUX-.js +34 -0
  144. package/renderer/dist/assets/wardleyDiagram-YMQ3BMBF-Da0cJeNc.js +78 -0
  145. package/renderer/dist/assets/xychartDiagram-TAQBALBS-cGfUffMk.js +7 -0
  146. package/renderer/dist/corner-tag.html +13 -0
  147. package/renderer/dist/emulator/audio-rules.mjs +74 -0
  148. package/renderer/dist/emulator/data/compression/README.md +11 -0
  149. package/renderer/dist/emulator/data/compression/extract7z.js +1 -0
  150. package/renderer/dist/emulator/data/compression/extractzip.js +1 -0
  151. package/renderer/dist/emulator/data/compression/libunrar.js +519 -0
  152. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  153. package/renderer/dist/emulator/data/cores/README.md +22 -0
  154. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  155. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  156. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  157. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  158. package/renderer/dist/emulator/data/cores/package.json +65 -0
  159. package/renderer/dist/emulator/data/cores/reports/mgba.json +1 -0
  160. package/renderer/dist/emulator/data/emulator.css +1614 -0
  161. package/renderer/dist/emulator/data/loader.js +170 -0
  162. package/renderer/dist/emulator/data/localization/README.md +63 -0
  163. package/renderer/dist/emulator/data/localization/af-FR.json +339 -0
  164. package/renderer/dist/emulator/data/localization/ar-AR.json +301 -0
  165. package/renderer/dist/emulator/data/localization/ben-BEN.json +301 -0
  166. package/renderer/dist/emulator/data/localization/de-GER.json +301 -0
  167. package/renderer/dist/emulator/data/localization/el-GR.json +301 -0
  168. package/renderer/dist/emulator/data/localization/en-US.json +339 -0
  169. package/renderer/dist/emulator/data/localization/es-ES.json +310 -0
  170. package/renderer/dist/emulator/data/localization/fa-AF.json +310 -0
  171. package/renderer/dist/emulator/data/localization/hi-HI.json +301 -0
  172. package/renderer/dist/emulator/data/localization/it-IT.json +302 -0
  173. package/renderer/dist/emulator/data/localization/ja-JA.json +301 -0
  174. package/renderer/dist/emulator/data/localization/jv-JV.json +301 -0
  175. package/renderer/dist/emulator/data/localization/ko-KO.json +336 -0
  176. package/renderer/dist/emulator/data/localization/pt-BR.json +361 -0
  177. package/renderer/dist/emulator/data/localization/retroarch.json +617 -0
  178. package/renderer/dist/emulator/data/localization/ro-RO.json +310 -0
  179. package/renderer/dist/emulator/data/localization/ru-RU.json +301 -0
  180. package/renderer/dist/emulator/data/localization/tr-TR.json +310 -0
  181. package/renderer/dist/emulator/data/localization/vi-VN.json +302 -0
  182. package/renderer/dist/emulator/data/localization/zh-CN.json +351 -0
  183. package/renderer/dist/emulator/data/src/GameManager.js +464 -0
  184. package/renderer/dist/emulator/data/src/compression.js +142 -0
  185. package/renderer/dist/emulator/data/src/emulator.js +6158 -0
  186. package/renderer/dist/emulator/data/src/gamepad.js +167 -0
  187. package/renderer/dist/emulator/data/src/nipplejs.js +1 -0
  188. package/renderer/dist/emulator/data/src/shaders.js +236 -0
  189. package/renderer/dist/emulator/data/src/socket.io.min.js +7 -0
  190. package/renderer/dist/emulator/data/src/storage.js +125 -0
  191. package/renderer/dist/emulator/data/version.json +5 -0
  192. package/renderer/dist/emulator/host.js +297 -0
  193. package/renderer/dist/emulator/index.html +28 -0
  194. package/renderer/dist/emulator/provenance.json +6 -0
  195. package/renderer/dist/index.html +16 -3
  196. package/scripts/after-install.mjs +14 -0
  197. package/scripts/approval-server.sh +31 -7
  198. package/scripts/before-publish.mjs +12 -10
  199. package/shared/agent-import.mjs +3 -1
  200. package/shared/agents.d.mts +1 -0
  201. package/shared/bridge-map.mjs +19 -0
  202. package/shared/claude-commands.generated.mjs +8 -6
  203. package/shared/claude-models.generated.mjs +10 -8
  204. package/shared/codex-modes.mjs +1 -1
  205. package/shared/corner-tag.d.mts +14 -0
  206. package/shared/corner-tag.mjs +141 -0
  207. package/shared/demo-world.mjs +1 -1
  208. package/shared/engines.mjs +20 -5
  209. package/shared/fresh-user-home.mjs +12 -1
  210. package/shared/harness-definitions.mjs +103 -0
  211. package/shared/instruction-defaults.json +1 -1
  212. package/shared/notify-rules.d.mts +25 -0
  213. package/shared/notify-rules.mjs +19 -0
  214. package/shared/project-folder-check.mjs +3 -3
  215. package/shared/provider-commands.mjs +10 -2
  216. package/shared/rank.d.mts +2 -0
  217. package/shared/rank.mjs +50 -0
  218. package/shared/relay-address.mjs +6 -0
  219. package/shared/relay-crypto.mjs +165 -0
  220. package/shared/runs-on.d.mts +10 -0
  221. package/shared/runs-on.mjs +55 -0
  222. package/shared/shortcuts.mjs +11 -5
  223. package/shared/spawn-trouble.mjs +58 -7
  224. package/shared/terminal-input.d.mts +1 -0
  225. package/shared/terminal-input.mjs +30 -0
  226. package/shared/terminal-keys.d.mts +1 -0
  227. package/shared/terminal-keys.mjs +16 -0
  228. package/shared/terminal-size.d.mts +4 -0
  229. package/shared/terminal-size.mjs +29 -0
  230. package/shared/usage.d.mts +3 -0
  231. package/shared/usage.mjs +7 -0
  232. package/shared/where-it-runs-from.d.mts +43 -0
  233. package/shared/where-it-runs-from.mjs +155 -0
  234. package/shared/work-items.mjs +28 -2
  235. package/shared/work-lines.mjs +1 -1
  236. package/shared/xterm-invalid-range.d.mts +2 -0
  237. package/shared/xterm-invalid-range.mjs +58 -0
  238. package/renderer/dist/assets/index-B-2Az952.css +0 -32
  239. package/renderer/dist/assets/index-D6qncgai.js +0 -1168
@@ -0,0 +1,494 @@
1
+ // THE LOCAL FILES A NEW TASK FOLDER IS GIVEN, AND THE ONES IT IS REFUSED.
2
+ //
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.
10
+ //
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.
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.
38
+ import { execFileSync } from 'node:child_process';
39
+ import fs from 'node:fs';
40
+ import os from 'node:os';
41
+ import path from 'node:path';
42
+
43
+ /** The file a repository says what it needs in, at its root. */
44
+ export const INCLUDE_FILE = '.worktreeinclude';
45
+
46
+ /**
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.
58
+ */
59
+ export function copyTree(from, to) {
60
+ const args = process.platform === 'darwin'
61
+ ? ['-c', '-R', from, to]
62
+ : ['-a', '--reflink=auto', from, to];
63
+ execFileSync('cp', args, { stdio: ['ignore', 'ignore', 'pipe'] });
64
+ }
65
+
66
+ /**
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.
76
+ */
77
+ export const DEFAULT_PATTERNS = ['/node_modules'];
78
+
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
+ */
85
+ const MANAGED = '.claude/worktrees';
86
+
87
+ const git = (cwd, args, input) => execFileSync('git', args, {
88
+ cwd, input, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'],
89
+ });
90
+
91
+ /** Lines that are a pattern: not blank, not a comment. */
92
+ const patternsIn = (text) => text.split('\n')
93
+ .map((line) => line.replace(/\r$/, ''))
94
+ .filter((line) => line.trim() !== '' && !line.trimStart().startsWith('#'));
95
+
96
+ /**
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.
107
+ */
108
+ export function askedFor(root) {
109
+ const file = path.join(root, INCLUDE_FILE);
110
+ if (!fs.existsSync(file)) return { patterns: DEFAULT_PATTERNS, source: 'the default list' };
111
+ try { return { patterns: patternsIn(fs.readFileSync(file, 'utf8')), source: INCLUDE_FILE }; }
112
+ catch (error) { throw Error(`${INCLUDE_FILE} is there and could not be read: ${error.message}`); }
113
+ }
114
+
115
+ /**
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.
122
+ */
123
+ export function localFilesIn(root) {
124
+ const { patterns, source } = askedFor(root);
125
+ if (!patterns.length) return { paths: [], patterns, source };
126
+ const held = fs.mkdtempSync(path.join(os.tmpdir(), 'worktree-include-'));
127
+ const file = path.join(held, 'patterns');
128
+ try {
129
+ fs.writeFileSync(file, `${patterns.join('\n')}\n`);
130
+ const out = git(root, ['ls-files', '-z', '--others', '--ignored', '--directory', `--exclude-from=${file}`]);
131
+ const paths = out.split('\0')
132
+ .map((p) => p.replace(/\/$/, ''))
133
+ .filter(Boolean)
134
+ .filter(keepable);
135
+ return { paths, patterns, source };
136
+ } catch (error) {
137
+ throw Error(`${source} could not be read against this checkout: ${String(error?.stderr ?? error?.message ?? '').trim()}`);
138
+ } finally {
139
+ try { fs.rmSync(held, { recursive: true, force: true }); } catch { /* a temp dir */ }
140
+ }
141
+ }
142
+
143
+ /**
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.
152
+ */
153
+ function keepable(rel) {
154
+ if (rel.split('/')[0] === '.git') return false;
155
+ if (rel === MANAGED || rel.startsWith(`${MANAGED}/`)) return false;
156
+ if (MANAGED.startsWith(`${rel}/`)) return false;
157
+ return true;
158
+ }
159
+
160
+ /**
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.
165
+ */
166
+ function ignoredIn(cwd, paths) {
167
+ if (!paths.length) return new Set();
168
+ try {
169
+ const out = git(cwd, ['check-ignore', '-z', '--stdin'], `${paths.join('\0')}\0`);
170
+ return new Set(out.split('\0').filter(Boolean));
171
+ } catch (error) {
172
+ if (error?.status === 1) return new Set();
173
+ throw Error(`git could not say what this checkout ignores: ${String(error?.stderr ?? error?.message ?? '').trim()}`);
174
+ }
175
+ }
176
+
177
+ /**
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.
188
+ */
189
+ function lookAt(p) {
190
+ try { return fs.lstatSync(p); }
191
+ catch (error) {
192
+ if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') return null;
193
+ throw Error(`${p} could not be looked at: ${error.message}`);
194
+ }
195
+ }
196
+
197
+ const isLink = (p) => lookAt(p)?.isSymbolicLink() === true;
198
+ /** On disk at all, including a link with nothing at the end of it. */
199
+ const stillThere = (p) => lookAt(p) !== null;
200
+
201
+ /**
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.
206
+ */
207
+ function linkTarget(p) {
208
+ try { return fs.readlinkSync(p); }
209
+ catch (error) {
210
+ if (['ENOENT', 'ENOTDIR', 'EINVAL'].includes(error?.code)) return null;
211
+ throw Error(`${p} is a link whose target could not be read: ${error.message}`);
212
+ }
213
+ }
214
+
215
+ /** Is any directory on the way to this path a link? Then nothing may be written through it. */
216
+ function reachedThroughALink(folder, rel) {
217
+ let at = folder;
218
+ for (const part of rel.split('/').slice(0, -1)) {
219
+ at = path.join(at, part);
220
+ if (isLink(at)) return true;
221
+ }
222
+ return false;
223
+ }
224
+
225
+ /**
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).
231
+ */
232
+ const climbsOut = (rel) => rel === '..' || rel.startsWith(`..${path.sep}`);
233
+ const within = (folder, abs) => {
234
+ const rel = path.relative(folder, abs);
235
+ return rel !== '' && !climbsOut(rel) && !path.isAbsolute(rel);
236
+ };
237
+ const withinOrIs = (folder, abs) => {
238
+ const rel = path.relative(folder, abs);
239
+ return !climbsOut(rel) && !path.isAbsolute(rel);
240
+ };
241
+
242
+ /**
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.
253
+ */
254
+ function nearestReal(abs) {
255
+ let at = abs;
256
+ for (;;) {
257
+ if (stillThere(at)) {
258
+ try { return fs.realpathSync(at); }
259
+ 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.
264
+ if (error?.code === 'ENOENT') return null;
265
+ throw Error(`${at} could not be followed: ${error.message}`);
266
+ }
267
+ }
268
+ const up = path.dirname(at);
269
+ if (up === at) return null;
270
+ at = up;
271
+ }
272
+ }
273
+
274
+ /**
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.
282
+ *
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.
294
+ */
295
+ function linksThatDoNotHold(root, folder, rel) {
296
+ const to = path.join(folder, rel);
297
+ let found = [];
298
+ try {
299
+ // `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.
303
+ found = execFileSync('find', [to, '-type', 'l', '-print0'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })
304
+ .split('\0').filter(Boolean);
305
+ } catch (error) {
306
+ throw Error(`the links inside ${rel} could not be checked: ${String(error?.stderr ?? error?.message ?? '').trim()}`);
307
+ }
308
+ const bad = [];
309
+ for (const link of found) {
310
+ const name = path.relative(folder, link);
311
+ const target = linkTarget(link);
312
+ if (target === null) continue; // gone from under us, or no longer a link
313
+ if (path.isAbsolute(target)) { bad.push({ path: name, why: 'it is a link to somewhere outside this folder' }); continue; }
314
+ 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).
321
+ 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.
325
+ const home = nearestReal(folder);
326
+ const reached = nearestReal(resolved);
327
+ if (home && reached && !withinOrIs(home, reached)) {
328
+ bad.push({ path: name, why: 'it is a link that leads out of this folder through another link' });
329
+ continue;
330
+ }
331
+ 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.
335
+ const sourceSide = path.resolve(path.dirname(path.join(root, name)), target);
336
+ 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
+ }
340
+ return bad;
341
+ }
342
+
343
+ /**
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.
356
+ */
357
+ export function carryLocalFiles(root, folder) {
358
+ const { paths, source } = localFilesIn(root);
359
+ const theirs = ignoredIn(root, paths);
360
+ const carried = [];
361
+ const refused = [];
362
+ const no = (rel, why) => refused.push({ path: rel, why });
363
+
364
+ for (const rel of paths) {
365
+ const from = path.join(root, rel);
366
+ const to = path.join(folder, rel);
367
+ 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
+ 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.
375
+ if (tracksIt(folder, rel)) { no(rel, 'this branch tracks it'); continue; }
376
+ if (reachedThroughALink(folder, rel)) { no(rel, 'a link stands where it would have to be written'); continue; }
377
+ if (fs.existsSync(to)) continue; // already there; silent
378
+ if (isLink(from) && !carriableLink(root, from)) { no(rel, 'it is a link to somewhere outside the repository'); continue; }
379
+ try {
380
+ fs.mkdirSync(path.dirname(to), { recursive: true });
381
+ copyTree(from, to);
382
+ } catch (error) {
383
+ no(rel, `it could not be copied: ${String(error?.stderr ?? error?.message ?? '').trim()}`);
384
+ continue;
385
+ }
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.
393
+ if (!ignoredIn(folder, [rel]).has(rel)) {
394
+ try { fs.rmSync(to, { recursive: true, force: true }); } catch { /* best effort */ }
395
+ no(rel, 'the branch in this folder does not ignore it, so closing the task would commit it');
396
+ continue;
397
+ }
398
+ carried.push(rel);
399
+ }
400
+
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).
409
+ for (const rel of carried) refused.push(...linksThatDoNotHold(root, folder, rel));
410
+
411
+ if (refused.length) {
412
+ throw Error(`${source} names files this folder could not be given:\n${
413
+ refused.map((r) => ` ${r.path}: ${r.why}`).join('\n')}`);
414
+ }
415
+ return { carried, source };
416
+ }
417
+
418
+ /**
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.
423
+ */
424
+ function carriableLink(root, abs) {
425
+ const target = linkTarget(abs);
426
+ if (target === null) return false;
427
+ if (path.isAbsolute(target)) return false;
428
+ const resolved = path.resolve(path.dirname(abs), target);
429
+ if (!within(root, resolved)) return false;
430
+ const home = nearestReal(root);
431
+ const reached = nearestReal(resolved);
432
+ return !home || !reached || withinOrIs(home, reached);
433
+ }
434
+
435
+ const tracksIt = (folder, rel) => {
436
+ try { git(folder, ['ls-files', '--error-unmatch', '-z', '--', rel]); return true; }
437
+ catch (error) { if (error?.status === 1) return false; throw Error(`git could not say whether this branch tracks ${rel}: ${String(error?.stderr ?? error?.message ?? '').trim()}`); }
438
+ };
439
+
440
+ /**
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.
453
+ */
454
+ export function onlyTrackedContent(folder) {
455
+ let found;
456
+ try { found = git(folder, ['ls-files', '-z', '--others', '--directory', '--no-empty-directory']); }
457
+ catch (error) { throw Error(`what is in this folder could not be listed: ${String(error?.stderr ?? error?.message ?? '').trim()}`); }
458
+ const gone = [];
459
+ for (const rel of found.split('\0').map((p) => p.replace(/\/$/, '')).filter(Boolean)) {
460
+ if (!keepable(rel)) continue;
461
+ const at = path.join(folder, rel);
462
+ 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.
466
+ catch (error) { if (stillThere(at)) throw Error(`${rel} came with the clone and could not be cleared: ${error.message}`); }
467
+ gone.push(rel);
468
+ }
469
+ return gone;
470
+ }
471
+
472
+ /**
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`.
476
+ *
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.
482
+ */
483
+ export function trackedTopLevel(root) {
484
+ let listed;
485
+ try { listed = git(root, ['ls-files', '-s', '-z']); }
486
+ catch (error) { throw Error(`the tracked files could not be listed: ${String(error?.stderr ?? error?.message ?? '').trim()}`); }
487
+ const top = new Set();
488
+ for (const line of listed.split('\0').filter(Boolean)) {
489
+ const [modes, rel] = line.split('\t');
490
+ if (modes?.startsWith('160000')) return { entries: [], submodules: true };
491
+ if (rel) top.add(rel.split('/')[0]);
492
+ }
493
+ return { entries: [...top], submodules: false };
494
+ }
package/mcp/core/page.mjs CHANGED
@@ -87,7 +87,7 @@ export async function lookAtPage({ product, url, width = 1280, height = 900, cap
87
87
  * stored in the browser, and it is thrown away with the profile directory.
88
88
  */
89
89
  export async function captureWithChrome({ url, width, height, chrome = findChrome(), timeoutMs = 20_000 }) {
90
- if (!chrome) return { error: 'No Chrome, Chromium or Edge on this Mac, so there is nothing to open the page with.' };
90
+ if (!chrome) return { error: 'No Chrome, Chromium or Edge on this computer, so there is nothing to open the page with.' };
91
91
  const profile = fs.mkdtempSync(path.join(process.env.TMPDIR ?? '/tmp', 'look-at-page-'));
92
92
  const child = spawn(chrome, [
93
93
  '--headless=new', '--remote-debugging-port=0', `--user-data-dir=${profile}`,
package/mcp/core/work.mjs CHANGED
@@ -167,6 +167,22 @@ export function saidToClose(item, words) {
167
167
  return [item?.answer, item?.body, item?.title].some((t) => plainly(t).includes(quote));
168
168
  }
169
169
 
170
+ /*
171
+ * A PUT-OFF THE USER ASKED FOR IS THEIRS (w-d6ea290028).
172
+ *
173
+ * A runAt an agent writes brakes workers and never hides the row
174
+ * (`parkedByAgent`, list-rules.ts), so an agent cannot take its own question
175
+ * out of the inbox. But "remind me in 4 days" asks for exactly that, and the
176
+ * row sat in Needs you wearing Scheduled until Sunday, with the user unsure
177
+ * whether closing it would lose the reminder.
178
+ *
179
+ * So a worker that quotes the user's own words asking for the put-off, in
180
+ * `deferBecause`, checked exactly as `closeBecause` is, has the moment written
181
+ * as the user's. That is what hides a row until it comes due, puts it in
182
+ * Scheduled, and lets the user's L move it again.
183
+ */
184
+ const DEFERRED_AS_AGENT = 'The put-off was saved as YOURS, not theirs, so the row stays in their inbox until then: the words in deferBecause are not ones they wrote on this row. If they did ask for it to be put off, send runAt again with deferBecause set to their exact words.';
185
+
170
186
  /*
171
187
  * A SUMMARY LINE IS SHORT ENOUGH TO READ (w-b51b2e1c86). An agent's problem,
172
188
  * progress or solution over SUMMARY_WORDS words is taken out of the write and
@@ -356,7 +372,7 @@ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {})
356
372
  // fence below still catches the case where somebody else took over.
357
373
  }
358
374
 
359
- const { closeBecause, ...asked } = patch;
375
+ const { closeBecause, deferBecause, ...asked } = patch;
360
376
  let leftOpen = false;
361
377
  if (asked.status === 'done') {
362
378
  const before = readWorkItem(holding.dir, id);
@@ -365,6 +381,17 @@ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {})
365
381
  leftOpen = true;
366
382
  }
367
383
  }
384
+ let theirMoment = 0;
385
+ let deferredAsAgent = null;
386
+ if (deferBecause !== undefined && Number.isFinite(asked.runAt) && asked.runAt > 0) {
387
+ const before = readWorkItem(holding.dir, id);
388
+ if (herRow(before) && saidToClose(before, deferBecause)) {
389
+ theirMoment = asked.runAt;
390
+ delete asked.runAt;
391
+ } else {
392
+ deferredAsAgent = { deferredAsAgent: DEFERRED_AS_AGENT };
393
+ }
394
+ }
368
395
  const summaryTooLong = takeOverLong(asked);
369
396
  patch = asked;
370
397
  let item = Object.keys(patch).length
@@ -389,6 +416,10 @@ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {})
389
416
  holding.pending = false;
390
417
  }
391
418
 
419
+ // Only now, past the fence, so a session that lost the row cannot still
420
+ // put it away in the user's name.
421
+ if (theirMoment) item = updateWorkItem(holding.dir, id, { runAt: theirMoment }, { source: 'founder' });
422
+
392
423
  // FINISHING A ROW GIVES THE CLAIM BACK NOW, not in five minutes. It used
393
424
  // to drop the row from this map and leave the claim line standing, so the
394
425
  // ledger went on reading "held by <this session>" for a full lease after
@@ -411,9 +442,9 @@ export function createClaimRegistry({ heartbeatMs = HEARTBEAT_MS, holder } = {})
411
442
  holding.live = false;
412
443
  stopTimer();
413
444
  }
414
- return { ...item, leftOpen: LEFT_OPEN, ...summaryTooLong };
445
+ return { ...item, leftOpen: LEFT_OPEN, ...deferredAsAgent, ...summaryTooLong };
415
446
  }
416
- return { ...item, ...summaryTooLong };
447
+ return { ...item, ...deferredAsAgent, ...summaryTooLong };
417
448
  },
418
449
 
419
450
  release(id) {
package/mcp/tools.mjs CHANGED
@@ -191,7 +191,7 @@ export function buildTools({ holder } = {}) {
191
191
  },
192
192
  {
193
193
  name: 'update_work_item',
194
- description: 'Report progress on an item: set status to done when it is finished, blocked when it cannot proceed until something outside it changes, and leave a note saying where things stand. Keep the summary current with problem (shown as Context) and progress (shown as the Done list), which is all a teammate sees of the thread. A worker the app started for a row ends with its answer as its last message, and the app writes that message onto the row as the result, so such a worker does not pass result for its own row: the same answer written twice is what they then read twice. A row the founder wrote (labelled founder) is closed only when they asked for it: answering them is not finishing it, so the row stays open for them. To close one, pass closeBecause with their exact words asking you to; without them the rest of the write lands and the row is left open. Writing to a row this session is not holding takes the row first, so there is no need to claim before you speak; the write is only refused when another session is genuinely on it, and that refusal is kept rather than thrown away, parked where the error says and handed to whoever takes the row next. Finishing a row hands the claim straight back, and you can still answer on it afterwards.',
194
+ description: 'Report progress on an item: set status to done when it is finished, blocked when it cannot proceed until something outside it changes, and leave a note saying where things stand. Keep the summary current with problem (shown as Context) and progress (shown as the Done list), which is all a teammate sees of the thread. A worker the app started for a row ends with its answer as its last message, and the app writes that message onto the row as the result, so such a worker does not pass result for its own row: the same answer written twice is what they then read twice. A row the founder wrote (labelled founder) is closed only when they asked for it: answering them is not finishing it, so the row stays open for them. To close one, pass closeBecause with their exact words asking you to; without them the rest of the write lands and the row is left open. A runAt you set holds workers off but keeps the row in their inbox; when they asked for it to be put off ("remind me Friday"), pass deferBecause with their exact words and it leaves their inbox until then. Writing to a row this session is not holding takes the row first, so there is no need to claim before you speak; the write is only refused when another session is genuinely on it, and that refusal is kept rather than thrown away, parked where the error says and handed to whoever takes the row next. Finishing a row hands the claim straight back, and you can still answer on it afterwards.',
195
195
  schema: {
196
196
  id: z.string(),
197
197
  status: z.enum(WORK_ITEM_STATUSES).optional(),
@@ -201,6 +201,7 @@ export function buildTools({ holder } = {}) {
201
201
  labels: z.array(z.string()).optional(),
202
202
  runAt: z.number().int().optional().describe('epoch ms to defer this item until; 0 clears a schedule and makes it available now'),
203
203
  closeBecause: z.string().optional().describe('only with status done on a row the founder wrote: their own words, quoted exactly from the row, asking you to close it'),
204
+ deferBecause: z.string().optional().describe('only with runAt on a row the founder wrote: their own words, quoted exactly from the row, asking for it to be put off or to be reminded later. Without them the row stays in their inbox'),
204
205
  problem: z.string().optional().describe(`the summary's CONTEXT: a few plain sentences, ${CONTEXT_WORDS} words at most, that bring the thread back to someone who has forgotten it: what was asked, what was found, the facts needed to decide`),
205
206
  progress: z.string().optional().describe(`the summary's DONE list: the steps taken so far, one per line, oldest first and newest last, ${DONE_STEPS} lines at most of ${DONE_STEP_WORDS} words each. Send the whole list each time; it replaces the last one`),
206
207
  solution: z.string().optional().describe(`no longer shown; leave it out (older threads may still carry one, ${SUMMARY_WORDS} words at most)`),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agentbox-app",
3
3
  "productName": "Agentbox",
4
- "version": "0.1.11",
4
+ "version": "0.1.13",
5
5
  "description": "An inbox for managing dozens of Claude Code and Codex agents.",
6
6
  "author": "Savannah Feder",
7
7
  "repository": {
@@ -34,6 +34,7 @@
34
34
  "shared/**/*.mjs",
35
35
  "shared/**/*.mts",
36
36
  "shared/**/*.json",
37
+ "!shared/*.local.json",
37
38
  "mcp/**/*.mjs",
38
39
  "mcp/**/*.sh",
39
40
  "briefs/**/*.md",
@@ -52,6 +53,7 @@
52
53
  "build": "node scripts/prepare-terminal.mjs && node scripts/read-claude-commands.mjs && node scripts/read-claude-models.mjs && vite build renderer",
53
54
  "start": "npm run build && node scripts/brand-electron.mjs && electron .",
54
55
  "test": "vitest run",
56
+ "read:claude": "node scripts/read-claude-commands.mjs --snapshot && node scripts/read-claude-models.mjs --snapshot",
55
57
  "typecheck": "node node_modules/typescript/bin/tsc --noEmit -p renderer/tsconfig.json",
56
58
  "test:changed": "node scripts/test-what-changed.mjs",
57
59
  "icon": "electron scripts/make-icon.mjs",
@@ -63,6 +65,9 @@
63
65
  "hooks:install": "node scripts/install-hooks.mjs",
64
66
  "check:public": "node scripts/check-before-public.mjs --all",
65
67
  "ship": "node scripts/ship.mjs",
68
+ "relay:build": "node relay/build.mjs",
69
+ "relay:dev": "node relay/build.mjs && cd relay && npm ci --no-audit --no-fund && npx wrangler dev --port 8787",
70
+ "relay:deploy": "node relay/deploy.mjs",
66
71
  "prepare": "node scripts/install-hooks.mjs",
67
72
  "serve": "node bin/agentbox.mjs",
68
73
  "prepack": "npm run build",
@@ -93,6 +98,7 @@
93
98
  "@xterm/xterm": "^6.0.0",
94
99
  "electron": "^43.0.0",
95
100
  "electron-builder": "^26.15.3",
101
+ "mermaid": "^12.1.0",
96
102
  "pdfjs-dist": "^6.3.289",
97
103
  "react": "^18.3.1",
98
104
  "react-dom": "^18.3.1",
@@ -108,6 +114,8 @@
108
114
  "electron-updater": "^6.8.9",
109
115
  "node-pty": "^1.1.0",
110
116
  "posthog-node": "^5.21.2",
117
+ "qrcode": "^1.5.4",
118
+ "web-push": "^3.6.7",
111
119
  "zod": "^4.4.3"
112
120
  },
113
121
  "build": {
@@ -122,6 +130,7 @@
122
130
  "main/**/*",
123
131
  "shared/**/*",
124
132
  "preload.cjs",
133
+ "build/notification-icon.png",
125
134
  "briefs/**/*",
126
135
  "!briefs/founder.md",
127
136
  "!briefs/writing-rules.md",
@@ -0,0 +1 @@
1
+ import{g as p,r as u,d as a}from"./chunk-AW2ZBBNX-C01s4yoF.js";import{p as f}from"./chunk-JWPE2WC7-Dsqwv3eW.js";import{_ as n,l as o}from"./mermaid.core-CnYzvFkz.js";import{M as c,b as d}from"./cynefin-EF2NZ3EQ-DHngLxvQ.js";import"./main-ChNdnUvt.js";import"./product-name-CTKLYNWY.js";var v=d().RailroadAbnf.parser.LangiumParser,i=n(e=>{const r=e.alternatives.map(g);return r.length===1?r[0]:{type:"choice",alternatives:r}},"transformAlternation"),g=n(e=>{const r=e.elements.map(y);return r.length===1?r[0]:{type:"sequence",elements:r}},"transformConcatenation"),b=n(e=>{if(e.includes("*")){const[t,s]=e.split("*"),l=t?parseInt(t,10):0,m=s?parseInt(s,10):1/0;return{min:l,max:m}}const r=parseInt(e,10);return{min:r,max:r}},"parseRepeat"),y=n(e=>{const r=A(e.primary);if(!e.repeat)return r;const{min:t,max:s}=b(e.repeat);return t===0&&s===1?{type:"optional",element:r}:{type:"repetition",element:r,min:t,max:s}},"transformElement"),A=n(e=>{switch(e.$type){case"AbnfStringLiteral":return{type:"terminal",value:e.value};case"AbnfNumVal":return{type:"terminal",value:e.value};case"AbnfRuleName":return{type:"nonterminal",name:e.name};case"AbnfGroup":return i(e.element);case"AbnfOptionalGroup":return{type:"optional",element:i(e.element)};default:throw new Error(`Unsupported ABNF primary node: ${e.$type}`)}},"transformPrimary"),P=n(e=>({name:e.name,definition:i(e.definition)}),"transformRule"),h=n(e=>{f(e,a),e.title&&a.setTitle(e.title),e.rules.map(r=>a.addRule(P(r)))},"populateDb"),R={parse:n(e=>{a.clear(),o.debug("[ABNF Parser] Starting Langium parse");const r=v.parse(e);if(r.lexerErrors.length>0||r.parserErrors.length>0)throw new c(r);const t=r.value;o.debug("[ABNF Parser] Parsed rules:",t.rules.length),h(t),o.debug("[ABNF Parser] Parse complete")},"parse"),parser:{yy:a}},F={parser:R,db:a,renderer:u,styles:p};export{F as diagram};