agentbox-app 0.1.7 → 0.1.11

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 (244) hide show
  1. package/README.md +209 -55
  2. package/bin/agentbox.mjs +6 -0
  3. package/briefs/message-rules.md +13 -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 +9 -10
  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 +114 -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/artifact-path.mjs +11 -0
  16. package/main/claude-bin.mjs +31 -5
  17. package/main/claude-input.mjs +45 -3
  18. package/main/claude-plan.mjs +11 -11
  19. package/main/code-change.mjs +3 -3
  20. package/main/codex-account.mjs +1 -1
  21. package/main/codex-bin.mjs +17 -0
  22. package/main/codex-memory-gate.mjs +260 -0
  23. package/main/codex-models.mjs +12 -7
  24. package/main/codex-threads.mjs +46 -23
  25. package/main/codex-usage-file.mjs +7 -7
  26. package/main/codex-usage.mjs +4 -4
  27. package/main/codex-watch.mjs +12 -12
  28. package/main/config.mjs +68 -8
  29. package/main/dev-window.mjs +9 -9
  30. package/main/doc-scheme.mjs +2 -0
  31. package/main/engine-setup.mjs +193 -0
  32. package/main/feedback.mjs +55 -0
  33. package/main/folder-pictures.mjs +48 -0
  34. package/main/frame-scrollbars.mjs +90 -0
  35. package/main/fresh-user.mjs +6 -3
  36. package/main/git-change-offthread.mjs +70 -0
  37. package/main/git-change-thread.mjs +11 -0
  38. package/main/git-change.mjs +1 -1
  39. package/main/img-scheme.mjs +70 -1
  40. package/main/instruction-settings.mjs +92 -54
  41. package/main/ipc.mjs +316 -42
  42. package/main/leftovers.mjs +518 -0
  43. package/main/live-replies.mjs +43 -5
  44. package/main/machine.mjs +47 -0
  45. package/main/main.mjs +119 -15
  46. package/main/memory-gate-history.mjs +254 -0
  47. package/main/memory-gate-server.mjs +461 -0
  48. package/main/memory-gate.mjs +206 -0
  49. package/main/message-priority.mjs +87 -0
  50. package/main/rail-note.mjs +2 -2
  51. package/main/refresh-agent-models.mjs +20 -0
  52. package/main/repeats.mjs +4 -4
  53. package/main/row-label.mjs +114 -51
  54. package/main/serve.mjs +67 -5
  55. package/main/settings.mjs +223 -26
  56. package/main/ship-queue.mjs +279 -0
  57. package/main/sign-in-files.mjs +39 -0
  58. package/main/source-updater.mjs +298 -0
  59. package/main/store/home.mjs +47 -2
  60. package/main/store/work-items.mjs +150 -6
  61. package/main/store.mjs +235 -9
  62. package/main/supervisor.mjs +1444 -406
  63. package/main/task-commands.mjs +8 -10
  64. package/main/task-folders.mjs +74 -13
  65. package/main/task-remote-control.mjs +3 -3
  66. package/main/task-terminals.mjs +2 -1
  67. package/main/team/index.mjs +440 -0
  68. package/main/team/memory-cloud.mjs +247 -0
  69. package/main/team/projects.mjs +162 -0
  70. package/main/team/session.mjs +225 -0
  71. package/main/team/sign-in.mjs +106 -0
  72. package/main/team/supabase-backend.mjs +231 -0
  73. package/main/team/sync.mjs +163 -0
  74. package/main/updater.mjs +15 -5
  75. package/main/write-before-reload.mjs +50 -0
  76. package/mcp/core/page.mjs +9 -11
  77. package/mcp/core/work.mjs +91 -25
  78. package/mcp/tools.mjs +12 -6
  79. package/package.json +21 -9
  80. package/renderer/dist/assets/agentbox-icon-F1oN9nj_.svg +15 -0
  81. package/renderer/dist/assets/index-B-2Az952.css +32 -0
  82. package/renderer/dist/assets/index-D6qncgai.js +1168 -0
  83. package/renderer/dist/index.html +2 -2
  84. package/scripts/memory-gate-hook.sh +59 -0
  85. package/shared/agent-import.mjs +38 -15
  86. package/shared/agents.d.mts +26 -8
  87. package/shared/agents.mjs +100 -108
  88. package/shared/analytics-events.mjs +19 -2
  89. package/shared/answers.mjs +12 -0
  90. package/shared/bridge-map.d.mts +12 -0
  91. package/shared/bridge-map.mjs +37 -1
  92. package/shared/claude-commands.generated.mjs +2 -2
  93. package/shared/claude-models.generated.mjs +4 -4
  94. package/shared/claude-usage.mjs +6 -6
  95. package/shared/codex-import.mjs +7 -7
  96. package/shared/codex-modes.d.mts +50 -0
  97. package/shared/codex-usage.mjs +7 -8
  98. package/shared/demo-world.mjs +5 -5
  99. package/shared/engines.mjs +27 -10
  100. package/shared/feedback.d.mts +14 -0
  101. package/shared/feedback.mjs +44 -0
  102. package/shared/first-run-examples.mjs +4 -4
  103. package/shared/first-run-practice.d.mts +16 -0
  104. package/shared/first-run-practice.mjs +176 -70
  105. package/shared/fresh-user-home.mjs +22 -3
  106. package/shared/instruction-defaults.json +2 -2
  107. package/shared/product-name.mjs +4 -2
  108. package/shared/project-folder-check.mjs +6 -0
  109. package/shared/rank.d.mts +1 -0
  110. package/shared/rank.mjs +22 -0
  111. package/shared/referenced-files.mjs +1 -1
  112. package/shared/schemes.d.mts +11 -0
  113. package/shared/schemes.mjs +1 -1
  114. package/shared/shortcuts.d.mts +41 -0
  115. package/shared/shortcuts.mjs +236 -0
  116. package/shared/spawn-trouble.d.mts +2 -0
  117. package/shared/spawn-trouble.mjs +39 -18
  118. package/shared/team-rules.d.mts +17 -0
  119. package/shared/team-rules.mjs +230 -0
  120. package/shared/team-status.d.mts +11 -0
  121. package/shared/team-status.mjs +90 -0
  122. package/shared/terminal-links.d.mts +2 -0
  123. package/shared/terminal-links.mjs +55 -0
  124. package/shared/terminal-state.d.mts +1 -0
  125. package/shared/terminal-state.mjs +16 -0
  126. package/shared/thread-cards.d.mts +19 -0
  127. package/shared/thread-cards.mjs +254 -0
  128. package/shared/usage.d.mts +1 -1
  129. package/shared/usage.mjs +6 -7
  130. package/shared/work-items.mjs +255 -27
  131. package/shared/work-lines.mjs +393 -16
  132. package/worker-permissions.json +1 -1
  133. package/renderer/dist/assets/cel-dusk-CS5I7kJS.webp +0 -0
  134. package/renderer/dist/assets/cel-dusk-DUslqS6B.webp +0 -0
  135. package/renderer/dist/assets/cel-dusk-Jt_xVGxp.webp +0 -0
  136. package/renderer/dist/assets/cel-dusk-sm-BBkV-tWT.webp +0 -0
  137. package/renderer/dist/assets/frost-haze-CxPSOBEg.webp +0 -0
  138. package/renderer/dist/assets/frost-haze-sm-C_w21qgd.webp +0 -0
  139. package/renderer/dist/assets/ice-BC8luINc.webp +0 -0
  140. package/renderer/dist/assets/ice-CP9PrKHo.webp +0 -0
  141. package/renderer/dist/assets/ice-river-D6ziOhLY.webp +0 -0
  142. package/renderer/dist/assets/ice-river-UVM2-JY_.webp +0 -0
  143. package/renderer/dist/assets/ice-river-bErJwYWW.webp +0 -0
  144. package/renderer/dist/assets/ice-river-sm-Bg1U-SIj.webp +0 -0
  145. package/renderer/dist/assets/ice-sm-CbVcg3_D.webp +0 -0
  146. package/renderer/dist/assets/index-B7hhfHrT.js +0 -684
  147. package/renderer/dist/assets/index-CwiDv5bL.css +0 -32
  148. package/renderer/dist/assets/lake-C0WaW-6E.webp +0 -0
  149. package/renderer/dist/assets/lake-CSKjLC5K.webp +0 -0
  150. package/renderer/dist/assets/lake-ETGcOb25.webp +0 -0
  151. package/renderer/dist/assets/lake-harbour-BCQRbrHb.webp +0 -0
  152. package/renderer/dist/assets/lake-harbour-B_wfXTmE.webp +0 -0
  153. package/renderer/dist/assets/lake-harbour-Te1pEvBh.webp +0 -0
  154. package/renderer/dist/assets/lake-harbour-sm-Cme_ShEg.webp +0 -0
  155. package/renderer/dist/assets/lake-sm-CbG72Hbv.webp +0 -0
  156. package/renderer/dist/assets/lino-coast-CUZHcucI.webp +0 -0
  157. package/renderer/dist/assets/lino-coast-DP4J7cgd.webp +0 -0
  158. package/renderer/dist/assets/lino-coast-kOIAffr_.webp +0 -0
  159. package/renderer/dist/assets/lino-coast-sm-C1Mdbgit.webp +0 -0
  160. package/renderer/dist/assets/moon-pines-BskgnnXR.webp +0 -0
  161. package/renderer/dist/assets/moon-pines-CFSIIwt0.webp +0 -0
  162. package/renderer/dist/assets/moon-pines-PNhdOYww.webp +0 -0
  163. package/renderer/dist/assets/moon-pines-sm-C6K1NomK.webp +0 -0
  164. package/renderer/dist/assets/orbit-rings-2XV36Ch3.webp +0 -0
  165. package/renderer/dist/assets/orbit-rings-C4NrcYGQ.webp +0 -0
  166. package/renderer/dist/assets/orbit-rings-DGLk07dT.webp +0 -0
  167. package/renderer/dist/assets/orbit-rings-sm-Dky3YdvL.webp +0 -0
  168. package/renderer/dist/assets/peach-haze-2-C6f7xnWw.webp +0 -0
  169. package/renderer/dist/assets/peach-haze-2-sm-Dj_Ez_Sb.webp +0 -0
  170. package/renderer/dist/assets/pixel-harbour-DQjqMROg.webp +0 -0
  171. package/renderer/dist/assets/pixel-harbour-Dw71H_bc.webp +0 -0
  172. package/renderer/dist/assets/pixel-harbour-kOZfm-GM.webp +0 -0
  173. package/renderer/dist/assets/pixel-harbour-sm-Dturjuky.webp +0 -0
  174. package/renderer/dist/assets/pixel-orbit-C296JVMO.webp +0 -0
  175. package/renderer/dist/assets/pixel-orbit-C4oMRDSx.webp +0 -0
  176. package/renderer/dist/assets/pixel-orbit-CqVmQ4eh.webp +0 -0
  177. package/renderer/dist/assets/pixel-orbit-sm-CHagC_nO.webp +0 -0
  178. package/renderer/dist/assets/pixel-ridge-C6LYu6uE.webp +0 -0
  179. package/renderer/dist/assets/pixel-ridge-CIUCEESs.webp +0 -0
  180. package/renderer/dist/assets/pixel-ridge-sm-BnE0F_w-.webp +0 -0
  181. package/renderer/dist/assets/riso-hills-7heKVtry.webp +0 -0
  182. package/renderer/dist/assets/riso-hills-CIjAL3J6.webp +0 -0
  183. package/renderer/dist/assets/riso-hills-sm-DHsnbCfP.webp +0 -0
  184. package/renderer/dist/assets/slate-haze-BPWdkNsw.webp +0 -0
  185. package/renderer/dist/assets/slate-haze-sm-B0Cb5a2W.webp +0 -0
  186. package/renderer/dist/assets/valley-haze-BDHPqoJy.webp +0 -0
  187. package/renderer/dist/assets/valley-haze-sm-CTyjmPAG.webp +0 -0
  188. package/renderer/dist/assets/vector-dunes-DzzWLdKo.webp +0 -0
  189. package/renderer/dist/assets/vector-dunes-Yr9NBQUy.webp +0 -0
  190. package/renderer/dist/assets/vector-dunes-sm-D8MYY6ms.webp +0 -0
  191. package/renderer/dist/assets/watercolour-mist-BCGKN4u4.webp +0 -0
  192. package/renderer/dist/assets/watercolour-mist-BF-jk5MC.webp +0 -0
  193. package/renderer/dist/assets/watercolour-mist-sm-Ds1xzTw9.webp +0 -0
  194. package/renderer/dist/assets/woodblock-sea-B1um67Z9.webp +0 -0
  195. package/renderer/dist/assets/woodblock-sea-DKQdYs32.webp +0 -0
  196. package/renderer/dist/assets/woodblock-sea-sm-BJzNq8xh.webp +0 -0
  197. package/renderer/dist/emulator/audio-rules.mjs +0 -74
  198. package/renderer/dist/emulator/data/compression/README.md +0 -11
  199. package/renderer/dist/emulator/data/compression/extract7z.js +0 -1
  200. package/renderer/dist/emulator/data/compression/extractzip.js +0 -1
  201. package/renderer/dist/emulator/data/compression/libunrar.js +0 -519
  202. package/renderer/dist/emulator/data/compression/libunrar.wasm +0 -0
  203. package/renderer/dist/emulator/data/cores/README.md +0 -22
  204. package/renderer/dist/emulator/data/cores/mgba-legacy-wasm.data +0 -0
  205. package/renderer/dist/emulator/data/cores/mgba-thread-legacy-wasm.data +0 -0
  206. package/renderer/dist/emulator/data/cores/mgba-thread-wasm.data +0 -0
  207. package/renderer/dist/emulator/data/cores/mgba-wasm.data +0 -0
  208. package/renderer/dist/emulator/data/cores/package.json +0 -65
  209. package/renderer/dist/emulator/data/cores/reports/mgba.json +0 -1
  210. package/renderer/dist/emulator/data/emulator.css +0 -1614
  211. package/renderer/dist/emulator/data/loader.js +0 -170
  212. package/renderer/dist/emulator/data/localization/README.md +0 -63
  213. package/renderer/dist/emulator/data/localization/af-FR.json +0 -339
  214. package/renderer/dist/emulator/data/localization/ar-AR.json +0 -301
  215. package/renderer/dist/emulator/data/localization/ben-BEN.json +0 -301
  216. package/renderer/dist/emulator/data/localization/de-GER.json +0 -301
  217. package/renderer/dist/emulator/data/localization/el-GR.json +0 -301
  218. package/renderer/dist/emulator/data/localization/en-US.json +0 -339
  219. package/renderer/dist/emulator/data/localization/es-ES.json +0 -310
  220. package/renderer/dist/emulator/data/localization/fa-AF.json +0 -310
  221. package/renderer/dist/emulator/data/localization/hi-HI.json +0 -301
  222. package/renderer/dist/emulator/data/localization/it-IT.json +0 -302
  223. package/renderer/dist/emulator/data/localization/ja-JA.json +0 -301
  224. package/renderer/dist/emulator/data/localization/jv-JV.json +0 -301
  225. package/renderer/dist/emulator/data/localization/ko-KO.json +0 -336
  226. package/renderer/dist/emulator/data/localization/pt-BR.json +0 -361
  227. package/renderer/dist/emulator/data/localization/retroarch.json +0 -617
  228. package/renderer/dist/emulator/data/localization/ro-RO.json +0 -310
  229. package/renderer/dist/emulator/data/localization/ru-RU.json +0 -301
  230. package/renderer/dist/emulator/data/localization/tr-TR.json +0 -310
  231. package/renderer/dist/emulator/data/localization/vi-VN.json +0 -302
  232. package/renderer/dist/emulator/data/localization/zh-CN.json +0 -351
  233. package/renderer/dist/emulator/data/src/GameManager.js +0 -464
  234. package/renderer/dist/emulator/data/src/compression.js +0 -142
  235. package/renderer/dist/emulator/data/src/emulator.js +0 -6158
  236. package/renderer/dist/emulator/data/src/gamepad.js +0 -167
  237. package/renderer/dist/emulator/data/src/nipplejs.js +0 -1
  238. package/renderer/dist/emulator/data/src/shaders.js +0 -236
  239. package/renderer/dist/emulator/data/src/socket.io.min.js +0 -7
  240. package/renderer/dist/emulator/data/src/storage.js +0 -125
  241. package/renderer/dist/emulator/data/version.json +0 -5
  242. package/renderer/dist/emulator/host.js +0 -299
  243. package/renderer/dist/emulator/index.html +0 -28
  244. 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.
