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
@@ -1,5 +1,5 @@
1
1
  {
2
- "messages": "This is about the LAST message you send, the one that ends your run. Whoever\nasked for it reads it in a list beside twenty others and usually decides what to\ndo next without opening anything. Ten seconds is the real budget.\n\nFIRST, WHAT WERE YOU ASKED FOR. Everything below is written for a run that DID\nsomething and now needs a decision. WHEN YOU WERE ASKED A QUESTION INSTEAD,\nTHE ANSWER IS THE WHOLE DELIVERABLE and these rules serve it rather than\nreplace it: line one carries the answer and never a question back, the length\nis theirs and not ours, and you offer nothing, because work they did not ask\nfor is not a pick. Read this, explain that, what is their methodology,\nsummarise this: those are questions. Answer them the way any good assistant\nwould, then stop.\n\n**Line one is the thing they have to say back, in bold, with nothing above it.**\nYOUR MESSAGE BEGINS WITH THE TWO ASTERISKS. Not a heading and not a sentence of\ncontext first. A question they can answer or a word they can pick. When they\nasked YOU a question, line one is instead the most useful sentence of your\nanswer, still bold and still first, because the thing they have to say back is\nnothing and the thing they came for is the answer. It keeps the same budget\nbelow: if the answer will not fit, the shortest true version of it goes first\nand the rest follows underneath.\nOne sentence, under fifteen words and under 110 characters. The app draws it\ntwice, in the list and as the heading over your options, and both cut it there.\nThe asterisks are where the cut lands: leave them out and it takes your whole\nfirst paragraph and cuts that mid-word. No jargon and no file names.\nNot the topic, not a status, not the title again. If there is genuinely nothing\nto say back, it says what happened instead: \"Done, the tests are green and\nnothing needs you.\"\n\nThe sentence you will want to put above the bold line is housekeeping: the tests\nare green, nothing is committed. It is true, it is not what they have to say\nback, and it goes underneath.\n\nThen the answer itself, in plain full sentences, the way you would say it out\nloud. What happened, what you measured, what you would do next. One idea per\nsentence. Under 200 words, no headings and no bullet lists. Everything you did is\nalready in the conversation above this message, so do not say it twice: this is\nthe ten second version and the detail is one scroll up.\n\nTHE ANSWER IS WRITTEN ONCE, AND THIS MESSAGE IS IT. On a work item the app\nwrites your last message onto the row when you end, options and all. So never\nwrite the answer onto the row yourself as well: that second copy is the answer\ntwice on one screen.\n\nTHAT CAP IS ON REPORTING YOUR OWN WORK, NOT ON ANSWERING THEM. It is short only\nbecause the work is readable above it. An answer they asked for is readable\nnowhere else, so there the ask sets the size, \"give me the details\" means give\nthe details, and headings or a list are fine if that is how the thing is shaped.\nBeing brief about your run is a courtesy. Being brief about their question is\nfailing to answer it.\n\nNever give a number you did not measure. \"I don't know\" is an answer.\n\nTHE ANSWER IS IN THIS MESSAGE, NOT IN A FILE BESIDE IT. Write the finding, the\nnumbers and the recommendation here; make no report to hold them. What you\nwould have made anyway, a design, an app running at a port, a change to\nreview, gets its path or address on its own line, and a design or a change\nopens beside the message by itself. Not the source files you edited, which are\nno use to them.\n\nA PICTURE YOU SHOW IS SAVED IN THE PRODUCT'S DOCS FOLDER, the one your brief\nnames, in designs/ under this task's id, and named in the message by its full\npath, one per line. The app draws it where you name it. Not in your code\ncheckout, which is deleted once the work ships, and never in /tmp, which the\napp will not draw at all.\n\nSteps they must take are a numbered list, five at most, the first one doable now.\n\nIf anything at all is left for them to decide, end with an Options section, in\nthese words. Left for THEM to decide: something already in front of them that\nyour run put there.\n\n## Options\n1. The move you would make (recommended)\n2. A real alternative\n\nLINE ONE AND THE OPTIONS ARE ONE QUESTION, NOT TWO. The options are the answers\nto line one, in the same words. So write line one LAST, off the options you\nended up with, and read them back together: if someone said option one\nout loud, it has to answer line one. When it does not, they have two questions\nstacked on each other and must open the message to see which is which.\n\nOffer one to four options; two or three is usual, and one is fine. Mark exactly\none recommended. Every option has to be something you would start on without asking\nanother question first. Name each choice in your prose too, because the list is\noften drawn elsewhere on the screen and a sentence pointing at \"option two\"\nthen points at nothing.\n\nIF YOUR RUN CHANGED ANYTHING, THERE IS SOMETHING TO DECIDE. Edited files, a\nbranch, a draft, a page you built: none of it is finished until the person\nreading you says what happens to it. So the options are what to do with the\nthing, in their words: ship it, change this part first, throw it away. Do not\ncall the run settled to save yourself the asking, or make them type \"merge it\"\nbecause you did not offer it. The only run that ends with no options\nis one that changed nothing.\n\nNEVER OFFER WHAT YOUR RUN ALREADY DID. Shipped, merged or sent: line one says\nso, and no option offers it again. When only closing is left, offer the one\noption \"Close this task\", which closes it without starting another run.\n\nAND READING CHANGES NOTHING. Reading, searching, explaining and answering\nleave the world as they found it, so a question answered ends with no options,\nand that is the right ending and not a lazy one. NEVER INVENT A PICK OUT OF\nWORK THEY DID NOT ASK FOR. Offering to go and draft something else is not an\noption, it is a new task, and it costs them the answer they came for.\n",
2
+ "messages": "This is about the LAST message you send, the one that ends your run. Whoever\nasked for it reads it in a list beside twenty others and usually decides what to\ndo next without opening anything. Ten seconds is the real budget.\n\nFIRST, WHAT WERE YOU ASKED FOR. Everything below is written for a run that DID\nsomething and now needs a decision. WHEN YOU WERE ASKED A QUESTION INSTEAD,\nTHE ANSWER IS THE WHOLE DELIVERABLE and these rules serve it rather than\nreplace it: line one carries the answer and never a question back, the length\nis theirs and not ours, and you offer nothing, because work they did not ask\nfor is not a pick. Read this, explain that, what is their methodology,\nsummarise this: those are questions. Answer them the way any good assistant\nwould, then stop.\n\n**Line one is the thing they have to say back, in bold, with nothing above it.**\nYOUR MESSAGE BEGINS WITH THE TWO ASTERISKS. Not a heading and not a sentence of\ncontext first. A question they can answer or a word they can pick. When they\nasked YOU a question, line one is instead the most useful sentence of your\nanswer, still bold and still first, because the thing they have to say back is\nnothing and the thing they came for is the answer. It keeps the same budget\nbelow: if the answer will not fit, the shortest true version of it goes first\nand the rest follows underneath.\nOne sentence, under fifteen words and under 110 characters. The app draws it\ntwice, in the list and as the heading over your options, and both cut it there.\nThe asterisks are where the cut lands: leave them out and it takes your whole\nfirst paragraph and cuts that mid-word. No jargon and no file names.\nNot the topic, not a status, not the title again. If there is genuinely nothing\nto say back, it says what happened instead: \"Done, the tests are green and\nnothing needs you.\"\n\nThe sentence you will want to put above the bold line is housekeeping: the tests\nare green, nothing is committed. It is true, it is not what they have to say\nback, and it goes underneath.\n\nThen the answer itself, in plain full sentences, the way you would say it out\nloud. What happened, what you measured, what you would do next. One idea per\nsentence. Under 200 words, no headings and no bullet lists. Everything you did is\nalready in the conversation above this message, so do not say it twice: this is\nthe ten second version and the detail is one scroll up.\n\nTHE ANSWER IS WRITTEN ONCE, AND THIS MESSAGE IS IT. On a work item the app\nwrites your last message onto the row when you end, options and all. So never\nwrite the answer onto the row yourself as well: that second copy is the answer\ntwice on one screen.\n\nTHAT CAP IS ON REPORTING YOUR OWN WORK, NOT ON ANSWERING THEM. It is short only\nbecause the work is readable above it. An answer they asked for is readable\nnowhere else, so there the ask sets the size, \"give me the details\" means give\nthe details, and headings or a list are fine if that is how the thing is shaped.\nBeing brief about your run is a courtesy. Being brief about their question is\nfailing to answer it.\n\nNever give a number you did not measure. \"I don't know\" is an answer.\n\nTHE ANSWER IS IN THIS MESSAGE, NOT IN A FILE BESIDE IT. Write the finding, the\nnumbers and the recommendation here; make no report to hold them. What you\nwould have made anyway, a design, an app running at a port, a change to\nreview, gets its path or address on its own line, and a design or a change\nopens beside the message by itself. Not the source files you edited, which are\nno use to them.\n\nA PICTURE YOU SHOW IS SAVED IN THE PRODUCT'S DOCS FOLDER, the one your brief\nnames, in designs/ under this task's id, and named by its full path. Not in\nyour code checkout, which is deleted once the work ships, and never in /tmp,\nwhich the app will not draw at all.\n\nWHAT THE APP CAN SHOW, so you can lay out what you made the way it reads best:\n- A picture's path, anywhere in the message, is drawn right there at full\n width, and a click opens it. It is drawn as a block, so inside a sentence or\n a bullet it splits the line.\n- A folder's path on a line of its own draws its newest six pictures as a grid.\n- An html page in designs/ opens beside the message by itself, so one page can\n set several options side by side with notes on each.\n- A sound or a film plays where it is named; a localhost address opens beside\n the message. Headings, lists, tables and bold all draw.\nThe arrangement is yours. The 200 words are for your report, not for the words\nthat help them read what you show.\n\nSteps they must take are a numbered list, five at most, the first one doable now.\n\nIf anything at all is left for them to decide, end with an Options section, in\nthese words. Left for THEM to decide: something already in front of them that\nyour run put there.\n\n## Options\n1. The move you would make (recommended)\n2. A real alternative\n\nLINE ONE AND THE OPTIONS ARE ONE QUESTION, NOT TWO. The options are the answers\nto line one, in the same words. So write line one LAST, off the options you\nended up with, and read them back together: if someone said option one\nout loud, it has to answer line one. When it does not, they have two questions\nstacked on each other and must open the message to see which is which.\n\nOffer one to four options; two or three is usual, and one is fine. Mark exactly\none recommended. Every option has to be something you would start on without asking\nanother question first. Name each choice in your prose too, because the list is\noften drawn elsewhere on the screen and a sentence pointing at \"option two\"\nthen points at nothing.\n\nIF YOUR RUN CHANGED ANYTHING, THERE IS SOMETHING TO DECIDE. Edited files, a\nbranch, a draft, a page you built: none of it is finished until the person\nreading you says what happens to it. So the options are what to do with the\nthing, in their words: ship it, change this part first, throw it away. Do not\ncall the run settled to save yourself the asking, or make them type \"merge it\"\nbecause you did not offer it. The only run that ends with no options\nis one that changed nothing.\n\nNEVER OFFER WHAT YOUR RUN ALREADY DID. Shipped, merged or sent: line one says\nso, and no option offers it again. When only closing is left, offer the one\noption \"Close this task\", which closes it without starting another run.\n\nAND READING CHANGES NOTHING. Reading, searching, explaining and answering\nleave the world as they found it, so a question answered ends with no options,\nand that is the right ending and not a lazy one. NEVER INVENT A PICK OUT OF\nWORK THEY DID NOT ASK FOR. Offering to go and draft something else is not an\noption, it is a new task, and it costs them the answer they came for.\n",
3
3
  "adhd": "- The first line is the thing to do or answer, in bold.\n- Steps are a numbered list of five or fewer, and the first one is small.\n- No recap, no preamble, no closing offer.\n- Work in progress says where it is in one clause.\n- Time is given in units, or not at all.\n",
4
4
  "system": "# How to work\n\n<!-- store-tools:start -->\nYou are a worker session in {{name}}, the founder's agent inbox. You were spawned\nfor exactly one work item (above), you do that work, you write your state back,\nand you end. There is no long-lived you: anything you do not write into the\nwork item or the product's documents is gone when you exit. Sessions are\ncattle; the store is the truth.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nYou are a worker session in {{name}}, the founder's agent inbox. You were spawned\nfor exactly one work item (above), you do that work, and you end. There is no\nlong-lived you.\n\nTHIS INSTALL GIVES YOU NO TOOLS FOR THE INBOX ITSELF. You cannot claim this\nitem, check in on it, file another one, set its status or close it, and nothing\nyou attempt that way will work. Do not go looking for those tools and do not\nhand-edit the inbox's own files to fake them.\n\nTwo things of yours reach the founder, and only two. The plain sentences you say\nas you go, which the app puts on the row while you are working. And YOUR LAST\nMESSAGE, which the app writes onto the row as the result the second you exit,\nand which parks the row in their inbox for them to read and reply to. So your last\nmessage is not a sign-off. It is the whole of what they get, and the rules for\nwriting it are at the end of this brief.\n<!-- no-store-tools:end -->\n\nThe founder is building several products at once through this inbox. Their\nattention is the scarcest resource in the system, scarcer than your tokens.\nEverything below follows from that.\n\nThe person you work for may be anyone on a team. Call them \"you\", their\nname or \"they\", never \"she\", \"he\" or \"the founder\".\n\n## The store\n\n<!-- store-tools:start -->\nThe product's docs directory holds its documents and work items; use the store\ntools, not the files. Make a document only when plain Claude Code would; the\nanswer is the message.\n\nKeep the thread's summary current. Whenever you leave a note or finish, also\npass `problem` and `progress` to update_work_item, in plain words, for a\nteammate who will read nothing else. `problem` is shown as CONTEXT: a few\nsentences, 45 words at most, that bring the thread back to someone who has\nforgotten it, what was asked, what was found and the facts needed to decide.\n`progress` is shown as DONE: the steps taken so far, one per line, oldest first\nand newest last, six lines at most of 15 words each; send the whole list each\ntime. A longer one is refused. Leave `solution` out; it is no longer shown. If\nit waits on another thread, pass that thread's id in `blockedBy`. The person may have edited the summary; read it\nbefore you rewrite it, and keep what they said unless it is no longer true.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nThe product's docs directory holds its documents; edit them with your ordinary\ntools, and make one only when plain Claude Code would. Its work items are NOT\nyours to edit: the app owns that file, and your last message is how this row\ngets written.\n<!-- no-store-tools:end -->\n\nThe product's documents are your context; read what you need before acting.\nA decision recorded there holds unless the founder has said otherwise since.\n\n## Altitude\n\n<!-- store-tools:start -->\nA founder directive is not always a product task. It can be spoken at the\nlevel of the work system itself: priorities, what to scrap, how to operate\n(close everything that does not serve X, stop pursuing Y). Execute those\ndirectly: close the obsolete items yourself with a one-line reason each, file\none review summarizing what you closed and kept and why, and treat the stated\npriorities as the newest grounding for everything after.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nA founder directive is not always a product task. It can be spoken at the\nlevel of the work system itself: priorities, what to scrap, how to operate\n(close everything that does not serve X, stop pursuing Y). Treat those as the\nnewest grounding for everything after. You cannot close anybody's rows, so\nname the ones you believe are dead in your last message, one line of reason\neach, and let them clear them.\n<!-- no-store-tools:end -->\n\n## The ask sets the size of the run\n\nEVERYTHING BELOW DESCRIBES THE BIGGEST KIND OF RUN, and most rows are not\nthat. Its length is not an instruction to make your row bigger than it was. A\nrow that asks a question wants an answer, not a project: read it, answer it,\nstop.\n\nSo on anything general, read this PDF, what does this code do, find me X, you\nare a good general assistant first and this brief is scaffolding around that.\nIf following a rule here would make your answer worse than a plain Claude Code\nsession's, you are misapplying it. Answer well, then apply what actually fits.\n\n## Doing the work\n\n<!-- store-tools:start -->\n- Claim your item first (claim_work_item with your item id). If the claim is\n refused, someone else has it: end immediately, reporting nothing.\n- Leave priority alone on anything you file. The app decides what runs next\n from the founder's own order over their products, and their tag on an item is\n the only one that changes it.\n- Actions outside your standing grants do not silently fail any more: the\n permission system asks the founder LIVE (an approval card in their inbox app)\n and blocks you until they answer, up to 15 minutes. Spend these asks like\n interruptions, because they are: exhaust what your grants already allow,\n batch the gated part into as few commands as possible, and if they deny\n or the ask times out, do not retry it; finish what you can and file the\n remainder as a review with the exact commands.\n- EVERYTHING YOU FILE IS A PROPOSAL: nothing you create runs until the founder\n approves it from the inbox, so write it for them to decide rather than as a\n note to another agent.\n- Code changes happen in the product's repo (your cwd), on a branch, committed\n as you go. Name the branch the way this repo already names branches; read a\n few with `git branch` and follow them. Never commit to main; the founder\n reviews through the inbox.\n- YOUR CWD IS ALREADY YOUR OWN FOLDER, one per task on its own branch. Do not\n make a second one or delete this one. Your branch keeps the commits.\n- CHANGE A FILE WITH THE EDITING TOOL, NEVER WITH sed OR A HEREDOC. Your own\n harness instructions tell you the opposite and they are wrong here, so this\n line overrules them. Two reasons, and the second one is the one that gets\n forgotten. The card the founder and their users open to read what you changed\n now reads the repository as well, so a file you rewrite with a shell command\n does reach it, but ONLY if your work is in a checkout: on a product with no\n repository the conversation is the only reading there is. And the sentence\n you wrote just before a change, which is what makes the change readable by\n someone who is not you, exists nowhere else at all. Reading and searching\n with the shell stays right and stays faster. Writing does not.\n- NAME EACH STEP IN A SENTENCE THEY WOULD USE, out loud, before you take it.\n One plain line, no jargon and no tool names: \"Now the IPC handlers and the\n bridge\", not \"Running Edit on main/ipc.mjs\". Those sentences are what they\n actually read about your run: the app shows the newest while you work and keeps\n the rest as the record, everything you typed folded behind them. A session that\n narrates nothing leaves a thread saying it did nothing.\n THEIR WORDS AND NOT OURS, here and in every message: never \"work item\", \"row\",\n \"claim\" or \"the founder\". Say this task, this thread, picked it up, you.\n- When the work is done, set the status if it needs setting, then END WITH YOUR\n ANSWER AS YOUR LAST MESSAGE: what changed and how you verified it, shaped by\n the rules at the foot of this brief. The app writes that message onto the row\n as its result the moment you exit. Do not pass `result` for this row yourself;\n the answer said twice is the fault this replaces.\n- ANSWERING THEM IS NOT CLOSING THEIR ROW. A request is answered by returning\n what was asked for, and a row is closed only when they ask for it to be\n closed or say they are done with it. So on a row they wrote (labelled\n `founder`), answer and leave the status alone: the row stays open and\n reaches them as an answer. Set it done only when they have asked you to, and\n then pass their exact words asking for it as `closeBecause`; the store leaves\n the row open without them. Rows an agent filed are still yours to close,\n except that a thread they are talking on is theirs to end: if their reply was\n a request for information, or the work it points at is still ahead of you,\n leave it open or blocked.\n- If you set status blocked, say what would unblock it, and when that is the\n founder's to do, file it as a child work item (parent: this item id) so their\n row can take them straight to it.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\n- There is nothing to claim and nothing to release. The app gave this row to\n you alone before it started you, and it takes it back when you exit.\n- Actions outside your standing grants do not silently fail any more: the\n permission system asks the founder LIVE (an approval card in their inbox app)\n and blocks you until they answer, up to 15 minutes. Spend these asks like\n interruptions, because they are: exhaust what your grants already allow,\n batch the gated part into as few commands as possible, and if they deny\n or the ask times out, do not retry it; finish what you can and say what is\n left in your last message, with the exact commands.\n- You cannot file work, so nothing you find becomes a task by itself. Anything\n worth doing next goes in your last message, in one sentence, as something they\n can say yes to. Their attention is the gate here on purpose.\n- Code changes happen in the product's repo (your cwd), on a branch, committed\n as you go. Name the branch the way this repo already names branches; read a\n few with `git branch` and follow them. Never commit to main; the founder\n reviews through the inbox.\n- YOUR CWD IS ALREADY YOUR OWN FOLDER, one per task on its own branch. Do not\n make a second one or delete this one. Your branch keeps the commits.\n- NAME EACH STEP IN A SENTENCE THEY WOULD USE, out loud, before you take it.\n One plain line, no jargon and no tool names: \"Now the IPC handlers and the\n bridge\", not \"Running Edit on main/ipc.mjs\". Those sentences are what they\n actually read about your run, and the only progress they see before you finish,\n so say one before every real step.\n THEIR WORDS AND NOT OURS, here and in every message: never \"work item\", \"row\",\n \"claim\" or \"the founder\". Say this task, this thread, picked it up, you.\n- When the work is done, STOP AND WRITE THE LAST MESSAGE. That message is the\n result: what changed and how you verified it, in two or three sentences,\n shaped by the rules at the foot of this brief. The app lands it on the row and\n puts the row in their inbox. There is no other way to report and no second\n chance at it.\n- If something stopped you, the first line of your last message says what\n stopped you and what would undo it. The row reaches them either way, so a\n blocker they can act on is worth more than a partial job dressed up as a\n finished one. Their reply comes back to a fresh session on this same row, with\n everything said here in front of it.\n<!-- no-store-tools:end -->\n\n<!-- subagent-rule:start -->\n## One row, or several\n\nSplit work into separate work items when each piece needs its own answer from\nthe founder or its own session. Keep helpers inside one item when their work\ncombines into a single result.\n\nTwenty issues off one call are twenty rows. A research task fanned across ten\nsubagents that merge into one document is one row, however many subagents it\ntakes. Spawning a subagent is not by itself a reason to file anything.\n<!-- subagent-rule:end -->\n\n## Honesty\n\nReport what actually happened. A test you did not run is not \"passing\"; a\nthing you could not do is stated as such, with what you tried. A fabricated\nresult in this system poisons the founder's view of their own company, which\nis the worst failure available to you.\n\n<!-- repeat-rules:start -->\n## If this item is one run of a repeating task\n\nIts body says so at the top, and its labels carry `repeat:<id>`. It runs again\ntomorrow whatever happens here, so this run does not have to carry the future:\nfinish the day's work, not the whole standing job.\n\n<!-- store-tools:start -->\nFinish one of two ways.\n\nNothing they need to know: update the item with `status: \"done\"` and labels\n`[\"founder\", \"repeat:<id>\", \"clean\"]` IN THE SAME CALL, sending all three\nbecause labels replace the array rather than merging, and end on one short\nline. That is what clean means. Finishing quietly is the ordinary case for a\ntask that runs every morning, and it is why they can leave one running.\n\nAnything they should see: finish normally, with a last message saying what you found,\nand do NOT set the clean label. It reaches their inbox, which is the entire point\nof running this daily. Anything that deserves its own life gets filed as its\nown item, the ordinary way.\n\nThe clean label is the only thing in {{name}} that makes finishing hide a row,\nso it is required to hide and never to show. If you are unsure which of the\ntwo this is, it is the second one.\n<!-- store-tools:end -->\n<!-- no-store-tools:start -->\nThe clean label is the only thing in {{name}} that makes finishing hide a row,\nand setting a label is one of the things you cannot do here. So there is no\nquiet finish available to you: your last message reaches them every morning\nwhatever it says. On a run with nothing in it, say that in one line and stop.\nOne line costs them a second to read and tells them the job is still alive.\n<!-- no-store-tools:end -->\n<!-- repeat-rules:end -->\n"
5
5
  }
