agentbox-app 0.1.7 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/README.md +220 -58
  2. package/bin/agentbox.mjs +18 -3
  3. package/briefs/message-rules.md +24 -3
  4. package/briefs/worker.md +63 -49
  5. package/briefs/writing-rules.md +90 -131
  6. package/main/account-discovery.mjs +21 -20
  7. package/main/account-tooling.mjs +11 -11
  8. package/main/agent-activity.mjs +45 -5
  9. package/main/agent-schedule.mjs +14 -14
  10. package/main/agent-sessions.mjs +94 -10
  11. package/main/agent-updates.mjs +116 -0
  12. package/main/agents.mjs +52 -46
  13. package/main/analytics.mjs +27 -0
  14. package/main/approval-prompt-server.mjs +2 -2
  15. package/main/approvals.mjs +14 -5
  16. package/main/artifact-path.mjs +11 -0
  17. package/main/claude-bin.mjs +32 -5
  18. package/main/claude-input.mjs +45 -3
  19. package/main/claude-plan.mjs +25 -13
  20. package/main/code-change.mjs +3 -3
  21. package/main/codex-account.mjs +1 -1
  22. package/main/codex-bin.mjs +18 -0
  23. package/main/codex-launch-env.mjs +31 -0
  24. package/main/codex-memory-gate.mjs +260 -0
  25. package/main/codex-models.mjs +12 -7
  26. package/main/codex-threads.mjs +46 -23
  27. package/main/codex-usage-file.mjs +7 -7
  28. package/main/codex-usage.mjs +4 -4
  29. package/main/codex-watch.mjs +12 -12
  30. package/main/config.mjs +82 -17
  31. package/main/demo.mjs +17 -14
  32. package/main/dev-window.mjs +9 -9
  33. package/main/doc-scheme.mjs +2 -0
  34. package/main/engine-setup.mjs +204 -0
  35. package/main/feedback.mjs +55 -0
  36. package/main/folder-pictures.mjs +48 -0
  37. package/main/frame-scrollbars.mjs +90 -0
  38. package/main/fresh-user.mjs +7 -4
  39. package/main/git-change-offthread.mjs +70 -0
  40. package/main/git-change-thread.mjs +11 -0
  41. package/main/git-change.mjs +9 -3
  42. package/main/img-scheme.mjs +70 -1
  43. package/main/instruction-settings.mjs +134 -60
  44. package/main/ipc.mjs +335 -42
  45. package/main/leftovers.mjs +518 -0
  46. package/main/live-replies.mjs +43 -5
  47. package/main/machine.mjs +48 -1
  48. package/main/main.mjs +138 -15
  49. package/main/memory-gate-history.mjs +254 -0
  50. package/main/memory-gate-server.mjs +461 -0
  51. package/main/memory-gate.mjs +206 -0
  52. package/main/message-priority.mjs +87 -0
  53. package/main/notify.mjs +1 -1
  54. package/main/rail-note.mjs +2 -2
  55. package/main/refresh-agent-models.mjs +21 -0
  56. package/main/repeats.mjs +4 -4
  57. package/main/row-label.mjs +115 -51
  58. package/main/runs-on.mjs +93 -0
  59. package/main/serve.mjs +77 -5
  60. package/main/settings.mjs +245 -26
  61. package/main/ship-queue.mjs +279 -0
  62. package/main/sign-in-files.mjs +39 -0
  63. package/main/source-updater.mjs +298 -0
  64. package/main/store/home.mjs +84 -7
  65. package/main/store/project.mjs +15 -19
  66. package/main/store/work-items.mjs +150 -6
  67. package/main/store.mjs +245 -13
  68. package/main/supervisor.mjs +1634 -417
  69. package/main/task-commands.mjs +8 -10
  70. package/main/task-folders.mjs +94 -16
  71. package/main/task-remote-control.mjs +3 -3
  72. package/main/task-terminals.mjs +14 -2
  73. package/main/team/index.mjs +440 -0
  74. package/main/team/memory-cloud.mjs +247 -0
  75. package/main/team/projects.mjs +162 -0
  76. package/main/team/session.mjs +225 -0
  77. package/main/team/sign-in.mjs +106 -0
  78. package/main/team/supabase-backend.mjs +231 -0
  79. package/main/team/sync.mjs +163 -0
  80. package/main/updater.mjs +15 -5
  81. package/main/write-before-reload.mjs +50 -0
  82. package/mcp/core/page.mjs +10 -12
  83. package/mcp/core/work.mjs +123 -26
  84. package/mcp/tools.mjs +13 -6
  85. package/package.json +21 -9
  86. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  87. package/renderer/dist/assets/index-ChKsQkRw.js +1169 -0
  88. package/renderer/dist/assets/index-ClBXnq3B.css +32 -0
  89. package/renderer/dist/index.html +2 -2
  90. package/scripts/after-install.mjs +14 -0
  91. package/scripts/approval-server.sh +7 -2
  92. package/scripts/memory-gate-hook.sh +59 -0
  93. package/shared/agent-import.mjs +39 -16
  94. package/shared/agents.d.mts +26 -8
  95. package/shared/agents.mjs +100 -108
  96. package/shared/analytics-events.mjs +19 -2
  97. package/shared/answers.mjs +12 -0
  98. package/shared/bridge-map.d.mts +12 -0
  99. package/shared/bridge-map.mjs +38 -1
  100. package/shared/claude-commands.generated.mjs +2 -2
  101. package/shared/claude-models.generated.mjs +6 -6
  102. package/shared/claude-usage.mjs +6 -6
  103. package/shared/codex-import.mjs +7 -7
  104. package/shared/codex-modes.d.mts +50 -0
  105. package/shared/codex-modes.mjs +1 -1
  106. package/shared/codex-usage.mjs +7 -8
  107. package/shared/demo-world.mjs +6 -6
  108. package/shared/engines.mjs +27 -10
  109. package/shared/feedback.d.mts +14 -0
  110. package/shared/feedback.mjs +44 -0
  111. package/shared/first-run-examples.mjs +4 -4
  112. package/shared/first-run-practice.d.mts +16 -0
  113. package/shared/first-run-practice.mjs +176 -70
  114. package/shared/fresh-user-home.mjs +34 -4
  115. package/shared/instruction-defaults.json +2 -2
  116. package/shared/product-name.mjs +4 -2
  117. package/shared/project-folder-check.mjs +9 -3
  118. package/shared/rank.d.mts +1 -0
  119. package/shared/rank.mjs +22 -0
  120. package/shared/referenced-files.mjs +1 -1
  121. package/shared/runs-on.d.mts +20 -0
  122. package/shared/runs-on.mjs +79 -0
  123. package/shared/schemes.d.mts +11 -0
  124. package/shared/schemes.mjs +1 -1
  125. package/shared/shortcuts.d.mts +41 -0
  126. package/shared/shortcuts.mjs +236 -0
  127. package/shared/spawn-trouble.d.mts +2 -0
  128. package/shared/spawn-trouble.mjs +40 -19
  129. package/shared/team-rules.d.mts +17 -0
  130. package/shared/team-rules.mjs +230 -0
  131. package/shared/team-status.d.mts +11 -0
  132. package/shared/team-status.mjs +90 -0
  133. package/shared/terminal-links.d.mts +2 -0
  134. package/shared/terminal-links.mjs +55 -0
  135. package/shared/terminal-state.d.mts +1 -0
  136. package/shared/terminal-state.mjs +16 -0
  137. package/shared/thread-cards.d.mts +19 -0
  138. package/shared/thread-cards.mjs +254 -0
  139. package/shared/usage.d.mts +1 -1
  140. package/shared/usage.mjs +6 -7
  141. package/shared/work-items.mjs +258 -28
  142. package/shared/work-lines.mjs +393 -16
  143. package/worker-permissions.json +1 -1
  144. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  145. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  146. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  147. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  148. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  149. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  150. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  151. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  152. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  153. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  154. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  155. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  156. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  157. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  158. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  159. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  160. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  161. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  162. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  163. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  164. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  165. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  166. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  167. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  168. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  169. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  170. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  171. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  172. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  173. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  174. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  175. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  176. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  177. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  178. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  179. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  180. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  181. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  182. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  183. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  184. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  185. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  186. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  187. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  188. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  189. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  190. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  191. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  192. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  193. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  194. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  195. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  196. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  197. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  198. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  199. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  200. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  201. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  202. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  203. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  204. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  205. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  206. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  207. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  208. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  209. package/renderer/dist/emulator/data/compression/README.md +0 -11
  210. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  211. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  212. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  213. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  214. package/renderer/dist/emulator/data/cores/README.md +0 -22
  215. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  216. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  217. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  218. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  219. package/renderer/dist/emulator/data/cores/package.json +0 -65
  220. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  221. package/renderer/dist/emulator/data/emulator.css +0 -1614
  222. package/renderer/dist/emulator/data/loader.js +0 -170
  223. package/renderer/dist/emulator/data/localization/README.md +0 -63
  224. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  225. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  226. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  227. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  228. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  229. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  230. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  231. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  232. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  233. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  234. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  235. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  236. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  237. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  238. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  239. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  240. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  241. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  242. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  243. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  244. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  245. package/renderer/dist/emulator/data/src/compression.js +0 -142
  246. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  247. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  248. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  249. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  250. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  251. package/renderer/dist/emulator/data/src/storage.js +0 -125
  252. package/renderer/dist/emulator/data/version.json +0 -5
  253. package/renderer/dist/emulator/host.js +0 -299
  254. package/renderer/dist/emulator/index.html +0 -28
  255. package/renderer/dist/emulator/provenance.json +0 -6