@@ -377,7 +482,41 @@ export class Store {
377
482
  );
378
483
  }
379
484
 
380
- composeItem(slug, { title, body, kind = 'directive', priority = 0, labels, runAt, engine, model, effort }) {
485
+ // WHO A SHARED ROW IS WITH, changed by a person: handed to an agent, kept,
486
+ // handed back, or passed on by a reply. Only the team fields, on the
487
+ // person's own authority, so an agent writing afterwards cannot undo it.
488
+ teamPatch(slug, id, patch) {
489
+ const allowed = Object.fromEntries(Object.entries(patch ?? {}).filter(([k]) => ['assignee', 'runner', 'due', 'people'].includes(k)));
490
+ if (!Object.keys(allowed).length) throw new Error('nothing to change');
491
+ return this.modules.workItemsDisk.updateWorkItem(this.productDir(slug), id, allowed, { source: 'founder' });
492
+ }
493
+
494
+ // A REACTION ON ONE MESSAGE (w-560647d4db): the chips under it in a chat.
495
+ // `on` is the uid of the ledger line the message was written as, which is the
496
+ // one name for a message that is the same on every teammate's Mac. Written as
497
+ // the person's own word, and appended like anything else: the fold keeps a
498
+ // press per person rather than one value per row, so two people reacting at
499
+ // the same moment keep both chips (shared/work-items.mjs).
500
+ react(slug, id, { on, emoji, off = false }) {
501
+ return this.modules.workItemsDisk.updateWorkItem(
502
+ this.productDir(slug), id, { react: off ? { on, emoji, off: true } : { on, emoji } }, { source: 'founder' },
503
+ );
504
+ }
505
+
506
+ // AN EDIT TO A THREAD FROM ITS SUMMARY (approved 2026-10-01): the summary's
507
+ // three lines and its links, who sees it, and its priority. Written as the
508
+ // person's own words; on the summary the later write wins either way
509
+ // (SHARED_FIELDS in shared/work-items.mjs).
510
+ threadEdit(slug, id, patch) {
511
+ // `start` is how a thread leaves Later: the summary and the thread page
512
+ // both write 'now' on it, which is what makes `isDue` true again.
513
+ const fields = ['problem', 'progress', 'solution', 'visibility', 'visibleTo', 'priority', 'blockedBy', 'blocks', 'start'];
514
+ const allowed = Object.fromEntries(Object.entries(patch ?? {}).filter(([k]) => fields.includes(k)));
515
+ if (!Object.keys(allowed).length) throw new Error('nothing to change');
516
+ return this.modules.workItemsDisk.updateWorkItem(this.productDir(slug), id, allowed, { source: 'founder' });
517
+ }
518
+
519
+ composeItem(slug, { title, body, kind = 'directive', priority = 0, labels, runAt, start, engine, model, effort, assignee, due, people, visibility, visibleTo }) {
381
520
  const { workItemsDisk } = this.modules;
382
521
  const dir = this.productDir(slug);
383
522
  // The 'founder' label is the human-in-the-loop marker: only items the
@@ -396,6 +535,9 @@ export class Store {
396
535
  title,
397
536
  ...(body ? { body } : {}),
398
537
  ...(runAt ? { runAt: Math.max(0, Math.trunc(runAt)) } : {}),
538
+ // "Add it to Later" on the card: the thread exists now and starts when
539
+ // she says, which is the one answer on that menu with no clock in it.
540
+ ...(start === 'later' ? { start: 'later' } : {}),
399
541
  // WHICH MODEL SHE CHOSE FOR THIS ONE, on founder authority like the rest
400
542
  // of her content (2026-08-26).It is a Claude Code alias, and the spawn
401
543
  // hands it to `--model`.
@@ -417,8 +559,27 @@ export class Store {
417
559
  // whole gate exists for. Whether the word may be written at all is decided
418
560
  // before it gets here, by `Supervisor#engineOffered`.
419
561
  ...(engine ? { engine: String(engine) } : {}),
562
+ // A TASK GIVEN TO A PERSON (the team version): who has to do it and
563
+ // when. No agent runs on it until that person hands it to one
564
+ // (shared/team-rules.mjs), and it reaches their inbox, not the sender's.
565
+ ...(assignee ? { assignee: String(assignee) } : {}),
566
+ ...(due ? { due: String(due) } : {}),
567
+ ...(Array.isArray(people) && people.length ? { people } : {}),
568
+ // WHO SEES IT (approved 2026-10-01): the team by default, nobody but its
569
+ // owner, or the people she chose (w-41ff964775). No field at all reads
570
+ // as the team, except in your personal project, where it reads as Only
571
+ // you (w-b989839656). So Team is written when it was chosen, which is
572
+ // how a thread in My Workspace is shared on purpose.
573
+ ...(visibility === 'private' || visibility === 'team' ? { visibility } : {}),
574
+ ...(visibility === 'people' && Array.isArray(visibleTo) && visibleTo.length
575
+ ? { visibility, visibleTo: [...new Set(visibleTo.map(String))] } : {}),
420
576
  };
421
- return workItemsDisk.updateWorkItem(dir, created.id, contentPatch, { source: 'founder' });
577
+ // WITH ITS PROJECT ON IT, the way `readItem` answers. The fold never carries
578
+ // one, and the renderer withdraws a just-made task with `made.product`, so
579
+ // without this Z after a send asked for "no such product: undefined" and
580
+ // reached past the task to whatever was under it (w-c78d1e1607).
581
+ const made = workItemsDisk.updateWorkItem(dir, created.id, contentPatch, { source: 'founder' });
582
+ return made ? { ...made, product: slug } : made;
422
583
  }
423
584
 
424
585
  /* ---------------------------- repeating tasks --------------------------- */
@@ -508,6 +669,9 @@ export class Store {
508
669
  name,
509
670
  createdAt: new Date().toISOString(),
510
671
  ...(repo ? { repoPath: repo } : {}),
672
+ // EVERY NEW PROJECT STARTS AS JUST YOU (w-b989839656): its threads
673
+ // reach the Team page only once you share the project or the thread.
674
+ seenBy: 'private',
511
675
  }, null, 2));