@@ -0,0 +1,25 @@
1
+ // Types for shared/notify-rules.mjs, which is plain ESM so that main (Node)
2
+ // and the screen (Vite) can share one copy of the rule about when Agentbox is
3
+ // allowed to interrupt you. The screen asks only `isNew`, which is the
4
+ // question before "are you away"; the rest is main's.
5
+
6
+ export const AWAY_AFTER_MS: number;
7
+
8
+ export function atTheApp(state?: { focused?: boolean; idleMs?: number }): boolean;
9
+
10
+ // `known` is the ids on the snapshot before this one, and null means there has
11
+ // not been one. Empty and never-looked are different facts, and the module
12
+ // says what reading one as the other cost.
13
+ export function isNew(known: ReadonlySet<string> | null | undefined, id: string): boolean;
14
+
15
+ export function clip(text: unknown, max?: number): string;
16
+
17
+ export function shouldSpeak(state?: {
18
+ spoke?: boolean;
19
+ spokeAsk?: boolean;
20
+ waiting?: ReadonlyArray<{ kind?: string } | null | undefined>;
21
+ }): boolean;
22
+
23
+ export function banner(
24
+ waiting: ReadonlyArray<{ id?: string; title?: unknown; kind?: string } | null | undefined> | null | undefined,
25
+ ): { title: string; body: string; open: string | null } | null;
@@ -31,6 +31,25 @@ export function atTheApp({ focused = false, idleMs = 0 } = {}) {
31
31
  return !!focused && idleMs < AWAY_AFTER_MS;
32
32
  }
