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
package/main/store.mjs CHANGED
@@ -12,12 +12,14 @@ import { answerFor, describeFor } from './first-run.mjs';
12
12
  import { checkProjectFolder } from '../shared/project-folder-check.mjs';
13
13
  import { EXAMPLES, stampsFor } from '../shared/first-run-examples.mjs';
14
14
  import {
15
- PRACTICE_ANSWER, PRACTICE_FLAG, PRACTICE_NAME, PRACTICE_NOTE, PRACTICE_ROWS, PRACTICE_SLUG,
16
- PRACTICE_TASK_TRACE,
15
+ PRACTICE_ANSWER, PRACTICE_BACKDROP, PRACTICE_FLAG, PRACTICE_NAME, PRACTICE_NOTE, PRACTICE_ROWS, PRACTICE_SLUG,
16
+ PRACTICE_REPLY_ANSWER, PRACTICE_TASK_TRACE,
17
17
  } from '../shared/first-run-practice.mjs';
18
18
  import { NOTE_NAME } from './rail-note.mjs';
19
19
  import { iconPathFor } from './project-identity.mjs';
20
+ import { patchProjectAt } from './store/project.mjs';
20
21
  import { imgUrl } from './img-scheme.mjs';
22
+ import { teamOf, PERSONAL_FLAG } from './team/projects.mjs';
21
23
  import {
22
24
  agentImportRow, agentsNeedingRows, threadImportRow, threadsNeedingRows,
23
25
  } from '../shared/agent-import.mjs';