512
676
  // AND IT FILES NOTHING INTO HER INBOX.
513
677
  //
@@ -761,6 +925,47 @@ export class Store {
761
925
  return { ids };
762
926
  }
763
927
 
928
+ /**
929
+ * THE REST OF THE PRACTICE TEAM'S WORK, so the tour and the board are not
930
+ * empty (w-58c8f466e7). The copy, and which column each row lands in, is
931
+ * `PRACTICE_BACKDROP` in shared/first-run-practice.mjs. The ids come back in
932
+ * its order, which is how `walkRows` knows which one is which.
933
+ *
934
+ * The column is the app's own reading of the row, so each is written in the
935
+ * shape that reads that way and nothing marks it: a finished one has a
936
+ * result, a working one is her ask with a run and no result yet, and the
937
+ * scheduled one is her ask with a time in the future, set as hers. */
938
+ stagePracticeBackdrop(slug, { now = Date.now(), label = 'first-run' } = {}) {
939
+ const { workItemsDisk } = this.modules;
940
+ const dir = this.productDir(slug);
941
+ const ids = [];
942
+ for (const row of PRACTICE_BACKDROP) {
943
+ const at = stampsFor(row, now);
944
+ const created = workItemsDisk.createWorkItem(
945
+ dir,
946
+ { title: row.title, kind: row.kind, priority: 5, labels: ['founder', label] },
947
+ { source: 'system', now: at.hers },
948
+ );
949
+ workItemsDisk.updateWorkItem(
950
+ dir, created.id,
951
+ { title: row.title, body: row.body, ...(row.state === 'scheduled' ? { runAt: now + (row.inMs ?? 0) } : {}) },
952
+ { source: 'founder', now: at.hers },
953
+ );
954
+ if (row.state !== 'scheduled') {
955
+ this.appendPracticeRun(slug, created.id, at.hers);
956
+ this.writePracticeTrace(slug, created.id, row.trace, at.hers + 1_000);
957
+ }
958
+ if (row.state === 'needs') {
959
+ workItemsDisk.updateWorkItem(
960
+ dir, created.id, { result: row.result },
961
+ { source: 'agent', now: at.hers + 1_000 + row.trace.length * 9_000 + 1_000 },
962
+ );
963
+ }
964
+ ids.push(created.id);
965
+ }
966
+ return { ids };
967
+ }
968
+
764
969
  /**
765
970
  * THE CLAIM AND THE RELEASE, so the row reads as one an agent worked on.
766
971
  *
@@ -840,9 +1045,23 @@ export class Store {
840
1045
  // was for. Only when a line was really found: the sentence that says a folder
841
1046
  // has no readme in it is a true answer and it is not a description, so it is
842
1047
  // never written under the project's name.
843
- finishFirstRunTask(slug, id) {
1048
+ finishFirstRunTask(slug, id, round = 0) {
844
1049
  const { workItemsDisk } = this.modules;
845
1050
  const product = this.listProducts().find((p) => p.slug === slug) ?? null;
1051
+ // A REPLY TO IT IS ANSWERED HERE TOO (2026-10-06). The first answer offers
1052
+ // to make the summary shorter; any reply after that gets the shorter one,
1053
+ // and no second trace, because the run it did is already on the thread.
1054
+ // AND THE REPLY IS SETTLED FIRST, the way the supervisor settles one when a
1055
+ // session exits: without it the row reads as an answer still waiting for an
1056
+ // agent and stays in In progress (`answerSettled`, shared/answers.mjs).
1057
+ if (product?.practice && round > 0) {
1058
+ const before = this.readItem(slug, id);
1059
+ this.settleAnswer(slug, id, before?.wrote?.answer?.ts ?? 0);
1060
+ const item = workItemsDisk.updateWorkItem(
1061
+ this.productDir(slug), id, { result: PRACTICE_REPLY_ANSWER }, { source: 'agent' },
1062
+ );
1063
+ return { item, line: PRACTICE_REPLY_ANSWER, oneLiner: null };
1064
+ }
846
1065
  // AND SINCE 2026-08-23 THE TASK IS SENT IN THE PRACTICE PROJECT, which
847
1066
  // points at no folder at all. Everything below reads one, so it cannot run
848
1067
  // here: a project that is not theirs has nothing honest to say about their
@@ -1231,6 +1450,13 @@ export class Store {
1231
1450
  if (file && !/work-items\.jsonl|dashboard\.jsonl|project\.json/.test(file)) return;
1232
1451
  this._notify();
1233
1452
  });
1453
+ // fs.watch may return successfully, then emit an error (for example
1454
+ // EMFILE). An unhandled error takes down the app, outside this catch.
1455
+ // Reads still work without a watcher; release only the failed one.
1456
+ watcher.on('error', () => {
1457
+ try { watcher.close(); } catch {}
1458
+ this.watchers = this.watchers.filter((w) => w !== watcher);
1459
+ });
1234
1460
  this.watchers.push(watcher);
1235
1461
  } catch {}
1236
1462
  }