agentbox-app 0.1.13 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
package/main/store.mjs CHANGED
@@ -8,8 +8,10 @@ import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import { loadStore } from './store-modules.mjs';
10
10
  import { Repeats } from './repeats.mjs';
11
+ import { CLEAN_LABEL, isCleanRun } from '../shared/repeats.mjs';
11
12
  import { answerFor, describeFor } from './first-run.mjs';
12
13
  import { checkProjectFolder } from '../shared/project-folder-check.mjs';
14
+ import { SAME_PROJECT, movedAway, movedHere, splitForCompose, whatMoves, whyItCannotMove } from '../shared/move-thread.mjs';
13
15
  import { EXAMPLES, stampsFor } from '../shared/first-run-examples.mjs';
14
16
  import {
15
17
  PRACTICE_ANSWER, PRACTICE_BACKDROP, PRACTICE_FLAG, PRACTICE_NAME, PRACTICE_NOTE, PRACTICE_ROWS, PRACTICE_SLUG,
@@ -29,12 +31,21 @@ import {
29
31
  } from '../shared/codex-import.mjs';
30
32
  import { buildClaimLine, buildReleaseLine } from '../shared/work-items.mjs';
31
33
  import { isEffortWord } from '../shared/effort-levels.mjs';
34
+ import { shippedNote } from '../shared/shipped-note.mjs';
32
35
  import { machineryPath, removeMachinery } from './store/home.mjs';
36
+ import { Teammates } from './teammates.mjs';
33
37
  import {
34
38
  answerMode as readAnswerMode, answerModes, clearAnswerMode as clearAnswerModeFile, setAnswerMode,
35
39
  touchAnswerMode as touchAnswerModeFile,
36
40
  } from './answer-modes.mjs';
37
41
 
42
+ // WHAT A ROW'S ID LOOKS LIKE, for the reads that take one from the window: a
43
+ // task, `w-` and hex, or a run of a repeating task, `r-<rule>-<YYYYMMDD>`
44
+ // (shared/repeats.mjs occurrenceId). Runs were refused until a repeating task
45
+ // began opening as its latest run (w-5e3f2e047f), and a run's page then had no
46
+ // conversation to draw.
47
+ const ROW_ID = /^(w-[0-9a-f]{6,}|r-[0-9a-f]{6,}-\d{8})$/;
48
+
38
49
  export class Store {
39
50
  constructor(config) {
40
51
  this.config = config;
@@ -48,6 +59,7 @@ export class Store {
48
59
  async init() {
49
60
  this.modules = await loadStore();
50
61
  this.repeats = new Repeats(this.modules);
62
+ this.teammates = this.config?.storeRoot && this.config?.accountId ? new Teammates(this) : null;
51
63
  // AND A PRACTICE PROJECT LEFT OVER FROM LAST TIME GOES NOW.A walk that was
52
64
  // quit, or a Mac that went to sleep in the middle of one, leaves the
53
65
  // practice project sitting in the account root, and everything downstream
@@ -108,6 +120,9 @@ export class Store {
108
120
  name: project.name || entry.name,
109
121
  oneLiner: project.oneLiner || '',
110
122
  repoPath: project.repoPath || null,
123
+ // What puts an unplaced project under the ones already there rather
124
+ // than alphabetically among them (renderer/src/project-priority.ts).
125
+ createdAt: Number.isFinite(Date.parse(project.createdAt)) ? Date.parse(project.createdAt) : null,
111
126
  // THE MARK SHE CHOSE FOR THIS PROJECT, already a url the window can
112
127
  // draw, or null on every project that has never been given one, which is
113
128
  // what makes the rail draw its burst instead (main/project-identity.mjs).
@@ -244,19 +259,13 @@ export class Store {
244
259
  // The lease lines go here rather than there because they are the bulk of the
245
260
  // file and none of them are news: a worker holding a task for an hour writes
246
261
  // a heartbeat a minute, and's 39 lines are 15 events.
247
- readHistory(slug, id, { max = 400 } = {}) {
248
- const { workItemsDisk } = this.modules;
249
- if (!/^w-[0-9a-f]{6,}$/.test(String(id ?? ''))) return [];
250
- const file = workItemsDisk._internals.ledgerPath(this.productDir(slug));
251
- const lines = [];
252
- for (const raw of workItemsDisk._internals.readLines(file)) {
253
- let line = null;
254
- try { line = JSON.parse(raw); } catch { continue; }
255
- if (!line || line.id !== id) continue;
256
- if (line.heartbeat || line.release) continue;
257
- lines.push(line);
258
- }
259
- return lines.slice(-max);
262
+ //
263
+ // Read once per change of the ledger, not once per call (main/store/
264
+ // work-items.mjs readItemHistory): the phone pass asks for 300 threads in a
265
+ // row, and reading the file for each blocked the app for 21 s (2026-10-14).
266
+ readHistory(slug, id, { max = 400, before } = {}) {
267
+ if (!ROW_ID.test(String(id ?? ''))) return [];
268
+ return this.modules.workItemsDisk.readItemHistory(this.productDir(slug), id, { max, before });
260
269
  }
261
270
 
262
271
  // THE CONVERSATION ON ONE ROW, oldest first, for the brief a spawning worker
@@ -269,7 +278,7 @@ export class Store {
269
278
  // here would silently decide it twice.
270
279
  readThread(slug, id) {
271
280
  const { workItemsDisk, workItemsCore } = this.modules;
272
- if (!/^w-[0-9a-f]{6,}$/.test(String(id ?? ''))) return [];
281
+ if (!ROW_ID.test(String(id ?? ''))) return [];
273
282
  const file = workItemsDisk._internals.ledgerPath(this.productDir(slug));
274
283
  return workItemsCore.threadOf(workItemsDisk._internals.readLines(file), id);
275
284
  }
@@ -328,7 +337,7 @@ export class Store {
328
337
  // `inReplyTo` makes the answer a reply in a thread (w-920461cbe6): the uid
329
338
  // of the message it answers, on THE SAME LINE as the words, so the reply's
330
339
  // own uid (what its reactions hang off) is the line that says where it goes.
331
- answerItem(slug, id, { answer, status, priority, permissionMode, model, effort, inReplyTo }) {
340
+ answerItem(slug, id, { answer, status, priority, permissionMode, engine, model, effort, inReplyTo, fromPhone }) {
332
341
  const { workItemsDisk } = this.modules;
333
342
  const dir = this.productDir(slug);
334
343
  let item = null;
@@ -341,9 +350,25 @@ export class Store {
341
350
  // is said everywhere else here: `modelForEngine` (shared/engines.mjs) reads
342
351
  // the empty string as nothing chosen, where a JSON null would have to be
343
352
  // special-cased in both engines' argument builders.
353
+ // AND WHICH ENGINE, BESIDE IT AND FOR THE SAME REASON (w-c9da359466). The
354
+ // reply box's drawer only ever offered the models of the engine the row
355
+ // was already on, so a thread begun on Claude Code could not be carried on
356
+ // in Codex by any path in the app: "that is what I'd reach for when Claude
357
+ // runs out" (power-test-2, issues 39 and 40). It is the field `composeItem`
358
+ // already writes and `Supervisor#_engineFor` already reads per run, so the
359
+ // next turn picks it up and nothing else has to know.
360
+ if (engine !== undefined) item = workItemsDisk.updateWorkItem(dir, id, { engine: engine ?? '' }, { source: 'founder' });
344
361
  if (model !== undefined) item = workItemsDisk.updateWorkItem(dir, id, { model: model ?? '' }, { source: 'founder' });
345
362
  if (effort !== undefined) item = workItemsDisk.updateWorkItem(dir, id, { effort: effort ?? '' }, { source: 'founder' });
346
- if (answer) item = workItemsDisk.updateWorkItem(dir, id, inReplyTo ? { answer, inReplyTo } : { answer }, { source: 'founder' });
363
+ if (answer) item = workItemsDisk.updateWorkItem(dir, id, { answer, ...(inReplyTo ? { inReplyTo } : {}), ...(fromPhone ? { fromPhone } : {}) }, { source: 'founder' });
364
+ // A REPLY ON A RUN THAT FINISHED QUIETLY MAKES IT NEWS AGAIN (w-5e3f2e047f).
365
+ // A quiet run is kept out of her inbox, and a repeating task now opens on
366
+ // its latest run, so without this the answer to what she just asked there
367
+ // could never reach her. Written as the app rather than as her, so the
368
+ // worker can still mark a later quiet finish of its own.
369
+ if (answer && answer !== '(withdrawn)' && isCleanRun(item)) {
370
+ item = workItemsDisk.updateWorkItem(dir, id, { labels: item.labels.filter((l) => l !== CLEAN_LABEL) }, { source: 'system' });
371
+ }
347
372
  // The pane may still show a running row after Stop or a provider result.
348
373
  // Resolve omitted reply status against the ledger that was just written.
349
374
  if (status == null && typeof answer === 'string' && answer.trim() && answer !== '(withdrawn)'
@@ -389,15 +414,17 @@ export class Store {
389
414
  shipped(slug, id, { sha, labels }) {
390
415
  const { workItemsDisk } = this.modules;
391
416
  const dir = this.productDir(slug);
392
- const note = sha ? `Shipped to main as ${sha}.` : 'Shipped to main.';
393
- return workItemsDisk.updateWorkItem(dir, id, { labels, note }, { source: 'system' });
417
+ return workItemsDisk.updateWorkItem(dir, id, { labels, note: shippedNote(sha) }, { source: 'system' });
394
418
  }
395
419
 
396
420
  shipFailed(slug, id, { note, labels }) {
397
421
  const { workItemsDisk } = this.modules;
398
422
  const dir = this.productDir(slug);
399
423
  workItemsDisk.updateWorkItem(dir, id, { labels, note }, { source: 'system' });
400
- return workItemsDisk.updateWorkItem(dir, id, { status: 'open' }, { source: 'system' });
424
+ // A row the person closed while it waited to ship is theirs, and only
425
+ // their own Reopen outranks that (see `closedBeforeItShipped` below).
426
+ const closed = this.readItem(slug, id)?.status === 'done';
427
+ return workItemsDisk.updateWorkItem(dir, id, { status: 'open' }, { source: closed ? 'founder' : 'system' });
401
428
  }
402
429
 
403
430
  releaseRunClaim(slug, id, run) {
@@ -509,6 +536,13 @@ export class Store {
509
536
  );
510
537
  }
511
538
 
539
+ // NEW WORDS FOR A MESSAGE YOU SENT, OR THE MESSAGE TAKEN BACK. The fold only
540
+ // believes it from the message's own author (shared/message-edits.mjs).
541
+ editMessage(slug, id, { on, text, gone = false }) {
542
+ const edit = gone ? { on, gone: true } : { on, text };
543
+ return this.modules.workItemsDisk.updateWorkItem(this.productDir(slug), id, { edit }, { source: 'founder' });
544
+ }
545
+
512
546
  // AN EDIT TO A THREAD FROM ITS SUMMARY (approved 2026-10-01): the summary's
513
547
  // three lines and its links, who sees it, and its priority. Written as the
514
548
  // person's own words; on the summary the later write wins either way
@@ -516,24 +550,33 @@ export class Store {
516
550
  threadEdit(slug, id, patch) {
517
551
  // `start` is how a thread leaves Later: the summary and the thread page
518
552
  // both write 'now' on it, which is what makes `isDue` true again.
519
- const fields = ['problem', 'progress', 'solution', 'visibility', 'visibleTo', 'priority', 'blockedBy', 'blocks', 'start'];
553
+ // `label` is a rename (w-ab3d51dc7d): the name every row shows. Never
554
+ // `title`, which is the message the thread was started with and briefs
555
+ // every run. Written on her authority, it outranks the namer's.
556
+ const fields = ['problem', 'progress', 'solution', 'visibility', 'visibleTo', 'priority', 'blockedBy', 'blocks', 'start', 'label'];
520
557
  const allowed = Object.fromEntries(Object.entries(patch ?? {}).filter(([k]) => fields.includes(k)));
558
+ if ('label' in allowed) {
559
+ allowed.label = String(allowed.label ?? '').replace(/\s+/g, ' ').trim();
560
+ if (!allowed.label) throw new Error('a thread needs a name');
561
+ }
521
562
  if (!Object.keys(allowed).length) throw new Error('nothing to change');
522
563
  return this.modules.workItemsDisk.updateWorkItem(this.productDir(slug), id, allowed, { source: 'founder' });
523
564
  }
524
565
 
525
- composeItem(slug, { title, body, kind = 'directive', priority = 0, labels, runAt, start, engine, model, effort, assignee, due, people, visibility, visibleTo }) {
566
+ composeItem(slug, { id = null, title, body, kind = 'directive', priority = 0, labels, runAt, start, engine, model, effort, assignee, due, people, visibility, visibleTo, fromPhone }) {
526
567
  const { workItemsDisk } = this.modules;
527
568
  const dir = this.productDir(slug);
528
569
  // The 'founder' label is the human-in-the-loop marker: only items the
529
570
  // founder composed (or explicitly approved) execute; agent-filed items
530
571
  // are proposals until then.
531
572
  const allLabels = [...new Set(['founder', ...(labels ?? [])])];
532
- const created = workItemsDisk.createWorkItem(
533
- dir,
534
- { title, kind, priority, labels: allLabels },
535
- { source: 'system' }
536
- );
573
+ // The phone command that asked for it, on the very line that makes the
574
+ // row, so no row it made is ever without it (Codex's fourth review).
575
+ const first = { title, kind, priority, labels: allLabels, ...(fromPhone ? { fromPhone: String(fromPhone) } : {}) };
576
+ // A thread the cloud started from the phone keeps the id it ran under there.
577
+ const created = id
578
+ ? workItemsDisk.createWorkItemIfAbsent(dir, id, first, { source: 'system' }).item
579
+ : workItemsDisk.createWorkItem(dir, first, { source: 'system' });
537
580
  // The founder's words carry founder authority; the lifecycle does not.
538
581
  // A runAt sent with the compose is "send later": the item exists now and
539
582
  // starts then, which is why it rides in with her content rather than after.
@@ -588,6 +631,57 @@ export class Store {
588
631
  return made ? { ...made, product: slug } : made;
589
632
  }
590
633
 
634
+ /**
635
+ * MOVE A THREAD TO ANOTHER PROJECT (w-c9da359466, power-test-1 issue 20).
636
+ *
637
+ * Only a thread nothing has ever run on, and shared/move-thread.mjs carries
638
+ * the whole of why: the rest of a thread is its session traces, its task
639
+ * folder and the git branch named after it, and a branch cannot change
640
+ * repository.
641
+ *
642
+ * THE RULE IS ASKED AGAIN HERE. ⌘K asked it to decide which rows to draw,
643
+ * off a snapshot that can be seconds old, and a worker that picked the
644
+ * thread up in those seconds would otherwise have the ground moved under it.
645
+ *
646
+ * THE NEW ROW IS WRITTEN BEFORE THE OLD ONE IS CLOSED. A close that landed
647
+ * first and a create that then failed on a full disk would be a thread that
648
+ * is in neither project, which is the one outcome this must not have.
649
+ */
650
+ moveItem(fromSlug, id, toSlug) {
651
+ if (fromSlug === toSlug) throw new Error(SAME_PROJECT);
652
+ const from = this.listProducts().find((p) => p.slug === fromSlug);
653
+ const to = this.listProducts().find((p) => p.slug === toSlug);
654
+ if (!to) throw new Error(`no such project: ${toSlug}`);
655
+ const item = this.readItem(fromSlug, id);
656
+ const why = whyItCannotMove(item, { live: false });
657
+ if (why) throw new Error(why);
658
+
659
+ const { compose, after } = splitForCompose(whatMoves(item));
660
+ // Where it came from, in the thread rather than only in a toast, because a
661
+ // thread that turns up in a project nobody filed it in is a thread nobody
662
+ // can account for a week later.
663
+ const said = movedHere(from?.name ?? fromSlug);
664
+ const body = compose.body ? `${compose.body}\n\n${said}` : said;
665
+ let made = this.composeItem(toSlug, { ...compose, body });
666
+ // The three `composeItem` has no argument for (shared/move-thread.mjs).
667
+ if (Object.keys(after).length) {
668
+ made = { ...this.modules.workItemsDisk.updateWorkItem(this.productDir(toSlug), made.id, after, { source: 'founder' }), product: toSlug };
669
+ }
670
+
671
+ // AND THE OLD ROW CLOSES SAYING WHERE ITS THREAD WENT. It is written as
672
+ // the app rather than as the founder: they moved a thread, they did not
673
+ // finish one, and a founder `done` is terminal in a way this need not be.
674
+ const note = movedAway(to.name ?? toSlug);
675
+ this.modules.workItemsDisk.updateWorkItem(this.productDir(fromSlug), id, { status: 'done', note }, { source: 'system' });
676
+ return { item: made, from: from?.name ?? fromSlug, to: to.name ?? toSlug, note };
677
+ }
678
+
679
+ /* ------------------------------- teammates ------------------------------ */
680
+ teammatesView(items, now = Date.now()) {
681
+ if (!this.teammates) return null;
682
+ return this.teammates.view(items ?? this.listItems(now), now);
683
+ }
684
+
591
685
  /* ---------------------------- repeating tasks --------------------------- */
592
686
  // Every product's rules, with their product on each, the way listItems does
593
687
  // it, so the renderer has one list to draw. Ended rules are gone from her
@@ -632,13 +726,52 @@ export class Store {
632
726
  // An ordinary agent-filed item: a PROPOSAL, which is what puts it in her
633
727
  // inbox and keeps it there until she answers. Used for the miss alert, which
634
728
  // is news about a repeating task rather than work she asked for.
635
- fileItem(slug, { title, body, kind = 'review', priority = 5, labels = [] }) {
729
+ // `problem` is the summary's Context, the few sentences a reader gets before
730
+ // they open anything. Without one the panel falls back to the first sentence
731
+ // of the body (summaryOf, shared/thread-cards.mjs), which on a row whose body
732
+ // is a list reads as that list with its line breaks taken out.
733
+ fileItem(slug, { title, body, problem, kind = 'review', priority = 5, labels = [] }) {
636
734
  const { workItemsDisk } = this.modules;
637
735
  return workItemsDisk.createWorkItem(
638
- this.productDir(slug), { title, body, kind, priority, labels }, { source: 'agent' },
736
+ this.productDir(slug), { title, body, problem, kind, priority, labels }, { source: 'agent' },
639
737
  );
640
738
  }
641
739
 
740
+ // NEWS THE APP ITSELF HAS, ON A ROW THAT ALREADY EXISTS. The app's own words,
741
+ // so 'system' and a note: the answer is theirs and the result is the agent's.
742
+ // A note is a thread field, so a second one reads as the next thing said on
743
+ // the row rather than overwriting the first. Used by main/main-went-red.mjs
744
+ // to say main is green again on the row that reported the red.
745
+ noteItem(slug, id, note) {
746
+ const { workItemsDisk } = this.modules;
747
+ return workItemsDisk.updateWorkItem(this.productDir(slug), id, { note }, { source: 'system' });
748
+ }
749
+
750
+ // A CLOUD RUN'S WORK KEPT ON A BRANCH OF ITS OWN, because laying it into the
751
+ // task folder would have undone work done there (main/cloud/run.mjs
752
+ // `park`). Said as a note; the row waits for her, and loses `ship`, so the
753
+ // folder never ships without the run's work.
754
+ cloudWorkKeptAside(slug, id, note) {
755
+ const { workItemsDisk } = this.modules;
756
+ const dir = this.productDir(slug);
757
+ const labels = this.readItem(slug, id)?.labels ?? [];
758
+ const kept = labels.filter((l) => l !== 'ship');
759
+ workItemsDisk.updateWorkItem(dir, id, { note, ...(kept.length !== labels.length ? { labels: kept } : {}) }, { source: 'system' });
760
+ return workItemsDisk.updateWorkItem(dir, id, { status: 'blocked' }, { source: 'system' });
761
+ }
762
+
763
+ // A CLOSED ROW WHOSE WORK NEVER REACHED MAIN, put back in front of the
764
+ // person. Their close outranks the app's own writes, so it reopens the way
765
+ // their Reopen does (`authorityOf` records that as the app's), then waits on
766
+ // them so no run starts by itself.
767
+ closedBeforeItShipped(slug, id, note) {
768
+ const { workItemsDisk } = this.modules;
769
+ const dir = this.productDir(slug);
770
+ workItemsDisk.updateWorkItem(dir, id, { note }, { source: 'system' });
771
+ workItemsDisk.updateWorkItem(dir, id, { status: 'open' }, { source: 'founder' });
772
+ return workItemsDisk.updateWorkItem(dir, id, { status: 'blocked' }, { source: 'system' });
773
+ }
774
+
642
775
  // New product: rare and deliberate. A directory, a project.json, and a first
643
776
  // work item that seeds the drive toward launch.
644
777
  //
@@ -938,9 +1071,11 @@ export class Store {
938
1071
  * its order, which is how `walkRows` knows which one is which.
939
1072
  *
940
1073
  * The column is the app's own reading of the row, so each is written in the
941
- * shape that reads that way and nothing marks it: a finished one has a
942
- * result, a working one is her ask with a run and no result yet, and the
943
- * scheduled one is her ask with a time in the future, set as hers. */
1074
+ * shape that reads that way and nothing marks it: a working one is the ask
1075
+ * with a run and no result yet, and a scheduled one is the ask with a time
1076
+ * in the future, set as hers. None of them carries a result, because a
1077
+ * result is what puts a row back in the inbox and nothing here may land
1078
+ * there (shared/first-run-practice.mjs). */
944
1079
  stagePracticeBackdrop(slug, { now = Date.now(), label = 'first-run' } = {}) {
945
1080
  const { workItemsDisk } = this.modules;
946
1081
  const dir = this.productDir(slug);
@@ -961,12 +1096,6 @@ export class Store {
961
1096
  this.appendPracticeRun(slug, created.id, at.hers);
962
1097
  this.writePracticeTrace(slug, created.id, row.trace, at.hers + 1_000);
963
1098
  }
964
- if (row.state === 'needs') {
965
- workItemsDisk.updateWorkItem(
966
- dir, created.id, { result: row.result },
967
- { source: 'agent', now: at.hers + 1_000 + row.trace.length * 9_000 + 1_000 },
968
- );
969
- }
970
1099
  ids.push(created.id);
971
1100
  }
972
1101
  return { ids };
@@ -1191,29 +1320,35 @@ export class Store {
1191
1320
  * Stamped a second apart, oldest last, so the newest thread on the card is
1192
1321
  * the newest row in the inbox. `chosen` arrives newest first from
1193
1322
  * `readSessionThreads`. */
1194
- importThreadRows(slug, chosen, { now = Date.now() } = {}) {
1323
+ importThreadRows(slug, chosen, { now = Date.now(), land = 'inbox' } = {}) {
1195
1324
  const { workItemsDisk } = this.modules;
1196
1325
  const dir = this.productDir(slug);
1197
- const product = this.listProducts().find((p) => p.slug === slug) ?? null;
1198
1326
  let existing = [];
1199
1327
  try { existing = workItemsDisk.readWorkItems(dir, now); } catch { /* a fresh project has none */ }
1200
1328
  // Codex conversations on the same card are importCodexThreads's, below.
1201
1329
  const claude = (chosen ?? []).filter((t) => t?.source !== 'codex');
1202
1330
  const wanted = threadsNeedingRows(claude, existing);
1203
1331
  const ids = [];
1332
+ let done = 0;
1204
1333
  wanted.forEach((thread, i) => {
1205
- const at = now - i * 1000;
1206
- const row = threadImportRow(thread, { projectName: product?.name ?? null, now });
1334
+ const { at, last } = chatTimes(thread, now - i * 1000);
1335
+ const row = threadImportRow(thread);
1207
1336
  const created = workItemsDisk.createWorkItem(
1208
1337
  dir,
1209
1338
  { title: row.title, kind: row.kind, priority: row.priority, labels: row.labels },
1210
1339
  { source: 'agent', now: at },
1211
1340
  );
1212
- workItemsDisk.updateWorkItem(dir, created.id, { body: row.body }, { source: 'agent', now: at });
1341
+ // Your words draw under You and the agent's under the agent.
1342
+ workItemsDisk.updateWorkItem(dir, created.id, { body: row.body }, { source: 'founder', now: at });
1343
+ if (row.result) workItemsDisk.updateWorkItem(dir, created.id, { result: row.result }, { source: 'agent', now: last + 1 });
1344
+ if (land === 'sorted' && !thread.waiting) {
1345
+ workItemsDisk.updateWorkItem(dir, created.id, { status: 'done' }, { source: 'founder', now: last + 2 });
1346
+ done += 1;
1347
+ }
1213
1348
  ids.push(created.id);
1214
1349
  });
1215
1350
  const seen = new Set(claude.map((t) => String(t?.id ?? '').trim()).filter(Boolean));
1216
- return { ids, added: ids.length, already: seen.size - ids.length };
1351
+ return { ids, added: ids.length, already: seen.size - ids.length, done };
1217
1352
  }
1218
1353
 
1219
1354
  /* ---------------------------- codex conversations ----------------------- */
@@ -1296,7 +1431,7 @@ export class Store {
1296
1431
  * the body, Codex's last message as the result, open, in her inbox. No closes
1297
1432
  * it under `not-imported`, which is where Closed finds it to offer it back.
1298
1433
  * Yes on a row she once said no to is the same yes. */
1299
- answerCodexImport(slug, id, choice, { thread = null, now = Date.now() } = {}) {
1434
+ answerCodexImport(slug, id, choice, { thread = null, now = Date.now(), bodyAt = now } = {}) {
1300
1435
  const { workItemsDisk } = this.modules;
1301
1436
  const dir = this.productDir(slug);
1302
1437
  const item = workItemsDisk.readWorkItem(dir, id, now);
@@ -1309,15 +1444,18 @@ export class Store {
1309
1444
  const t = { ...this.codexThreadFromRow(item), ...(thread ?? {}) };
1310
1445
  const patch = codexMirrorPatch(t);
1311
1446
  // it draws under You, not under an agent's name.
1312
- workItemsDisk.updateWorkItem(dir, id, { body: patch.body }, { source: 'founder', now });
1313
- workItemsDisk.updateWorkItem(dir, id, { kind: patch.kind, labels: patch.labels, result: patch.result }, { source: 'agent', now: now + 1 });
1447
+ workItemsDisk.updateWorkItem(dir, id, { body: patch.body }, { source: 'founder', now: bodyAt });
1448
+ const mirrored = workItemsDisk.updateWorkItem(dir, id, { kind: patch.kind, labels: patch.labels, result: patch.result }, { source: 'agent', now: now + 1 });
1449
+ // Only a row that was not open is reopened; on an open one the line reads
1450
+ // as "You sent it back" in the thread.
1451
+ if (item.status === 'open') return mirrored;
1314
1452
  return workItemsDisk.updateWorkItem(dir, id, { status: 'open' }, { source: 'founder', now: now + 1 });
1315
1453
  }
1316
1454
 
1317
1455
  /**
1318
1456
  * THE ⌘K CARD'S PRESS on Codex conversations: each becomes a readable row at
1319
1457
  * once, whether it had an asking row, a declined one, or none. */
1320
- importCodexThreads(slug, chosen, { now = Date.now() } = {}) {
1458
+ importCodexThreads(slug, chosen, { now = Date.now(), land = 'inbox' } = {}) {
1321
1459
  const { workItemsDisk } = this.modules;
1322
1460
  const dir = this.productDir(slug);
1323
1461
  const product = this.listProducts().find((p) => p.slug === slug) ?? null;
@@ -1325,6 +1463,7 @@ export class Store {
1325
1463
  try { existing = workItemsDisk.readWorkItems(dir, now); } catch { existing = []; }
1326
1464
  const ids = [];
1327
1465
  let already = 0;
1466
+ let done = 0;
1328
1467
  for (const thread of chosen ?? []) {
1329
1468
  if (thread?.source !== 'codex') continue; // a Claude Code thread is importThreadRows's
1330
1469
  const id = String(thread?.id ?? '').trim();
@@ -1332,20 +1471,28 @@ export class Store {
1332
1471
  const had = existing.find((i) => codexIdOf(i) === id);
1333
1472
  if (had && isCodexMirrorRow(had) && had.status !== 'done') { already += 1; continue; }
1334
1473
  let rowId = had?.id ?? null;
1335
- if (!rowId) {
1474
+ const fresh = !rowId;
1475
+ // A row it already had keeps the import's time: an older line would lose
1476
+ // to what that row already says.
1477
+ const { at, last } = fresh ? chatTimes(thread, now) : { at: now, last: now };
1478
+ if (fresh) {
1336
1479
  const row = codexImportRow(thread, { projectName: product?.name ?? null, now });
1337
1480
  const created = workItemsDisk.createWorkItem(
1338
1481
  dir,
1339
1482
  { title: row.title, kind: row.kind, priority: row.priority, labels: row.labels },
1340
- { source: 'agent', now },
1483
+ { source: 'agent', now: at },
1341
1484
  );
1342
- workItemsDisk.updateWorkItem(dir, created.id, { body: row.body }, { source: 'agent', now });
1485
+ workItemsDisk.updateWorkItem(dir, created.id, { body: row.body }, { source: 'agent', now: at });
1343
1486
  rowId = created.id;
1344
1487
  }
1345
- this.answerCodexImport(slug, rowId, 'yes', { thread, now: now + 2 });
1488
+ this.answerCodexImport(slug, rowId, 'yes', { thread, now: last + 2, bodyAt: fresh ? at + 1 : last + 2 });
1489
+ if (land === 'sorted' && !thread.waiting) {
1490
+ workItemsDisk.updateWorkItem(dir, rowId, { status: 'done' }, { source: 'founder', now: last + 4 });
1491
+ done += 1;
1492
+ }
1346
1493
  ids.push(rowId);
1347
1494
  }
1348
- return { ids, added: ids.length, already };
1495
+ return { ids, added: ids.length, already, done };
1349
1496
  }
1350
1497
 
1351
1498
  /**
@@ -1380,6 +1527,8 @@ export class Store {
1380
1527
  let refreshed = 0;
1381
1528
  for (const item of this.listAllWorkItems(now)) {
1382
1529
  if (!isCodexMirrorRow(item) || item.status === 'done') continue;
1530
+ // Replied on here, so an agent here answers it from now on.
1531
+ if (item.answer) continue;
1383
1532
  const t = byId.get(codexIdOf(item));
1384
1533
  if (!t) continue;
1385
1534
  const patch = codexMirrorPatch(t);
@@ -1450,6 +1599,20 @@ export class Store {
1450
1599
  watch() {
1451
1600
  this.unwatch();
1452
1601
  const dirs = [this.config.accountRoot, ...this.listProducts().map((p) => p.dir)];
1602
+ // The team chat and the teammates, which agents write to through the store
1603
+ // tools: watched so their words reach the screen as they are said.
1604
+ if (this.teammates) {
1605
+ try {
1606
+ const dir = this.teammates.dir();
1607
+ fs.mkdirSync(dir, { recursive: true });
1608
+ const watcher = fs.watch(dir, () => this._notify());
1609
+ watcher.on('error', () => {
1610
+ try { watcher.close(); } catch {}
1611
+ this.watchers = this.watchers.filter((w) => w !== watcher);
1612
+ });
1613
+ this.watchers.push(watcher);
1614
+ } catch {}
1615
+ }
1453
1616
  for (const dir of dirs) {
1454
1617
  try {
1455
1618
  const watcher = fs.watch(dir, (event, file) => {
@@ -1499,3 +1662,14 @@ export class Store {
1499
1662
  function clockAt(ms) {
1500
1663
  return new Date(ms).toISOString().slice(11, 19);
1501
1664
  }
1665
+
1666
+ /**
1667
+ * When a chat brought in happened: `at` when it started, `last` when it last
1668
+ * moved. A chat with no time it can be trusted for, or one ahead of `fallback`
1669
+ * (the moment of import), is stamped with `fallback`. */
1670
+ function chatTimes(thread, fallback) {
1671
+ const last = Number(thread?.when);
1672
+ if (!(last > 0 && last <= fallback)) return { at: fallback, last: fallback };
1673
+ const first = Number(thread?.startedAt);
1674
+ return { at: first > 0 && first <= last ? first : last, last };
1675
+ }