@@ -66,7 +68,12 @@ export class Store {
66
68
  /* ------------------------------- products ------------------------------ */
67
69
  // A product is a project directory under the account root carrying a
68
70
  // project.json. Archived products (or the reserved analytics id) don't ride.
69
- listProducts() {
71
+ // ARCHIVED PROJECTS ARE LEFT OUT unless a caller asks for them, which is
72
+ // what makes archiving one take it off the inbox, the sidebar, the fleet and
73
+ // the Projects page in one write. Two callers do ask: the Projects page, so
74
+ // an archived one can be brought back, and the team sync, which must still
75
+ // count a shared project you archived as here or it joins it a second time.
76
+ listProducts({ includeArchived = false } = {}) {
70
77
  const root = this.config.accountRoot;
71
78
  let entries = [];
72
79
  try {
@@ -93,9 +100,10 @@ export class Store {
93
100
  } catch {
94
101
  continue;
95
102
  }
96
- if (project.archived) continue;
103
+ if (project.archived && !includeArchived) continue;
97
104
  products.push({
98
105
  slug: entry.name,
106
+ ...(includeArchived ? { archived: project.archived === true } : {}),
99
107
  dir,
100
108
  name: project.name || entry.name,
101
109
  oneLiner: project.oneLiner || '',
@@ -117,11 +125,56 @@ export class Store {
117
125
  // that turn on this flag — never spawning a worker in it, drawing a
118
126
  // band over it — must never happen to real work.
119
127
  practice: project[PRACTICE_FLAG] === true,
128
+ // YOUR PERSONAL PROJECT, where a thread is Only you unless you say
129
+ // otherwise (main/team/projects.mjs, w-b989839656).
130
+ personal: project[PERSONAL_FLAG] === true,
131
+ // WHO SEES ITS THREADS on the Team page (w-b989839656), read as
132
+ // written; shared/thread-cards.mjs projectSeenBy is the one reading.
133
+ ...(typeof project.seenBy === 'string' ? { seenBy: project.seenBy } : {}),
134
+ ...(Array.isArray(project.seenByPeople) ? { seenByPeople: project.seenByPeople.filter((p) => typeof p === 'string') } : {}),
135
+ // SHARED OR PRIVATE. Null on a private project, which is every
136
+ // project of a person who never signs in; otherwise the cloud id that
137
+ // is the same on every teammate's Mac (main/team/projects.mjs).
138
+ team: teamOf(project),
120
139
  });
121
140
  }
122
141
  return products.sort((a, b) => a.name.localeCompare(b.name));
123
142
  }
124
143
 
144
+ /** ARCHIVE A PROJECT, OR BRING ONE BACK. One flag in its project.json and
145
+ * nothing else: the folder, its documents and its threads stay on disk, and
146
+ * `listProducts` is what stops showing it. Agents already running there
147
+ * finish; nothing new starts, because the fleet reads the same list.
148
+ *
149
+ * Resolved through the list itself, so the only thing this can ever write
150
+ * is a project folder directly inside this account, named exactly. */
151
+ setProductArchived(slug, archived) {
152
+ const product = slug ? this.listProducts({ includeArchived: true }).find((p) => p.slug === slug) : null;
153
+ if (!product) throw new Error('That project could not be found.');
154
+ if (product.archived !== (archived === true)) {
155
+ patchProjectAt(product.dir, { archived: archived === true });
156
+ // The watched folders are the listed ones, so one brought back is not
157
+ // watched until the list is read again.
158
+ if (this.watchers.length) this.watch();
159
+ }
160
+ return { slug, archived: archived === true };
161
+ }
162
+
163
+ /** WHO SEES A PROJECT'S THREADS on the Team page (w-b989839656): 'private'
164
+ * (Just you), 'team', or 'people' with the people named. Written into the
165
+ * project's own file, because it is the project's, and nothing of the
166
+ * project itself leaves this Mac either way. My Workspace is refused: it is
167
+ * Just you by what it is. */
168
+ setProductSeenBy(slug, { who, people } = {}) {
169
+ const product = slug ? this.listProducts({ includeArchived: true }).find((p) => p.slug === slug) : null;
170
+ if (!product) throw new Error('That project could not be found.');
171
+ if (product.personal) throw new Error('My Workspace is always just you.');
172
+ if (who !== 'private' && who !== 'team' && who !== 'people') throw new Error(`unknown setting for who sees it: ${who}`);
173
+ const ids = [...new Set((Array.isArray(people) ? people : []).filter((p) => typeof p === 'string' && p))];
174
+ patchProjectAt(product.dir, { seenBy: who, seenByPeople: who === 'people' ? ids : undefined });
175
+ return { slug, who, people: who === 'people' ? ids : [] };
176
+ }
177
+
125
178
  /* ------------------------------ work items ----------------------------- */
126
179
  // The cross-product fold. Every item carries its product slug so the
127
180
  // renderer can group, filter, and rail without a second lookup.
@@ -149,6 +202,23 @@ export class Store {
149
202
  return all;
150
203
  }
151
204
 
205
+ // THE NEXT PAGE OF FINISHED THREADS THE SNAPSHOT'S 8 MB READ LEFT OUT
206
+ // (`readOlderWorkItems`), newest finished first, for the Done and All tabs
207
+ // as she scrolls to their foot (w-fda2165ec6). Archived projects stay out,
208
+ // as they do of `listItems`.
209
+ listOlderItems({ offset = 0, limit = 50, now = Date.now() } = {}) {
210
+ const { workItemsDisk } = this.modules;
211
+ const all = [];
212
+ for (const product of this.listProducts()) {
213
+ let items = [];
214
+ try { items = workItemsDisk.readOlderWorkItems(product.dir, now); } catch { continue; }
215
+ for (const item of items) all.push({ ...item, product: product.slug, productName: product.name });
216
+ }
217
+ const finished = (i) => i.wrote?.status?.ts || i.updatedAt;
218
+ all.sort((a, b) => finished(b) - finished(a));
219
+ return { items: all.slice(offset, offset + limit), more: all.length > offset + limit };
220
+ }
221
+
152
222
  // One item, folded fresh off disk. The supervisor holds a SPAWN-TIME snapshot
153
223
  // for the whole life of a session, so it cannot ask what the session did to
154
224
  // the row without reading it again; that question is the difference between a
@@ -254,7 +324,11 @@ export class Store {
254
324
  //
255
325
  // Undefined means she never opened the drawer and the row keeps what it had.
256
326
  // Null means she cleared it, and the engine chooses again.
257
- answerItem(slug, id, { answer, status, priority, permissionMode, model, effort }) {
327
+ //
328
+ // `inReplyTo` makes the answer a reply in a thread (w-920461cbe6): the uid
329
+ // of the message it answers, on THE SAME LINE as the words, so the reply's
330
+ // 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 }) {
258
332
  const { workItemsDisk } = this.modules;
259
333
  const dir = this.productDir(slug);
260
334
  let item = null;
@@ -269,7 +343,7 @@ export class Store {
269
343
  // special-cased in both engines' argument builders.
270
344
  if (model !== undefined) item = workItemsDisk.updateWorkItem(dir, id, { model: model ?? '' }, { source: 'founder' });
271
345
  if (effort !== undefined) item = workItemsDisk.updateWorkItem(dir, id, { effort: effort ?? '' }, { source: 'founder' });
272
- if (answer) item = workItemsDisk.updateWorkItem(dir, id, { answer }, { source: 'founder' });
346
+ if (answer) item = workItemsDisk.updateWorkItem(dir, id, inReplyTo ? { answer, inReplyTo } : { answer }, { source: 'founder' });
273
347
  // The pane may still show a running row after Stop or a provider result.
274
348
  // Resolve omitted reply status against the ledger that was just written.
275
349
  if (status == null && typeof answer === 'string' && answer.trim() && answer !== '(withdrawn)'
@@ -308,6 +382,28 @@ export class Store {
308
382
  return workItemsDisk.updateWorkItem(dir, id, { status }, { source: 'system' });
309
383
  }
310
384
 
385
+ // THE APP SHIPPED A TASK, OR COULD NOT (main/ship-queue.mjs). Both are the
386
+ // app's own words, so both go in as 'system', as a note: the reply field is
387
+ // hers. A failure reopens the row, and the queue hands the script's words
388
+ // straight to the agent that wrote the change.
389
+ shipped(slug, id, { sha, labels }) {
390
+ const { workItemsDisk } = this.modules;
391
+ 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' });
394
+ }
395
+
396
+ shipFailed(slug, id, { note, labels }) {
397
+ const { workItemsDisk } = this.modules;
398
+ const dir = this.productDir(slug);
399
+ workItemsDisk.updateWorkItem(dir, id, { labels, note }, { source: 'system' });
400
+ return workItemsDisk.updateWorkItem(dir, id, { status: 'open' }, { source: 'system' });
401
+ }
402
+
403
+ releaseRunClaim(slug, id, run) {
404
+ return this.modules.workItemsDisk.releaseRunClaim(this.productDir(slug), id, run);
405
+ }
406
+
311
407
  // THE WRITTEN NAME THE LIST DRAWS. Written by the app rather than by a
312
408
  // session, on agent authority, and never onto `title`: the title is what she
313
409
  // typed and the fold keeps it hers (main/row-label.mjs says why at length).
@@ -320,6 +416,15 @@ export class Store {
320
416
  return workItemsDisk.updateWorkItem(this.productDir(slug), id, { label: clean }, { source: 'agent' });
321
417
  }
322
418
 
419
+ // How urgent a teammate's message is, as the sorter judged it
420
+ // (main/message-priority.mjs). On agent authority, so a level the person set
421
+ // by hand stays theirs.
422
+ prioritizeItem(slug, id, value) {
423
+ const { workItemsDisk } = this.modules;
424
+ if (!Number.isFinite(value)) return null;
425
+ return workItemsDisk.updateWorkItem(this.productDir(slug), id, { priority: value }, { source: 'agent' });
426
+ }
427
+
323
428
  // A one-off permission mode has been spent. Called by the supervisor at the
324
429
  // moment it launches the run that mode was set for, so a mode she chose for
325
430
  // one message cannot silently govern a respawn three hours later.
@@ -366,10 +471,7 @@ export class Store {
366
471
  // app restart, a reboot, and a store restored onto another machine, and so no
367
472
  // agent line can quietly move a time she set. 0 clears it.
368
473
  //
369
- // This is the one write behind both gestures: deferring an agent's proposal
370
- // out of the inbox and scheduling her own work to start at six. They were
371
- // separate mechanisms before, and only the first existed: the old snooze was
372
- // a renderer localStorage map that hid a row and deferred no work at all.
474
+ // Execution scheduling only. Inbox reminders use snoozeItem instead.
373
475
  scheduleItem(slug, id, runAt) {
374
476
  const { workItemsDisk } = this.modules;
375
477
  return workItemsDisk.updateWorkItem(
@@ -377,7 +479,50 @@ export class Store {
377
479
  );
378
480
  }
379
481
 
380
- composeItem(slug, { title, body, kind = 'directive', priority = 0, labels, runAt, engine, model, effort }) {
482
+ // Put the existing thread back in the person's inbox later. This never
483
+ // changes its execution schedule, answer, status, claim or delivery marks.
484
+ snoozeItem(slug, id, snoozedUntil) {
485
+ if (!Number.isFinite(snoozedUntil)) throw new Error('Snooze needs a valid time');
486
+ return this.modules.workItemsDisk.updateWorkItem(
487
+ this.productDir(slug), id, { snoozedUntil: Math.max(0, Math.trunc(snoozedUntil)) }, { source: 'founder' },
488
+ );
489
+ }
490
+
491
+ // WHO A SHARED ROW IS WITH, changed by a person: handed to an agent, kept,
492
+ // handed back, or passed on by a reply. Only the team fields, on the
493
+ // person's own authority, so an agent writing afterwards cannot undo it.
494
+ teamPatch(slug, id, patch) {
495
+ const allowed = Object.fromEntries(Object.entries(patch ?? {}).filter(([k]) => ['assignee', 'runner', 'due', 'people'].includes(k)));
496
+ if (!Object.keys(allowed).length) throw new Error('nothing to change');
497
+ return this.modules.workItemsDisk.updateWorkItem(this.productDir(slug), id, allowed, { source: 'founder' });
498
+ }
499
+
500
+ // A REACTION ON ONE MESSAGE (w-560647d4db): the chips under it in a chat.
501
+ // `on` is the uid of the ledger line the message was written as, which is the
502
+ // one name for a message that is the same on every teammate's Mac. Written as
503
+ // the person's own word, and appended like anything else: the fold keeps a
504
+ // press per person rather than one value per row, so two people reacting at
505
+ // the same moment keep both chips (shared/work-items.mjs).
506
+ react(slug, id, { on, emoji, off = false }) {
507
+ return this.modules.workItemsDisk.updateWorkItem(
508
+ this.productDir(slug), id, { react: off ? { on, emoji, off: true } : { on, emoji } }, { source: 'founder' },
509
+ );
510
+ }
511
+
512
+ // AN EDIT TO A THREAD FROM ITS SUMMARY (approved 2026-10-01): the summary's
513
+ // three lines and its links, who sees it, and its priority. Written as the
514
+ // person's own words; on the summary the later write wins either way
515
+ // (SHARED_FIELDS in shared/work-items.mjs).
516
+ threadEdit(slug, id, patch) {
517
+ // `start` is how a thread leaves Later: the summary and the thread page
518
+ // both write 'now' on it, which is what makes `isDue` true again.
519
+ const fields = ['problem', 'progress', 'solution', 'visibility', 'visibleTo', 'priority', 'blockedBy', 'blocks', 'start'];
520
+ const allowed = Object.fromEntries(Object.entries(patch ?? {}).filter(([k]) => fields.includes(k)));
521
+ if (!Object.keys(allowed).length) throw new Error('nothing to change');
522
+ return this.modules.workItemsDisk.updateWorkItem(this.productDir(slug), id, allowed, { source: 'founder' });
523
+ }
524
+
525
+ composeItem(slug, { title, body, kind = 'directive', priority = 0, labels, runAt, start, engine, model, effort, assignee, due, people, visibility, visibleTo }) {
381
526
  const { workItemsDisk } = this.modules;
382
527
  const dir = this.productDir(slug);
383
528
  // The 'founder' label is the human-in-the-loop marker: only items the
@@ -396,6 +541,9 @@ export class Store {
396
541
  title,
397
542
  ...(body ? { body } : {}),
398
543
  ...(runAt ? { runAt: Math.max(0, Math.trunc(runAt)) } : {}),
544
+ // "Add it to Later" on the card: the thread exists now and starts when
545
+ // she says, which is the one answer on that menu with no clock in it.
546
+ ...(start === 'later' ? { start: 'later' } : {}),
399
547
  // WHICH MODEL SHE CHOSE FOR THIS ONE, on founder authority like the rest
400
548
  // of her content (2026-08-26).It is a Claude Code alias, and the spawn
401
549
  // hands it to `--model`.
@@ -417,8 +565,27 @@ export class Store {
417
565
  // whole gate exists for. Whether the word may be written at all is decided
418
566
  // before it gets here, by `Supervisor#engineOffered`.
419
567
  ...(engine ? { engine: String(engine) } : {}),
568
+ // A TASK GIVEN TO A PERSON (the team version): who has to do it and
569
+ // when. No agent runs on it until that person hands it to one
570
+ // (shared/team-rules.mjs), and it reaches their inbox, not the sender's.
571
+ ...(assignee ? { assignee: String(assignee) } : {}),
572
+ ...(due ? { due: String(due) } : {}),
573
+ ...(Array.isArray(people) && people.length ? { people } : {}),
574
+ // WHO SEES IT (approved 2026-10-01): the team by default, nobody but its
575
+ // owner, or the people she chose (w-41ff964775). No field at all reads
576
+ // as the team, except in your personal project, where it reads as Only
577
+ // you (w-b989839656). So Team is written when it was chosen, which is
578
+ // how a thread in My Workspace is shared on purpose.
579
+ ...(visibility === 'private' || visibility === 'team' ? { visibility } : {}),
580
+ ...(visibility === 'people' && Array.isArray(visibleTo) && visibleTo.length
581
+ ? { visibility, visibleTo: [...new Set(visibleTo.map(String))] } : {}),
420
582
  };
421
- return workItemsDisk.updateWorkItem(dir, created.id, contentPatch, { source: 'founder' });
583
+ // WITH ITS PROJECT ON IT, the way `readItem` answers. The fold never carries
584
+ // one, and the renderer withdraws a just-made task with `made.product`, so
585
+ // without this Z after a send asked for "no such product: undefined" and
586
+ // reached past the task to whatever was under it (w-c78d1e1607).
587
+ const made = workItemsDisk.updateWorkItem(dir, created.id, contentPatch, { source: 'founder' });
588
+ return made ? { ...made, product: slug } : made;
422
589
  }
423
590
 
424
591
  /* ---------------------------- repeating tasks --------------------------- */
@@ -508,6 +675,9 @@ export class Store {
508
675
  name,
509
676
  createdAt: new Date().toISOString(),
510
677
  ...(repo ? { repoPath: repo } : {}),
678
+ // EVERY NEW PROJECT STARTS AS JUST YOU (w-b989839656): its threads
679
+ // reach the Team page only once you share the project or the thread.
680
+ seenBy: 'private',
511
681
  }, null, 2));
512
682
  // AND IT FILES NOTHING INTO HER INBOX.
513
683
  //
@@ -761,6 +931,47 @@ export class Store {
761
931
  return { ids };
762
932
  }
763
933
 
934
+ /**
935
+ * THE REST OF THE PRACTICE TEAM'S WORK, so the tour and the board are not
936
+ * empty (w-58c8f466e7). The copy, and which column each row lands in, is
937
+ * `PRACTICE_BACKDROP` in shared/first-run-practice.mjs. The ids come back in
938
+ * its order, which is how `walkRows` knows which one is which.
939
+ *
940
+ * 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. */
944
+ stagePracticeBackdrop(slug, { now = Date.now(), label = 'first-run' } = {}) {
945
+ const { workItemsDisk } = this.modules;
946
+ const dir = this.productDir(slug);
947
+ const ids = [];
948
+ for (const row of PRACTICE_BACKDROP) {
949
+ const at = stampsFor(row, now);
950
+ const created = workItemsDisk.createWorkItem(
951
+ dir,
952
+ { title: row.title, kind: row.kind, priority: 5, labels: ['founder', label] },
953
+ { source: 'system', now: at.hers },
954
+ );
955
+ workItemsDisk.updateWorkItem(
956
+ dir, created.id,
957
+ { title: row.title, body: row.body, ...(row.state === 'scheduled' ? { runAt: now + (row.inMs ?? 0) } : {}) },
958
+ { source: 'founder', now: at.hers },
959
+ );
960
+ if (row.state !== 'scheduled') {
961
+ this.appendPracticeRun(slug, created.id, at.hers);
962
+ this.writePracticeTrace(slug, created.id, row.trace, at.hers + 1_000);
963
+ }
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
+ ids.push(created.id);
971
+ }
972
+ return { ids };
973
+ }
974
+
764
975
  /**
765
976
  * THE CLAIM AND THE RELEASE, so the row reads as one an agent worked on.
766
977
  *
@@ -840,9 +1051,23 @@ export class Store {
840
1051
  // was for. Only when a line was really found: the sentence that says a folder
841
1052
  // has no readme in it is a true answer and it is not a description, so it is
842
1053
  // never written under the project's name.
843
- finishFirstRunTask(slug, id) {
1054
+ finishFirstRunTask(slug, id, round = 0) {
844
1055
  const { workItemsDisk } = this.modules;
845
1056
  const product = this.listProducts().find((p) => p.slug === slug) ?? null;
1057
+ // A REPLY TO IT IS ANSWERED HERE TOO (2026-10-06). The first answer offers
1058
+ // to make the summary shorter; any reply after that gets the shorter one,
1059
+ // and no second trace, because the run it did is already on the thread.
1060
+ // AND THE REPLY IS SETTLED FIRST, the way the supervisor settles one when a
1061
+ // session exits: without it the row reads as an answer still waiting for an
1062
+ // agent and stays in In progress (`answerSettled`, shared/answers.mjs).
1063
+ if (product?.practice && round > 0) {
1064
+ const before = this.readItem(slug, id);
1065
+ this.settleAnswer(slug, id, before?.wrote?.answer?.ts ?? 0);
1066
+ const item = workItemsDisk.updateWorkItem(
1067
+ this.productDir(slug), id, { result: PRACTICE_REPLY_ANSWER }, { source: 'agent' },
1068
+ );
1069
+ return { item, line: PRACTICE_REPLY_ANSWER, oneLiner: null };
1070
+ }
846
1071
  // AND SINCE 2026-08-23 THE TASK IS SENT IN THE PRACTICE PROJECT, which
847
1072
  // points at no folder at all. Everything below reads one, so it cannot run
848
1073
  // here: a project that is not theirs has nothing honest to say about their
@@ -1231,6 +1456,13 @@ export class Store {
1231
1456
  if (file && !/work-items\.jsonl|dashboard\.jsonl|project\.json/.test(file)) return;
1232
1457
  this._notify();
1233
1458
  });
1459
+ // fs.watch may return successfully, then emit an error (for example
1460
+ // EMFILE). An unhandled error takes down the app, outside this catch.
1461
+ // Reads still work without a watcher; release only the failed one.
1462
+ watcher.on('error', () => {
1463
+ try { watcher.close(); } catch {}
1464
+ this.watchers = this.watchers.filter((w) => w !== watcher);
1465
+ });
1234
1466
  this.watchers.push(watcher);
1235
1467
  } catch {}
1236
1468
  }