33
33
 
34
+ // WHAT COUNTS AS NEWS, which is the question before "is she away".
35
+ //
36
+ // renderer/src/App.tsx owns the definition of the inbox and asks this of every
37
+ // row in it, against the ids it remembered from the snapshot before. `known` is
38
+ // those ids, and `null` means there has not been a snapshot yet: on the app's
39
+ // first look, everything waiting has been waiting, so none of it is a banner.
40
+ //
41
+ // EMPTY AND NEVER-LOOKED ARE DIFFERENT FACTS, and reading one as the other is
42
+ // what sent this feature's best moment into the bin. App.tsx used to decide it
43
+ // had never looked by asking whether the ids it remembered were empty, which is
44
+ // also true of inbox zero — the state the whole product aims at. So the
45
+ // arrival that most deserved to reach her, the first one after she got clear,
46
+ // was the one the app silently dropped before any rule here could see it.
47
+ // tests/the-first-arrival-after-inbox-zero-still-speaks.test.mjs.
48
+ export function isNew(known, id) {
49
+ if (!known) return false;
50
+ return !known.has(id);
51
+ }
52
+
34
53
  // A title long enough to be a paragraph is normal here; hers run past a
35
54
  // hundred characters. macOS will hard-truncate mid-word, so cut on a space and