@@ -66,7 +66,7 @@ export const CLAUDE_PERMISSION_MODES = [
66
66
  // `answerMode` used to live on the ledger: what one reply is allowed to do on
67
67
  // the founder's Mac, turned straight into `--permission-mode` on a real process
68
68
  // by spawnPlan. A guard here stripped it off any line that did not claim to be
69
- // hers, which closed the STRUCTURED path, the MCP and the app.
69
+ // the founder's, which closed the STRUCTURED path, the MCP and the app.
70
70
  //
71
71
  // IT COULD NOT CLOSE THE FILE, AND NOTHING WRITTEN HERE EVER COULD. `source` is
72
72
  // a plain string in a line and nothing checks it. A worker with Bash could
@@ -78,8 +78,8 @@ export const CLAUDE_PERMISSION_MODES = [
78
78
  // own storage now, written by main and by nothing else (main/answer-modes.mjs),
79
79
  // which is the convention Claude Code uses for its standing policy. Everything
80
80
  // on the ledger today is a claim about the WORK, and a forged line can still
81
- // fake her archive or her answer — true of any append-only log nobody signs, and
82
- // filed for her rather than papered over here. What it can no longer do is hand
81
+ // fake the founder's archive or answer, which is true of any append-only log
82
+ // nobody signs, and is filed as a known issue rather than papered over here. What it can no longer do is hand
83
83
  // anything a permission.
84
84
  //
85
85
  // If a future field is a grant rather than an opinion, the answer is the same
@@ -90,29 +90,96 @@ export const CLAUDE_PERMISSION_MODES = [
90
90
  // state is worse than one that visibly does nothing.
91
91
  // `label` IS THE NAME ON THE ROW, AND IT EXISTS BECAUSE `title` CANNOT BE ONE.
92
92
  //
93
- // A message she dictates becomes an item whose title is its first line,
94
- // verbatim (message-split.ts). Measured on her Agentbox store, 2026-09-03: 214
95
- // items are hers, median title 84 characters, 145 of them over 60, 37 ending in
96
- // an ellipsis mid sentence, and 76 opening with "I", "I've been", "Can you" or
97
- // "We". Eleven of those in a column is eleven rows that look like one row.
93
+ // A dictated message becomes an item whose title is its first line, verbatim
94
+ // (message-split.ts). Such titles run long, are often cut off mid sentence with
95
+ // an ellipsis, and many open with "I", "I've been", "Can you" or "We". Eleven
96
+ // of those in a column is eleven rows that look like one row.
98
97
  //
99
98
  // The obvious fix, having a session write a better title, does not work and
100
- // never did: `beats` below makes her outrank an agent per field, so 208 of those
101
- // 214 titles are hers and an agent's rewrite of one is accepted here and then
102
- // dropped on the floor, silently. That is the whole reason this is a second
103
- // field rather than a better-behaved writer.
99
+ // never did: `beats` below makes the founder outrank an agent per field, so
100
+ // nearly every such title is the founder's and an agent's rewrite of one is
101
+ // accepted here and then dropped on the floor, silently. That is the whole
102
+ // reason this is a second field rather than a better-behaved writer.
104
103
  //
105
- // So `title` stays exactly what she typed and nothing about who owns it changes.
106
- // `label` is the short written name a session puts beside it, and the LIST
107
- // prefers it (`rowTitle`, list-rules.ts). Everything else still reads the title,
108
- // so her own words are what she finds when she opens the row, and search matches
109
- // either name.
104
+ // So `title` stays exactly what the user typed and nothing about who owns it
105
+ // changes. `label` is the short written name a session puts beside it, and the
106
+ // LIST prefers it (`rowTitle`, list-rules.ts). Everything else still reads the
107
+ // title, so the user's own words are what they find when they open the row, and
108
+ // search matches either name.
110
109
  export const WORK_ITEM_FIELDS = [
111
110
  'title', 'label', 'body', 'kind', 'labels', 'priority', 'parent',
112
- 'status', 'result', 'note', 'answer', 'product', 'runAt', 'answeredThrough',
111
+ 'status', 'result', 'note', 'answer', 'product', 'runAt', 'snoozedUntil', 'answeredThrough',
113
112
  'engine', 'model', 'effort',
113
+ // WHETHER THIS HAS BEEN STARTED AT ALL. 'later' is a thread written down and
114
+ // deliberately not begun: it waits for a person, not for a clock, and
115
+ // `isDue` below is where that becomes true of the whole app. 'now' is how it
116
+ // is started, and a thread without the field at all is an ordinary one that
117
+ // started the moment it was sent.
118
+ 'start',
119
+ // THE TEAM FIELDS. All four are about PEOPLE on a shared project, and a
120
+ // private project never sets them, so a row without them reads as before.
121
+ // assignee the person who has to act next, or 'agent' once a person has
122
+ // handed it back to the agents
123
+ // runner the person whose Mac runs agents on this row; without one it
124
+ // is whoever started it (`createdBy`)
125
+ // due a calendar day, YYYY-MM-DD, or 'none' to clear one
126
+ // people who is on the conversation
127
+ 'assignee', 'runner', 'due', 'people',
128
+ // THE THREAD'S OWN FIELDS (the team version, approved 2026-10-01).
129
+ // visibility 'team' (the default), 'people' or 'private': whether every
130
+ // teammate sees this thread's summary on the Team board, only
131
+ // the people named in `visibleTo`, or nobody but you
132
+ // visibleTo the people a 'people' thread reaches, by id. Ignored on the
133
+ // other two, and a 'people' thread naming nobody reaches
134
+ // nobody (shared/thread-cards.mjs says why it fails closed)
135
+ // problem, progress, solution
136
+ // the SUMMARY, one short sentence each (an agent's is held to
137
+ // SUMMARY_WORDS, shared/thread-cards.mjs). The agent keeps it
138
+ // current and the person can edit it in place, so these are
139
+ // the fields where the later write wins whoever wrote it
140
+ // (SHARED_FIELDS below)
141
+ // blockedBy, blocks
142
+ // other threads' ids, the summary's Linked part
143
+ 'visibility', 'visibleTo', 'problem', 'progress', 'solution', 'blockedBy', 'blocks',
144
+ // A REACTION, AND IT IS THE ONE FIELD ON THIS LIST THAT IS A DELTA RATHER
145
+ // THAN A VALUE: `{ on, emoji }` to put one on, `{ on, emoji, off: true }` to
146
+ // take yours back, where `on` is the uid of the line the message was written
147
+ // as. Every other field here keeps the latest write and drops the rest, which
148
+ // for a reaction would mean your 👀 erasing the one a teammate put on the same
149
+ // message a second earlier. The fold accumulates these per person instead and
150
+ // reads them back as `reactions` (see foldWorkItems); nothing ever reads a
151
+ // bare `react` off an item.
152
+ 'react',
153
+ // A REPLY IN A THREAD (w-920461cbe6): the uid of the message line it answers,
154
+ // written on the same line as the reply's own `answer`. Like `react` it is a
155
+ // mark on ONE line rather than a value the row holds, so the fold skips it;
156
+ // the chat reads it per line (renderer/src/thread-history.ts).
157
+ 'inReplyTo',
114
158
  ];
115
159
 
160
+ // THE SUMMARY IS SHARED BETWEEN THE PERSON AND THE AGENT. Everywhere else the
161
+ // founder's word outranks an agent's forever, which here would mean one edit
162
+ // of hers freezes the summary the agent is meant to keep up to date. So on
163
+ // these fields the later write wins, whoever made it, and the source is still
164
+ // recorded so the panel can say who touched it last.
165
+ const SHARED_FIELDS = new Set(['problem', 'progress', 'solution', 'blockedBy', 'blocks']);
166
+ const MAX_SUMMARY = 1200;
167
+
168
+ // A person id or a line id: short, plain, never an object a writer slipped in.
169
+ const MAX_PERSON_CHARS = 64;
170
+ function shortId(value) {
171
+ return typeof value === 'string' && value && value.length <= MAX_PERSON_CHARS ? value : null;
172
+ }
173
+ const DAY_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
174
+ function calendarDay(value) {
175
+ if (value === 'none') return 'none';
176
+ const m = typeof value === 'string' ? DAY_RE.exec(value) : null;
177
+ if (!m) return undefined;
178
+ const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
179
+ const date = new Date(Date.UTC(y, mo - 1, d));
180
+ return date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d ? value : undefined;
181
+ }
182
+
116
183
  // How long a claim survives without a heartbeat. The MCP server heartbeats on a
117
184
  // timer while it holds any claim, so this is a liveness signal about the PROCESS
118
185
  // (session dies, child dies, heartbeats stop) rather than anything the model has
@@ -133,6 +200,9 @@ const MAX_SHORT = 200;
133
200
  // see. Four fields at this size still leave a line at half the append ceiling.
134
201
  export const MAX_TEXT_BYTES = 32 * 1024;
135
202
  const MAX_LABELS = 20;
203
+ // How long one emoji may be, in code points. 👩🏽‍🚀 is five; eight leaves room
204
+ // for the longest sequence anyone picks and refuses a sentence pasted in.
205
+ const MAX_EMOJI = 8;
136
206
  const MAX_ID_CHARS = 64;
137
207
  // A single line longer than this is treated as torn or hostile and skipped
138
208
  // rather than parsed. Matches the ceiling records.mjs already uses.
@@ -235,6 +305,11 @@ export function normalizeLine(line) {
235
305
  id: raw.id,
236
306
  ts: Number.isFinite(raw.ts) ? raw.ts : 0,
237
307
  source,
308
+ // WHO WROTE IT and the line's own id. Stamped where a line reaches the
309
+ // disk on a signed-in Mac (main/store/work-items.mjs), absent everywhere
310
+ // else, and a line without them reads exactly as it always did.
311
+ by: shortId(raw.by),
312
+ uid: shortId(raw.uid),
238
313
  // A founder line is never fenced, so its epoch is ignored even if present.
239
314
  epoch: source === 'agent' && Number.isFinite(raw.epoch) ? raw.epoch : null,
240
315
  claim: raw.claim && typeof raw.claim === 'object' ? raw.claim : null,
@@ -321,6 +396,12 @@ function coerceField(field, value) {
321
396
  // that coerces to undefined is dropped by the fold, so without a falsy
322
397
  // sentinel a schedule could be set and never cleared.
323
398
  case 'runAt': return Number.isFinite(value) ? Math.trunc(value) : undefined;
399
+ // An inbox reminder, independent of when work may start. 0 brings it back.
400
+ case 'snoozedUntil': return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : undefined;
401
+ // 'later' or 'now', and 'now' is the falsy sentinel for the same reason
402
+ // runAt keeps 0: a field that coerces to undefined is dropped by the fold,
403
+ // so without a real value for "started" nothing could ever clear it.
404
+ case 'start': return value === 'later' || value === 'now' ? value : undefined;
324
405
  // WHICH ANSWER A SESSION HAS ALREADY ACTED ON, as that answer's own ts.
325
406
  //
326
407
  // An answer had two states and the ledger recorded one. A worker finishes
@@ -340,6 +421,40 @@ function coerceField(field, value) {
340
421
  const labels = value.map((l) => str(l, MAX_SHORT)).filter(Boolean).slice(0, MAX_LABELS);
341
422
  return labels.length ? labels : undefined;
342
423
  }
424
+ case 'assignee': case 'runner': return shortId(value) ?? undefined;
425
+ case 'due': return calendarDay(value);
426
+ case 'people': {
427
+ if (!Array.isArray(value)) return undefined;
428
+ const people = [...new Set(value.map(shortId).filter(Boolean))].slice(0, MAX_LABELS);
429
+ return people.length ? people : undefined;
430
+ }
431
+ // WHICH MESSAGE AND WHICH EMOJI. `on` is a line uid, bounded like any other
432
+ // id; the emoji is counted in CHARACTERS rather than bytes, because a flag
433
+ // or a person with a skin tone is several code points joined and cutting one
434
+ // mid-sequence stores a glyph nobody chose.
435
+ case 'react': {
436
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
437
+ const on = shortId(value.on);
438
+ const emoji = typeof value.emoji === 'string' ? [...value.emoji.trim()].slice(0, MAX_EMOJI).join('') : '';
439
+ if (!on || !emoji) return undefined;
440
+ return value.off === true ? { on, emoji, off: true } : { on, emoji };
441
+ }
442
+ case 'inReplyTo': return shortId(value) ?? undefined;
443
+ case 'visibility': return value === 'private' || value === 'team' || value === 'people' ? value : undefined;
444
+ // An empty list is a real value: it is how the last chosen person comes
445
+ // off a thread, and the thread then reaches nobody until she names one.
446
+ case 'visibleTo': {
447
+ if (!Array.isArray(value)) return undefined;
448
+ return [...new Set(value.map(shortId).filter(Boolean))].slice(0, MAX_LABELS);
449
+ }
450
+ // An empty string is a real value here: it is how a person clears a line
451
+ // of the summary, and a field that coerces to undefined cannot be cleared.
452
+ case 'problem': case 'progress': case 'solution':
453
+ return typeof value === 'string' ? value.trim().slice(0, MAX_SUMMARY) : undefined;
454
+ case 'blockedBy': case 'blocks': {
455
+ if (!Array.isArray(value)) return undefined;
456
+ return [...new Set(value.map(shortId).filter(Boolean))].slice(0, MAX_LABELS);
457
+ }
343
458
  default: return undefined;
344
459
  }
345
460
  }
@@ -355,10 +470,9 @@ function coerceField(field, value) {
355
470
  // WHO An agent's `done` and the founder's archive are the same word for
356
471
  // opposite events. Her own ask, ended by an agent, is an answer she has
357
472
  // not received; ended by her, it is filed. the app's inbox could not tell
358
- // them apart, so every answer to a question she asked went straight to
359
- // the archive and she reasonably concluded the work never happened
360
- // (2026-08-06: she asked the same question three times in six hours,
361
- // and all three had been answered within twenty minutes).
473
+ // them apart, so every answer to a question the founder asked went
474
+ // straight to the archive, and it reasonably looked as though the work
475
+ // never happened, so the same question got asked again and again.
362
476
  // WHEN Two identical nudges are two asks. A delivery keyed on the words
363
477
  // alone treats the second as already handled and swallows it.
364
478
  //
@@ -367,6 +481,22 @@ function coerceField(field, value) {
367
481
  // 2. skip an agent line fenced out by a newer epoch (the zombie writer)
368
482
  // 3. apply the claim, if this line carries one
369
483
  // 4. apply the patch field by field, founder outranking agent
484
+ // THE CHIPS UNDER A MESSAGE, out of the presses the ledger holds. Message by
485
+ // message, emoji by emoji, the people still on it — oldest press first, so the
486
+ // chips do not reorder themselves under somebody's pointer as a teammate's
487
+ // lines arrive. Returns null when every press has been taken back, which is
488
+ // what leaves a row with no `reactions` at all.
489
+ function rollUpReactions(presses) {
490
+ const live = [...presses.values()].filter((p) => !p.off).sort((a, b) => a.ts - b.ts);
491
+ if (!live.length) return null;
492
+ const out = {};
493
+ for (const press of live) {
494
+ const message = out[press.on] ?? (out[press.on] = {});
495
+ (message[press.emoji] ?? (message[press.emoji] = [])).push(press.who);
496
+ }
497
+ return out;
498
+ }
499
+
370
500
  export function foldWorkItems(lines, now = Date.now()) {
371
501
  const items = new Map();
372
502
 
@@ -379,10 +509,15 @@ export function foldWorkItems(lines, now = Date.now()) {
379
509
  item = {
380
510
  id: line.id, status: 'open', title: '', kind: '', labels: [],
381
511
  priority: 0, epoch: 0, claim: null, createdAt: line.ts, updatedAt: line.ts,
382
- wrote: {},
512
+ createdBy: line.by, wrote: {},
383
513
  };
384
514
  items.set(line.id, item);
385
515
  }
516
+ // Who started the row is whoever wrote its EARLIEST line, not the first
517
+ // line this Mac happened to receive: a teammate's lines arrive late.
518
+ if (line.ts < item.createdAt || (line.ts === item.createdAt && !item.createdBy)) {
519
+ if (line.by) item.createdBy = line.by;
520
+ }
386
521
 
387
522
  // Fencing. An agent line written under a superseded epoch is the whole
388
523
  // reason this field exists: it is a live worker whose lease lapsed, and
@@ -392,7 +527,9 @@ export function foldWorkItems(lines, now = Date.now()) {
392
527
  if (line.claim) {
393
528
  // A claim at or above the current epoch takes the item. A heartbeat only
394
529
  // extends a lease it still legitimately holds.
395
- if (line.epoch >= item.epoch) {
530
+ const renewsHeldClaim = !line.heartbeat || (item.claim
531
+ && line.epoch === item.epoch && line.claim.holder === item.claim.holder);
532
+ if (line.epoch >= item.epoch && renewsHeldClaim) {
396
533
  item.epoch = line.epoch;
397
534
  item.claim = {
398
535
  holder: String(line.claim.holder ?? ''),
@@ -412,16 +549,48 @@ export function foldWorkItems(lines, now = Date.now()) {
412
549
  // are already in her ledger and will be there forever.
413
550
  const patch = line.heartbeat ? {} : line.patch;
414
551
 
552
+ // WHO SAID WHAT, AND WHERE, for a conversation between people
553
+ // (w-920461cbe6). Every person's message, kept in a list of its own because
554
+ // the fold below keeps only the newest answer, and a busy chat needs to know
555
+ // who spoke last in the chat itself and in each thread. Only a person's own
556
+ // words count (a line with a writer), and only rows with people on them keep
557
+ // the result (see the end of the fold).
558
+ const words = typeof patch.answer === 'string' && patch.answer !== WITHDRAWN ? patch.answer
559
+ : typeof patch.body === 'string' ? patch.body : null;
560
+ if (line.by && words && words.trim()) {
561
+ (item.said ??= []).push({ uid: line.uid ?? null, by: line.by, ts: line.ts, text: words.trim().slice(0, TALK_TEXT), inReplyTo: patch.inReplyTo ?? null, body: typeof patch.answer !== 'string' });
562
+ }
563
+
415
564
  for (const [field, value] of Object.entries(patch)) {
565
+ // A REACTION IS ACCUMULATED, NOT HELD. Every other field below keeps one
566
+ // value and the newest writer owns it; a reaction belongs to the person
567
+ // who pressed it, so two people on one message have to survive each other.
568
+ // The key is the message, the emoji and the person, and within that one
569
+ // key the newest press wins — by its own timestamp, because a page pulled
570
+ // from the cloud is not promised in the order anybody pressed anything.
571
+ if (field === 'react') {
572
+ if (!item.reacts) item.reacts = new Map();
573
+ const who = line.by || 'you';
574
+ const key = `${value.on}\u0000${value.emoji}\u0000${who}`;
575
+ const was = item.reacts.get(key);
576
+ if (!was || line.ts >= was.ts) {
577
+ item.reacts.set(key, { ts: line.ts, off: value.off === true, who, on: value.on, emoji: value.emoji });
578
+ }
579
+ continue;
580
+ }
581
+ // WHICH MESSAGE A REPLY ANSWERS belongs to that reply's line alone. Held
582
+ // as a value, the row would carry the newest reply's parent forever.
583
+ if (field === 'inReplyTo') continue;
416
584
  const held = item.wrote[field];
417
585
  // There used to be an exception here, for machinery spending a one-off
418
586
  // permission mode over the founder's own grant. That field is not on the
419
587
  // ledger any more (see the note beside WORK_ITEM_FIELDS), so
420
588
  // the plain rule is the only rule: a weaker writer never overwrites a
421
589
  // stronger one's value.
422
- if (held && !beats(line, held)) continue;
590
+ if (held && !beats(line, held, field)) continue;
423
591
  item[field] = value;
424
592
  item.wrote[field] = { ts: line.ts, source: authorityOf(line, field, value) };
593
+ if (line.by) item.wrote[field].by = line.by;
425
594
  }
426
595
 
427
596
  item.updatedAt = Math.max(item.updatedAt, line.ts);
@@ -440,18 +609,68 @@ export function foldWorkItems(lines, now = Date.now()) {
440
609
  for (const item of items.values()) {
441
610
  item.claimExpired = !!(item.claim && item.claim.leaseUntil < now);
442
611
  if (item.status === 'claimed' && (!item.claim || item.claimExpired)) item.status = 'open';
612
+ if (item.reacts) {
613
+ const chips = rollUpReactions(item.reacts);
614
+ delete item.reacts;
615
+ if (chips) item.reactions = chips;
616
+ }
617
+ if (item.said) {
618
+ const said = item.said;
619
+ delete item.said;
620
+ if (Array.isArray(item.people) && item.people.length) item.talk = talkOf(said);
621
+ }
443
622
  }
444
623
 
445
624
  return items;
446
625
  }
447
626
 
627
+ // A reply taken back reads as this, and is nobody's news.
628
+ const WITHDRAWN = '(withdrawn)';
629
+ // How much of a message the record keeps: enough for a row's one line.
630
+ const TALK_TEXT = 280;
631
+ // How many of the newest messages the record keeps, in the chat and in each
632
+ // thread. The counts it answers stop at your own newest word, which in a chat
633
+ // you are part of is never this far back.
634
+ const TALK_KEEP = 100;
635
+
636
+ /**
637
+ * THE CHAT AND ITS THREADS, AS WHO SPOKE WHERE (w-920461cbe6): `chat` is the
638
+ * main conversation, oldest first, and `threads` the replies under each message
639
+ * they answer, with who wrote that message and its words. Pure bookkeeping for
640
+ * `whatWaits` (shared/team-rules.mjs) and the row's one line; the messages
641
+ * themselves are read off the ledger by the thread view, as before.
642
+ */
643
+ function talkOf(said) {
644
+ const lines = [...said].sort((a, b) => a.ts - b.ts);
645
+ const chat = [];
646
+ const threads = {};
647
+ const parents = new Map();
648
+ const bodies = new Set();
649
+ for (const s of lines) {
650
+ // A rename that sends the same opening words again is not a new message.
651
+ if (s.body) { if (bodies.has(s.text)) continue; bodies.add(s.text); }
652
+ const said1 = { by: s.by, ts: s.ts, text: s.text };
653
+ const parent = s.inReplyTo ? parents.get(s.inReplyTo) : null;
654
+ if (parent) {
655
+ (threads[s.inReplyTo] ??= { by: parent.by, text: parent.text, replies: [] }).replies.push(said1);
656
+ continue;
657
+ }
658
+ // A reply to a message this chat does not hold is drawn in the chat, so it
659
+ // counts there too.
660
+ chat.push(said1);
661
+ if (s.uid) parents.set(s.uid, said1);
662
+ }
663
+ for (const t of Object.values(threads)) t.replies = t.replies.slice(-TALK_KEEP);
664
+ return { chat: chat.slice(-TALK_KEEP), threads };
665
+ }
666
+
448
667
  /* -------------------------------- the thread ----------------------------- */
449
668
  // EVERY TURN OF ONE ROW, OLDEST FIRST. The fold answers "what does this item
450
669
  // say now"; this answers "what has been said here", which is a different
451
670
  // question and nothing could ask it before.
452
671
  //
453
- // She was describing a real hole. Every reply she wrote spawned a fresh session
454
- // holding one field, `answer`, with no sight of her earlier answers on the same
672
+ // There was a real hole here. Every reply the user wrote spawned a fresh session
673
+ // holding one field, `answer`, with no sight of their earlier answers on the same
455
674
  // row and no sight of the work it was replying about. One session built
456
675
  // thirty-nine images off a bare line asking for more options, because cold that
457
676
  // is what the line says. The whole exchange was on disk the entire time: the
@@ -512,7 +731,8 @@ function authorityOf(line, field, value) {
512
731
 
513
732
  // Does the incoming line outrank the one already holding this field? The founder
514
733
  // wins over any agent regardless of time; between equals, later wins.
515
- function beats(incoming, held) {
734
+ function beats(incoming, held, field) {
735
+ if (SHARED_FIELDS.has(field)) return incoming.ts >= held.ts;
516
736
  if (held.source === 'founder' && incoming.source !== 'founder') return false;
517
737
  if (incoming.source === 'founder' && held.source !== 'founder') return true;
518
738
  return incoming.ts >= held.ts;
@@ -530,10 +750,20 @@ function beats(incoming, held) {
530
750
  // the whole reason this is a predicate over the clock rather than a timer, and
531
751
  // it is what makes a schedule survive an app restart, a reboot, and a store
532
752
  // restored onto another machine (the founder's requirement, 2026-08-06).
753
+ // AND A THREAD IN LATER WAITS FOR A PERSON RATHER THAN A CLOCK (w-afb66e6661).
754
+ // "Add it to Later" on the card writes `start: 'later'`, which means exactly
755
+ // what a future moment means and never arrives on its own: not claimable, no
756
+ // session, not in the inbox. Pressing start on the thread writes 'now'.
533
757
  export function isDue(item, now = Date.now()) {
758
+ if (item?.start === 'later') return false;
534
759
  return !item?.runAt || item.runAt <= now;
535
760
  }
536
761
 
762
+ /** Written down and deliberately not begun. */
763
+ export function notStarted(item) {
764
+ return item?.start === 'later';
765
+ }
766
+
537
767
  export function isClaimable(item, now = Date.now()) {
538
768
  if (!item) return false;
539
769
  if (!PULLABLE.includes(item.status)) return false;