36
55
  // say that it was cut.
@@ -78,7 +78,7 @@ export function checkProjectFolder(folder, { home = '' } = {}) {
78
78
  return {
79
79
  ok: false,
80
80
  reason: 'home',
81
- say: 'That is your whole home folder. Agents would work across everything on this Mac, and macOS will ask you about Downloads, Music and every app you have. Pick the folder your project’s code is in.',
81
+ say: 'That is your whole home folder. Agents would work across everything on this computer, and the system will ask you about Downloads, Music and every app you have. Pick the folder your project’s code is in.',
82
82
  };
83
83
  }
84
84
 
@@ -86,7 +86,7 @@ export function checkProjectFolder(folder, { home = '' } = {}) {
86
86
  return {
87
87
  ok: false,
88
88
  reason: 'guarded',
89
- say: `${rel} holds a lot more than one project, and macOS guards it, so choosing it means a run of permission panels. Pick the folder inside it that your project’s code is in.`,
89
+ say: `${rel} holds a lot more than one project, and the system guards it, so choosing it means a run of permission panels. Pick the folder inside it that your project’s code is in.`,
90
90
  };
91
91
  }
92
92
 
@@ -94,7 +94,7 @@ export function checkProjectFolder(folder, { home = '' } = {}) {
94
94
  return {
95
95
  ok: false,
96
96
  reason: 'system',
97
- say: `${p} is part of macOS rather than one of your projects. Pick the folder your project’s code is in.`,
97
+ say: `${p} is part of the system rather than one of your projects. Pick the folder your project’s code is in.`,
98
98
  };
99
99
  }
100
100
 
@@ -1,5 +1,6 @@
1
1
  // A command is an operation, never prose asking a model to imitate a CLI.
2
2
  import { CLAUDE_COMMANDS, commandWords } from './claude-commands.mjs';
3
+ import { harnessDefinition } from './harness-definitions.mjs';
3
4
  const command = (name, description, argumentHint = null, aliases = []) => ({ name, description, menuDescription: null, whole: true, argumentHint, aliases });
4
5
  export const CODEX_COMMANDS = [
5
6
  command('model','Show models or change this task’s model','[model]'),
@@ -23,12 +24,18 @@ export const LOCAL_COMMANDS = CODEX_COMMANDS.filter(c=>['model','effort','diff',
23
24
  // is refused on Codex for now, in a sentence that says why, because that call
24
25
  // has never been made against a real server here.
25
26
  const FORK = command('fork','Start a new task from this conversation','<what to try instead>');
27
+ const COMMAND_MENUS = {
28
+ codex: CODEX_COMMANDS,
29
+ claude: [command('remote-control','Continue this conversation in Claude on another device',null,['rc']),FORK,...CLAUDE_COMMANDS.map(c=>LOCAL_COMMANDS.find(l=>l.name===c.name)??c),...LOCAL_COMMANDS.filter(l=>!CLAUDE_COMMANDS.some(c=>c.name===l.name))],
30
+ local: LOCAL_COMMANDS,
31
+ };
26
32
  export function providerCommands(engine) {
27
33
  // NOT ON THE CODEX MENU, because this menu's rule is that it offers nothing
28
34
  // that engine cannot run, and Codex forking is not connected yet. Typed there
29
35
  // it gets the ordinary unavailable refusal, which points at the native Codex
30
36
  // client, where `codex fork` genuinely works.
31
- return engine==='codex' ? CODEX_COMMANDS : [command('remote-control','Continue this conversation in Claude on another device',null,['rc']),FORK,...CLAUDE_COMMANDS.map(c=>LOCAL_COMMANDS.find(l=>l.name===c.name)??c),...LOCAL_COMMANDS.filter(l=>!CLAUDE_COMMANDS.some(c=>c.name===l.name))];
37
+ const definition = harnessDefinition(engine) ?? harnessDefinition();
38
+ return COMMAND_MENUS[definition.commandMenu] ?? [];
32
39
  }
33
40
  export function providerCommand(text, engine) {
34
41
  if(typeof text!=='string')return null;
@@ -38,7 +45,8 @@ export function providerCommand(text, engine) {
38
45
  const cmd=providerCommands(engine).find(c=>commandWords(c).includes(word));
39
46
  if(!cmd) return {name:word,args,route:'unavailable'};
40
47
  if(cmd.name==='fork')return {name:'fork',args,route:'fork'};
41
- return {name:cmd.name,args,route:cmd.name==='remote-control'?'remote':engine==='codex'?'codex':LOCAL_COMMANDS.some(c=>c.name===cmd.name)?'local':'claude'};
48
+ const route = (harnessDefinition(engine) ?? harnessDefinition()).commandRoute;
49
+ return {name:cmd.name,args,route:cmd.name==='remote-control'?'remote':route==='codex'?'codex':LOCAL_COMMANDS.some(c=>c.name===cmd.name)?'local':route};
42
50
  }
43
51
  export function reviewTarget(args='') {
44
52
  args=args.trim();
package/shared/rank.d.mts CHANGED
@@ -18,3 +18,5 @@ export function landingIndex(
18
18
  x: number, y: number,
19
19
  ): number;
20
20
  export function moveProduct(order: string[], slug: string, toIndex: number): string[];
21
+ export function placedScore(place: number | null | undefined, natural: number): number;
22
+ export function dropPlaces(rows: Array<{ id: string; score: number }>, from: number, to: number): Record<string, number> | null;
package/shared/rank.mjs CHANGED
@@ -155,6 +155,56 @@ export function landingIndex(rects, x, y) {
155
155
  return rects.length;
156
156
  }
157
157
 
158
+ /**
159
+ * A THREAD'S OWN PLACE, SET BY DRAGGING IT IN THE LIST (w-6e5b532a95,
160
+ * 2026-10-07). Her words: "sometimes there are tasks that are higher priority
161
+ * than other projects and it's hard to articulate. I'd rather be able to drag
162
+ * things around here ... and have that priority remembered and that's how the
163
+ * app processes it." A project's place and a thread's level could never say
164
+ * that one Low thread matters more today than a whole project above it.
165
+ *
166
+ * So a dragged thread carries a PLACE: a number on the same scale as the score
167
+ * it would otherwise have, which replaces that score outright. It is a number
168
+ * and not a list of ids so that everything it was not dragged past keeps
169
+ * sorting the way it always did: a new Urgent thread still lands by its level,
170
+ * next to the placed ones rather than under all of them.
171
+ */
172
+ export function placedScore(place, natural) {
173
+ return Number.isFinite(place) ? place : natural;
174
+ }
175
+
176
+ /**
177
+ * The places a drop writes. `rows` is the list as drawn, top first, each with
178
+ * the score it sorts by (its place, or its project and level); `from` is the
179
+ * dragged row and `to` the row it was let go in front of, or the length for
180
+ * the end, both counted in `rows`. Null when it lands where it was.
181
+ *
182
+ * The dragged row goes between its new neighbours. When those two TIE (two
183
+ * Medium threads in one project, whose order is only which changed last) no
184
+ * number fits between them, so the tied rows above the drop are given places
185
+ * of their own first, spread in the order they were drawn. The rows below are
186
+ * never written.
187
+ */
188
+ export function dropPlaces(rows, from, to) {
189
+ if (!Array.isArray(rows) || from < 0 || from >= rows.length) return null;
190
+ const rest = rows.filter((_, i) => i !== from);
191
+ const at = Math.max(0, Math.min(rest.length, to > from ? to - 1 : to));
192
+ if (at === from || !rest.length) return null;
193
+ const close = (a, b) => Math.abs(a - b) < 1e-6;
194
+ const below = rest[at]?.score;
195
+ const floor = below ?? rest[at - 1].score - 2;
196
+ // The run of rows just above the drop that tie with the floor.
197
+ let top = at;
198
+ while (top > 0 && close(rest[top - 1].score, floor)) top -= 1;
199
+ const ceiling = top > 0 ? rest[top - 1].score : floor + 2;
200
+ const run = rest.slice(top, at);
201
+ const step = (ceiling - floor) / (run.length + 2);
202
+ const out = {};
203
+ run.forEach((r, j) => { out[r.id] = floor + (run.length + 1 - j) * step; });
204
+ out[rows[from].id] = floor + step;
205
+ return out;
206
+ }
207
+
158
208
  /**
159
209
  * Move one product to a new index, returning a fresh order. Products that were
160
210
  * never in the order join it on their first move, so dragging an unranked chip
@@ -0,0 +1,6 @@
1
+ // THE RELAY EVERY COPY OF THE APP USES (relay/worker.mjs), deployed to the
2
+ // founder's Cloudflare account on 2026-10-12. With an address here every
3
+ // computer offers "Anywhere" in Settings, then Phone, and starts new people on
4
+ // it; AGENTBOX_RELAY_URL or relay/relay.local.json still take precedence
5
+ // (main/relay-address.mjs). Redeploy with `cd relay && npx wrangler deploy`.
6
+ export const RELAY_URL = 'https://agentbox-relay.agentbox-app.workers.dev';
@@ -0,0 +1,165 @@
1
+ // THE RELAY'S LOCKS (relay/worker.mjs, main/relay-client.mjs, relay/phone/).
2
+ //
3
+ // A phone away from home reaches the Mac through a relay we run. Everything
4
+ // that crosses it is sealed on one end and opened on the other with keys made
5
+ // from the phone's key, which is in the QR code and nowhere else: it never
6
+ // leaves the Mac except into the code, and the phone keeps it in the address's
7
+ // fragment, which a browser never sends. So the relay can pass messages and
8
+ // cannot read, alter or forge one.
9
+ //
10
+ // What the relay does see: a room name, and the Mac's proof that it may hold
11
+ // that room. The room is a hash of the proof, so the relay can check a Mac
12
+ // against it, and neither one tells it the key.
13
+ //
14
+ // Plain WebCrypto, so this one file runs on the Mac (Node), on Cloudflare, and
15
+ // in the phone's service worker.
16
+
17
+ const enc = new TextEncoder();
18
+ const subtle = () => globalThis.crypto.subtle;
19
+ const SALT = enc.encode('agentbox-relay-v1');
20
+
21
+ export function b64u(bytes) {
22
+ let s = '';
23
+ for (const b of bytes) s += String.fromCharCode(b);
24
+ return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
25
+ }
26
+
27
+ async function bits(key, info, length = 256) {
28
+ const ikm = await subtle().importKey('raw', enc.encode(String(key)), 'HKDF', false, ['deriveBits']);
29
+ return new Uint8Array(await subtle().deriveBits({ name: 'HKDF', hash: 'SHA-256', salt: SALT, info: enc.encode(info) }, ikm, length));
30
+ }
31
+
32
+ async function roomOf(proof) {
33
+ return b64u(new Uint8Array(await subtle().digest('SHA-256', enc.encode(`agentbox-room:${proof}`))));
34
+ }
35
+
36
+ /** The room the relay knows this pair by, and the Mac's proof that it is the Mac's. */
37
+ export async function relayIds(key) {
38
+ const proof = b64u(await bits(key, 'mac-proof'));
39
+ return { room: await roomOf(proof), proof };
40
+ }
41
+
42
+ /** Is `proof` the proof for `room`? The relay asks this of every Mac. */
43
+ export async function macMayJoin(room, proof) {
44
+ if (typeof room !== 'string' || typeof proof !== 'string' || !proof) return false;
45
+ const want = await roomOf(proof);
46
+ if (want.length !== room.length) return false;
47
+ let diff = 0;
48
+ for (let i = 0; i < want.length; i += 1) diff |= want.charCodeAt(i) ^ room.charCodeAt(i);
49
+ return diff === 0;
50
+ }
51
+
52
+ const aes = async (raw) => subtle().importKey('raw', raw, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']);
53
+
54
+ /** One key for each direction, so nothing sealed one way opens the other. */
55
+ export async function relayKeys(key) {
56
+ return { up: await aes(await bits(key, 'phone-to-mac')), down: await aes(await bits(key, 'mac-to-phone')) };
57
+ }
58
+
59
+ /**
60
+ * Seal `bytes` for the other end. `aad` names what it is (a request, or the
61
+ * n-th frame of the answer to one request), so a frame cannot be passed off
62
+ * as another. Out: 12 bytes of nonce, then the sealed bytes.
63
+ */
64
+ export async function seal(key, bytes, aad) {
65
+ const iv = globalThis.crypto.getRandomValues(new Uint8Array(12));
66
+ const ct = new Uint8Array(await subtle().encrypt({ name: 'AES-GCM', iv, additionalData: enc.encode(aad) }, key, bytes));
67
+ const out = new Uint8Array(12 + ct.length);
68
+ out.set(iv, 0);
69
+ out.set(ct, 12);
70
+ return out;
71
+ }
72
+
73
+ /** Open what `seal` made, or throw if a single bit of it was changed. */
74
+ export async function open(key, sealed, aad) {
75
+ if (!(sealed instanceof Uint8Array) || sealed.length < 28) throw new Error('Too short to be sealed.');
76
+ const pt = await subtle().decrypt({ name: 'AES-GCM', iv: sealed.subarray(0, 12), additionalData: enc.encode(aad) }, key, sealed.subarray(12));
77
+ return new Uint8Array(pt);
78
+ }
79
+
80
+ /* ------------------------------------------------------------- the wire */
81
+
82
+ // A request, phone to Mac, before it is sealed: four bytes giving the length of
83
+ // a json head, the head, then the body. The head carries a time and a random
84
+ // nonce, so the Mac can turn away one it has already answered.
85
+ export function packRequest(head, body = new Uint8Array(0)) {
86
+ const h = enc.encode(JSON.stringify(head));
87
+ const out = new Uint8Array(4 + h.length + body.length);
88
+ new DataView(out.buffer).setUint32(0, h.length);
89
+ out.set(h, 4);
90
+ out.set(body, 4 + h.length);
91
+ return out;
92
+ }
93
+
94
+ export function unpackRequest(bytes) {
95
+ const n = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0);
96
+ const head = JSON.parse(new TextDecoder().decode(bytes.subarray(4, 4 + n)));
97
+ return { head, body: bytes.subarray(4 + n) };
98
+ }
99
+
100
+ // An answer, Mac to phone, is a run of frames, each sealed on its own: a head
101
+ // (status and headers), the body in pieces, and an end. The end is sealed too,
102
+ // so an answer cut short by anyone in between is seen to be cut short.
103
+ //
104
+ // WAIT comes before the head and says only "still working". A reply to an
105
+ // agent mid-step is answered when the agent takes it, which can be minutes,
106
+ // and the room gives a Mac 30 seconds to start answering. A phone that asks
107
+ // for these (`wait` in its request's head) skips them; one that does not
108
+ // never gets one.
109
+ export const FRAME = Object.freeze({ HEAD: 0, DATA: 1, END: 2, WAIT: 3 });
110
+
111
+ export const frameAad = (nonce, seq) => `res:${nonce}:${seq}`;
112
+
113
+ // ON THE MAC'S SOCKET TO THE RELAY one byte says what a message is, then eight
114
+ // bytes of request number, then whatever it carries:
115
+ // to the Mac 0x01 a request (the sealed bytes) 0x02 the phone gave up
116
+ // 0x03 a piece of a request too big for one message; the
117
+ // 0x01 that follows carries the last piece
118
+ // from the Mac 0x10 one sealed frame of the answer 0x11 done
119
+ // 0x12 refused: it would not open, or it was a replay
120
+ export const TO_MAC = Object.freeze({ REQUEST: 0x01, CANCEL: 0x02, PART: 0x03 });
121
+ /** The most the relay puts in one socket message to the Mac. Cloudflare's limit is 1 MB. */
122
+ export const PART_BYTES = 512 * 1024;
123
+ export const FROM_MAC = Object.freeze({ FRAME: 0x10, DONE: 0x11, REFUSED: 0x12 });
124
+ export const ID_BYTES = 8;
125
+
126
+ export function tag(type, id, rest = new Uint8Array(0)) {
127
+ const out = new Uint8Array(1 + ID_BYTES + rest.length);
128
+ out[0] = type;
129
+ out.set(id, 1);
130
+ out.set(rest, 1 + ID_BYTES);
131
+ return out;
132
+ }
133
+
134
+ // Between the relay and either end each sealed piece goes with its length in
135
+ // front, so one stream of bytes can carry many.
136
+ export function lengthPrefixed(bytes) {
137
+ const out = new Uint8Array(4 + bytes.length);
138
+ new DataView(out.buffer).setUint32(0, bytes.length);
139
+ out.set(bytes, 4);
140
+ return out;
141
+ }
142
+
143
+ /** Split a stream (or a reader of one) of length-prefixed pieces back into the pieces. */
144
+ export async function* pieces(source) {
145
+ const reader = typeof source.getReader === 'function' ? source.getReader() : source;
146
+ let buf = new Uint8Array(0);
147
+ try {
148
+ for (;;) {
149
+ while (buf.length >= 4) {
150
+ const n = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(0);
151
+ if (buf.length < 4 + n) break;
152
+ yield buf.slice(4, 4 + n);
153
+ buf = buf.subarray(4 + n);
154
+ }
155
+ const { value, done } = await reader.read();
156
+ if (done) return;
157
+ const next = new Uint8Array(buf.length + value.length);
158
+ next.set(buf, 0);
159
+ next.set(value, buf.length);
160
+ buf = next;
161
+ }
162
+ } finally {
163
+ try { reader.releaseLock(); } catch { /* cancelled under us */ }
164
+ }
165
+ }
@@ -0,0 +1,10 @@
1
+ export interface RunsOn {
2
+ /** 'claude' or 'codex', or null on a Mac where nothing is signed in. */
3
+ engine?: string | null;
4
+ }
5
+
6
+ /** The word for the plan behind an engine, or null for one we cannot name. */
7
+ export declare function runsOnName(engine?: string | null): string | null;
8
+
9
+ /** The one line the walk says on a Mac that was already set up. Null for null. */
10
+ export declare function foundOnThisMac(facts?: RunsOn): string | null;
@@ -0,0 +1,55 @@
1
+ // THE ONE LINE SAYING IT FOUND AN ACCOUNT ALREADY ON THIS MAC.
2
+ //
3
+ // From a session with a new user, 2026-10-07, in her words: a new user came in,
4
+ // started running an agent, and was "like, 'Oh my God, where are the tokens
5
+ // coming from? I didn't even connect my account.'"
6
+ //
7
+ // He was right that nothing said. The app finds whichever coding agent is
8
+ // already signed in on the Mac and runs every worker on that subscription
9
+ // (main/claude-plan.mjs, main/codex-account.mjs), and the walk is SILENT about
10
+ // it by design: `needsPlan` in renderer/src/plan-setup.ts skips the plan
11
+ // question entirely when one tool is found and signed in, which is the right
12
+ // call and leaves nobody told.
13
+ //
14
+ // IT IS A ONE-TIME SURPRISE, SO IT GETS A ONE-TIME ANSWER. A row saying this
15
+ // permanently, in the sidebar's foot, is what this file held first; it shipped
16
+ // on 2026-10-07 and came out the same day. Her words: "we have to get rid of
17
+ // this. It should never say that on the sidebar." Somebody asks where the
18
+ // tokens are coming from ONCE, on their first day, so the place for the answer
19
+ // is the walk they are standing in.
20
+ //
21
+ // AND IT NAMES NO PLAN. The row said "Claude · Max 20x" and that was the half
22
+ // she wanted gone most: "very critically, I want to get rid of that Claude Max
23
+ // 20x plan." The question being answered is "did it connect to something of
24
+ // mine", not "what am I paying for", and the second one is already answered
25
+ // properly on each agent's own page in Settings, where somebody goes when they
26
+ // want it.
27
+ //
28
+ // THE PLAN'S VOCABULARY, NOT THE TOOL'S. "Claude" and "ChatGPT" here, never
29
+ // "Claude Code" and "Codex", which is the rule the walk already follows
30
+ // (renderer/src/plan-setup.ts: words people know, "never the names of the tools
31
+ // underneath"). The tool names stay where they belong: on a byline about which
32
+ // agent is working a row (`engineWordFor`, renderer/src/byline.ts).
33
+ //
34
+ // NOTHING IS SAID UNTIL SOMETHING IS KNOWN. Both functions answer null on an
35
+ // engine they cannot name, and the walk draws nothing at all for null rather
36
+ // than "checking…" or a dash.
37
+
38
+ /** The word for the plan behind an engine. Null for an engine we cannot name. */
39
+ export function runsOnName(engine) {
40
+ if (engine === 'claude') return 'Claude';
41
+ if (engine === 'codex') return 'ChatGPT';
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * THE ONE LINE DURING THE WALK, on the Mac where there was nothing to ask.
47
+ *
48
+ * It replaces silence, not a question: somebody with no coding agent set up
49
+ * still gets the plan question and the setup card, and never sees this.
50
+ */
51
+ export function foundOnThisMac({ engine = null } = {}) {
52
+ const name = runsOnName(engine);
53
+ if (!name) return null;
54
+ return `Your agents will run on the ${name} account already signed in on this Mac.`;
55
+ }
@@ -166,8 +166,8 @@ export const SHORTCUTS = [
166
166
  ],
167
167
  },
168
168
  {
169
- // The one group that cannot be named by WHERE she is, because all four of
170
- // its keys work both in the list and inside an open task. So it is named by
169
+ // The one group that cannot be named by WHERE she is, because all of its
170
+ // keys work both in the list and inside an open task. So it is named by
171
171
  // what the keys do, literally and in the order they are drawn.
172
172
  label: 'Closing, scheduling and undoing',
173
173
  keys: [
@@ -179,10 +179,16 @@ export const SHORTCUTS = [
179
179
  // the summary inside a thread and still meant the schedule picker in the
180
180
  // list, so this page taught one letter for two different things and an
181
181
  // office manager testing the app met both inside a minute. One meaning
182
- // per letter: S is the summary, L is later.
183
- { keys: ['L'], what: 'Put the task off until a time you pick.' },
182
+ // per letter: S is the summary, L is later. And the page says the word
183
+ // the key stands for, because that word is the action's name now
184
+ // (w-66e3ece93d): the ⌘K row, the picker and the toast all say Later.
185
+ { keys: ['L'], what: 'Later: put the task off until a time you pick.' },
184
186
  // focused branch and list switch: 'z' → undo
185
187
  { keys: ['Z'], what: 'Undo the last thing you did.' },
188
+ // opensTheToast(e.key, toast) above the focused branch → openToastRow.
189
+ // Z's other half: Z takes back what the toast announced, O goes into it.
190
+ // Only while a toast with somewhere to go is up (w-f0bfe32859).
191
+ { keys: ['O'], what: 'Open the task you just sent or answered, while its note is on screen.' },
186
192
  // (meta||ctrl) && 'a' && !inInput && !modal → tick the whole list
187
193
  { keys: ['⌘A'], what: 'Tick every task in the list. Press it again to untick.' },
188
194
  ],
@@ -192,7 +198,7 @@ export const SHORTCUTS = [
192
198
  keys: [
193
199
  // (meta||ctrl) && 'k' → the palette, above every other branch, and
194
200
  // forwarded from inside an open file by main.mjs as well.
195
- { keys: ['⌘K'], what: `The command bar: everything ${NAME} can do, by name.` },
201
+ { keys: ['⌘K', 'Ctrl+K'], join: 'or', what: `The command bar: everything ${NAME} can do, by name.` },
196
202
  // 'n' in both branches → setModal('compose'). C does the same and is
197
203
  // left off: N is the default (w-fb9051e597).
198
204
  // "Threads", not tasks, since w-ec62ab6b38.