@songsid/agend 2.3.0-beta.22 → 2.3.0-beta.24

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 (298) hide show
  1. package/README.md +2 -2
  2. package/README.zh-TW.md +2 -2
  3. package/dist/agent-cli.js +4 -2
  4. package/dist/agent-cli.js.map +1 -1
  5. package/dist/agent-endpoint.d.ts +1 -0
  6. package/dist/agent-endpoint.js +4 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +5 -0
  9. package/dist/agent-whats-new.js +16 -1
  10. package/dist/agent-whats-new.js.map +1 -1
  11. package/dist/auth-api.d.ts +7 -0
  12. package/dist/auth-api.js +54 -21
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/codex-package.d.ts +30 -0
  15. package/dist/backend/codex-package.js +133 -0
  16. package/dist/backend/codex-package.js.map +1 -0
  17. package/dist/backend/codex.d.ts +2 -0
  18. package/dist/backend/codex.js +9 -2
  19. package/dist/backend/codex.js.map +1 -1
  20. package/dist/backend/grok.js +3 -1
  21. package/dist/backend/grok.js.map +1 -1
  22. package/dist/backend/kiro.d.ts +9 -1
  23. package/dist/backend/kiro.js +102 -1
  24. package/dist/backend/kiro.js.map +1 -1
  25. package/dist/backend/opencode-permission.js +1 -1
  26. package/dist/backend/opencode-permission.js.map +1 -1
  27. package/dist/backend/opencode.js +2 -1
  28. package/dist/backend/opencode.js.map +1 -1
  29. package/dist/binary-probe.js +1 -1
  30. package/dist/bug-report-cli.js +11 -10
  31. package/dist/bug-report-cli.js.map +1 -1
  32. package/dist/bug-report.js +11 -10
  33. package/dist/bug-report.js.map +1 -1
  34. package/dist/bundled-skills.d.ts +7 -3
  35. package/dist/bundled-skills.js +16 -7
  36. package/dist/bundled-skills.js.map +1 -1
  37. package/dist/channel/adapters/discord.js +3 -3
  38. package/dist/channel/adapters/discord.js.map +1 -1
  39. package/dist/channel/mcp-server.js +12 -4
  40. package/dist/channel/mcp-server.js.map +1 -1
  41. package/dist/channel/mcp-tools.d.ts +3 -1
  42. package/dist/channel/mcp-tools.js +36 -0
  43. package/dist/channel/mcp-tools.js.map +1 -1
  44. package/dist/child-process.d.ts +21 -0
  45. package/dist/child-process.js +54 -0
  46. package/dist/child-process.js.map +1 -0
  47. package/dist/cli.js +188 -204
  48. package/dist/cli.js.map +1 -1
  49. package/dist/command-table.d.ts +1 -1
  50. package/dist/command-table.js +1 -1
  51. package/dist/command-table.js.map +1 -1
  52. package/dist/config-validator.js +20 -1
  53. package/dist/config-validator.js.map +1 -1
  54. package/dist/config.js +3 -1
  55. package/dist/config.js.map +1 -1
  56. package/dist/daemon.d.ts +8 -0
  57. package/dist/daemon.js +31 -8
  58. package/dist/daemon.js.map +1 -1
  59. package/dist/darwin-memory.js +1 -1
  60. package/dist/doctor.d.ts +5 -0
  61. package/dist/doctor.js +42 -0
  62. package/dist/doctor.js.map +1 -1
  63. package/dist/export-import.js +4 -3
  64. package/dist/export-import.js.map +1 -1
  65. package/dist/fleet-level-config.d.ts +1 -1
  66. package/dist/fleet-level-config.js +2 -0
  67. package/dist/fleet-level-config.js.map +1 -1
  68. package/dist/fleet-manager.d.ts +45 -0
  69. package/dist/fleet-manager.js +324 -36
  70. package/dist/fleet-manager.js.map +1 -1
  71. package/dist/full-restart.d.ts +5 -5
  72. package/dist/full-restart.js +5 -5
  73. package/dist/full-restart.js.map +1 -1
  74. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +48 -10
  75. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  76. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +7 -8
  77. package/dist/general-knowledge/skills/multi-channel/SKILL.md +1 -1
  78. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +319 -0
  79. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +529 -0
  80. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +17 -4
  81. package/dist/general-knowledge/steering/core-rules.md +1 -1
  82. package/dist/instance-config-impact.js +10 -3
  83. package/dist/instance-config-impact.js.map +1 -1
  84. package/dist/instance-lifecycle.js +4 -4
  85. package/dist/locale.js +296 -4
  86. package/dist/locale.js.map +1 -1
  87. package/dist/login-flows.js +1 -1
  88. package/dist/login-shell.js +3 -1
  89. package/dist/login-shell.js.map +1 -1
  90. package/dist/ls-rows.js +1 -1
  91. package/dist/needs-you-hub.d.ts +98 -1
  92. package/dist/needs-you-hub.js +313 -44
  93. package/dist/needs-you-hub.js.map +1 -1
  94. package/dist/needs-you-live.d.ts +5 -2
  95. package/dist/needs-you-live.js +6 -3
  96. package/dist/needs-you-live.js.map +1 -1
  97. package/dist/needs-you.d.ts +58 -3
  98. package/dist/needs-you.js +36 -3
  99. package/dist/needs-you.js.map +1 -1
  100. package/dist/outbound-schemas.d.ts +3 -3
  101. package/dist/public-link-progress.d.ts +12 -1
  102. package/dist/public-link-progress.js +5 -10
  103. package/dist/public-link-progress.js.map +1 -1
  104. package/dist/public-tunnel-config.d.ts +5 -0
  105. package/dist/public-tunnel-config.js +45 -1
  106. package/dist/public-tunnel-config.js.map +1 -1
  107. package/dist/public-web-link.js +6 -5
  108. package/dist/public-web-link.js.map +1 -1
  109. package/dist/quickstart.js +15 -9
  110. package/dist/quickstart.js.map +1 -1
  111. package/dist/resource-report.js +1 -1
  112. package/dist/scheduler/task-board.d.ts +41 -1
  113. package/dist/scheduler/task-board.js +152 -5
  114. package/dist/scheduler/task-board.js.map +1 -1
  115. package/dist/sd-notify.js +2 -1
  116. package/dist/sd-notify.js.map +1 -1
  117. package/dist/service-installer.d.ts +69 -0
  118. package/dist/service-installer.js +182 -0
  119. package/dist/service-installer.js.map +1 -1
  120. package/dist/service-owner.d.ts +44 -0
  121. package/dist/service-owner.js +48 -0
  122. package/dist/service-owner.js.map +1 -0
  123. package/dist/service-plan.d.ts +8 -0
  124. package/dist/service-plan.js +13 -0
  125. package/dist/service-plan.js.map +1 -1
  126. package/dist/settings-api.d.ts +4 -0
  127. package/dist/settings-api.js +21 -3
  128. package/dist/settings-api.js.map +1 -1
  129. package/dist/settings-change.js +88 -4
  130. package/dist/settings-change.js.map +1 -1
  131. package/dist/settings-confirmation.d.ts +2 -0
  132. package/dist/settings-confirmation.js +14 -8
  133. package/dist/settings-confirmation.js.map +1 -1
  134. package/dist/settings-control.js +2 -1
  135. package/dist/settings-control.js.map +1 -1
  136. package/dist/settings-effect.js +2 -0
  137. package/dist/settings-effect.js.map +1 -1
  138. package/dist/settings-http-confirmation.d.ts +2 -0
  139. package/dist/settings-http-confirmation.js +3 -2
  140. package/dist/settings-http-confirmation.js.map +1 -1
  141. package/dist/settings-request-capability.d.ts +4 -1
  142. package/dist/settings-request-capability.js +11 -3
  143. package/dist/settings-request-capability.js.map +1 -1
  144. package/dist/setup-auth.d.ts +1 -0
  145. package/dist/setup-auth.js +2 -1
  146. package/dist/setup-auth.js.map +1 -1
  147. package/dist/setup-host.d.ts +1 -0
  148. package/dist/setup-host.js +7 -3
  149. package/dist/setup-host.js.map +1 -1
  150. package/dist/setup-wizard.js +1 -1
  151. package/dist/setup-wizard.js.map +1 -1
  152. package/dist/task-board-summary.d.ts +101 -0
  153. package/dist/task-board-summary.js +177 -0
  154. package/dist/task-board-summary.js.map +1 -0
  155. package/dist/task-board-waker.d.ts +8 -1
  156. package/dist/task-board-waker.js +15 -6
  157. package/dist/task-board-waker.js.map +1 -1
  158. package/dist/tmux-control.js +1 -1
  159. package/dist/tmux-control.js.map +1 -1
  160. package/dist/tmux-exec-helper.d.ts +13 -3
  161. package/dist/tmux-exec-helper.js +34 -29
  162. package/dist/tmux-exec-helper.js.map +1 -1
  163. package/dist/tmux-manager.js +1 -1
  164. package/dist/tmux-read.js +1 -1
  165. package/dist/tmux-read.js.map +1 -1
  166. package/dist/topic-commands.d.ts +3 -0
  167. package/dist/topic-commands.js +29 -18
  168. package/dist/topic-commands.js.map +1 -1
  169. package/dist/transcript-monitor.js +5 -0
  170. package/dist/transcript-monitor.js.map +1 -1
  171. package/dist/transcript-sources.d.ts +2 -0
  172. package/dist/transcript-sources.js +4 -0
  173. package/dist/transcript-sources.js.map +1 -1
  174. package/dist/tunnel/cloudflared-install.js +3 -1
  175. package/dist/tunnel/cloudflared-install.js.map +1 -1
  176. package/dist/tunnel/cloudflared.d.ts +1 -1
  177. package/dist/tunnel/cloudflared.js +51 -10
  178. package/dist/tunnel/cloudflared.js.map +1 -1
  179. package/dist/tunnel/named-diagnostics.d.ts +13 -0
  180. package/dist/tunnel/named-diagnostics.js +52 -0
  181. package/dist/tunnel/named-diagnostics.js.map +1 -0
  182. package/dist/tunnel/types.d.ts +2 -1
  183. package/dist/tunnel/types.js.map +1 -1
  184. package/dist/types.d.ts +9 -0
  185. package/dist/ui/app-effects.js +10 -4
  186. package/dist/ui/chat-strings.js +3 -0
  187. package/dist/ui/chat-thread.js +14 -7
  188. package/dist/ui/effect-trial-offer.js +2 -31
  189. package/dist/ui/effects-waker.js +2 -70
  190. package/dist/ui/fleet-task-card.js +5 -5
  191. package/dist/ui/fleet-task-common.js +43 -9
  192. package/dist/ui/fleet-task-work.js +107 -0
  193. package/dist/ui/fleet-tasks.js +46 -12
  194. package/dist/ui/image-lightbox.js +39 -1
  195. package/dist/ui/image-zoom.js +242 -0
  196. package/dist/ui/panel-chat.js +24 -13
  197. package/dist/ui/panel-details.js +2 -0
  198. package/dist/ui/panel-fleet.js +6 -2
  199. package/dist/ui/panel-needs.js +21 -6
  200. package/dist/ui/panel-settings.js +35 -13
  201. package/dist/ui/preview-panel.js +2 -1
  202. package/dist/ui/search-dialog.js +2 -1
  203. package/dist/ui/settings-appearance.js +44 -10
  204. package/dist/ui/settings-dialogs.js +33 -17
  205. package/dist/ui/settings-discord.js +6 -2
  206. package/dist/ui/settings-guests.js +2 -1
  207. package/dist/ui/settings-model.js +2 -11
  208. package/dist/ui/settings-mods.js +8 -3
  209. package/dist/ui/settings-strings.js +32 -0
  210. package/dist/ui/settings-token.js +3 -1
  211. package/dist/ui/settings-web-authority.js +6 -3
  212. package/dist/ui/settings-wizard.js +13 -7
  213. package/dist/ui/shared/app-i18n.js +2 -2
  214. package/dist/ui/shared/app-needs.js +2 -2
  215. package/dist/ui/shared/app-store.js +2 -2
  216. package/dist/ui/shared/app.css +93 -32
  217. package/dist/ui/shared/app.js +5 -1
  218. package/dist/ui/shared/effect-app-runtime.js +7 -5
  219. package/dist/ui/shared/effect-chat-runtime.js +2 -7
  220. package/dist/ui/shared/effect-cursor.js +3 -2
  221. package/dist/ui/shared/effect-display-metadata.js +39 -0
  222. package/dist/ui/shared/effect-registry.js +4 -2
  223. package/dist/ui/shared/effect-runtime-loader.js +3 -1
  224. package/dist/ui/shared/effect-stock-trust.js +5 -5
  225. package/dist/ui/shared/feature-flags.js +127 -0
  226. package/dist/ui/shared/identity-color.js +6 -3
  227. package/dist/ui/shared/instance-nav.js +3 -2
  228. package/dist/ui/shared/panel-view.js +14 -29
  229. package/dist/ui/shared/text-undo.js +210 -0
  230. package/dist/ui/shared/ui-dialog.js +4 -3
  231. package/dist/ui/shared/ui-icons.js +2 -0
  232. package/dist/ui/shared/usage-dialog.js +29 -8
  233. package/dist/ui/shared/view-strings.js +2 -4
  234. package/dist/ui/web-terminal/terminal.html +10 -10
  235. package/dist/ui/web-terminal/terminal.js +58 -12
  236. package/dist/update-dispatch.js +1 -1
  237. package/dist/update-launch.d.ts +1 -1
  238. package/dist/update-launch.js +1 -2
  239. package/dist/update-launch.js.map +1 -1
  240. package/dist/update-preflight.js +1 -1
  241. package/dist/usage/providers.js +1 -1
  242. package/dist/usage/providers.js.map +1 -1
  243. package/dist/view-api.js +1 -1
  244. package/dist/web-api.d.ts +6 -0
  245. package/dist/web-api.js +163 -21
  246. package/dist/web-api.js.map +1 -1
  247. package/dist/web-app-icon-worker.js +2 -1
  248. package/dist/web-app-icon-worker.js.map +1 -1
  249. package/dist/web-app-icon.d.ts +10 -8
  250. package/dist/web-app-icon.js +14 -13
  251. package/dist/web-app-icon.js.map +1 -1
  252. package/dist/web-appearance-tools.d.ts +1 -0
  253. package/dist/web-appearance-tools.js +7 -3
  254. package/dist/web-appearance-tools.js.map +1 -1
  255. package/dist/web-appearance.d.ts +4 -10
  256. package/dist/web-appearance.js +4 -3
  257. package/dist/web-appearance.js.map +1 -1
  258. package/dist/web-auth.d.ts +8 -4
  259. package/dist/web-auth.js +28 -17
  260. package/dist/web-auth.js.map +1 -1
  261. package/dist/web-chat-history.d.ts +14 -0
  262. package/dist/web-chat-history.js +26 -1
  263. package/dist/web-chat-history.js.map +1 -1
  264. package/dist/web-cookie-names.d.ts +5 -0
  265. package/dist/web-cookie-names.js +12 -0
  266. package/dist/web-cookie-names.js.map +1 -0
  267. package/dist/web-experimental-http.d.ts +2 -0
  268. package/dist/web-experimental-http.js +12 -0
  269. package/dist/web-experimental-http.js.map +1 -0
  270. package/dist/web-experimental.d.ts +11 -0
  271. package/dist/web-experimental.js +49 -0
  272. package/dist/web-experimental.js.map +1 -0
  273. package/dist/web-guest-access.d.ts +1 -0
  274. package/dist/web-guest-access.js +4 -2
  275. package/dist/web-guest-access.js.map +1 -1
  276. package/dist/web-mod-store.d.ts +2 -0
  277. package/dist/web-mod-store.js +10 -0
  278. package/dist/web-mod-store.js.map +1 -1
  279. package/dist/web-png.d.ts +4 -0
  280. package/dist/web-png.js +10 -2
  281. package/dist/web-png.js.map +1 -1
  282. package/dist/web-preview.d.ts +2 -0
  283. package/dist/web-preview.js +2 -0
  284. package/dist/web-preview.js.map +1 -1
  285. package/dist/web-returning-device.d.ts +4 -2
  286. package/dist/web-returning-device.js +12 -5
  287. package/dist/web-returning-device.js.map +1 -1
  288. package/dist/web-settings-patch.d.ts +2 -1
  289. package/dist/web-settings-patch.js +5 -1
  290. package/dist/web-settings-patch.js.map +1 -1
  291. package/dist/web-settings-tools.d.ts +1 -0
  292. package/dist/web-settings-tools.js +14 -10
  293. package/dist/web-settings-tools.js.map +1 -1
  294. package/dist/web-terminal.js +1 -1
  295. package/dist/web-theme-store.d.ts +2 -2
  296. package/dist/web-theme-store.js +6 -3
  297. package/dist/web-theme-store.js.map +1 -1
  298. package/package.json +11 -11
@@ -45,7 +45,8 @@ export class TaskBoardError extends Error {
45
45
  }
46
46
  const STATUS_ORDER = `CASE status WHEN 'claimed' THEN 0 WHEN 'blocked' THEN 1 WHEN 'review' THEN 2 WHEN 'open' THEN 3 WHEN 'done' THEN 4 ELSE 5 END`;
47
47
  const PRIORITY_ORDER = `CASE priority WHEN 'urgent' THEN 0 WHEN 'high' THEN 1 WHEN 'normal' THEN 2 ELSE 3 END`;
48
- const CODE_RE = /^\s*([MS])\s*-?\s*(\d+)\s*$/i;
48
+ // Keep the second whitespace run inside the hyphen branch: without a hyphen, adjacent runs backtrack quadratically.
49
+ const CODE_RE = /^\s*([MS])\s*(?:-\s*)?(\d+)\s*$/i;
49
50
  const length = (s) => [...s].length;
50
51
  const isFinished = (c) => c.status === "done" || c.status === "cancelled";
51
52
  const isPerson = (a) => a.kind === "person";
@@ -166,12 +167,26 @@ function toCard(r) {
166
167
  seenEntryId: Number(r.seen_entry_id ?? 0),
167
168
  };
168
169
  }
170
+ /**
171
+ * #2352: a lead is a name, or none ("" and null alike). Whether it is one of the fleet's instances is the fleet's to
172
+ * check (the board keeps names, as for an assignee); a lead no longer in fleet.yaml reads as none.
173
+ */
174
+ function checkLeadInstance(raw) {
175
+ const name = (raw ?? "").trim();
176
+ if (!name)
177
+ return null;
178
+ // eslint-disable-next-line no-control-regex
179
+ if (name.length > 256 || /[\s\u0000-\u001f\u007f]/.test(name))
180
+ throw new TaskBoardError("invalid", "a lead is an instance's name", "lead_not_instance");
181
+ return name;
182
+ }
169
183
  function toProject(r) {
170
184
  return {
171
185
  id: Number(r.id),
172
186
  name: r.name,
173
187
  description: r.description,
174
188
  archived: Number(r.archived) === 1,
189
+ leadInstance: typeof r.lead_instance === "string" && r.lead_instance ? r.lead_instance : null,
175
190
  sort: Number(r.sort),
176
191
  createdAt: r.created_at,
177
192
  };
@@ -269,6 +284,17 @@ export class TaskBoard {
269
284
  CREATE INDEX IF NOT EXISTS idx_task_events_task ON task_events(task_id, id);
270
285
  CREATE TABLE IF NOT EXISTS task_counters (prefix TEXT PRIMARY KEY, next INTEGER NOT NULL);
271
286
  `);
287
+ // #2352: a project's lead instance, added in place (null: its waiting cards' buttons stay in General).
288
+ const projectCols = new Set(this.db.prepare("PRAGMA table_info(task_projects)").all().map((c) => c.name));
289
+ if (!projectCols.has("lead_instance")) {
290
+ try {
291
+ this.db.exec("ALTER TABLE task_projects ADD COLUMN lead_instance TEXT");
292
+ }
293
+ catch (err) {
294
+ if (!/duplicate column/i.test(String(err)))
295
+ throw err;
296
+ }
297
+ }
272
298
  // Every open (the CLI opens this file for each command) only reads: the write lock is taken only when a counter
273
299
  // is missing or a row needs a number. (CREATE … IF NOT EXISTS of what exists takes no lock; an INSERT does.)
274
300
  if (this.db.prepare("SELECT COUNT(*) AS n FROM task_counters WHERE prefix IN ('M', 'S', 'R')").get().n < 3) {
@@ -330,8 +356,9 @@ export class TaskBoard {
330
356
  const name = this.checkProjectName(input.name, null);
331
357
  const description = checkText("the description", input.description ?? "", CARD_LIMITS.projectDescription);
332
358
  const { s } = this.db.prepare("SELECT COALESCE(MAX(sort), 0) + 1 AS s FROM task_projects").get();
333
- const row = this.db.prepare("INSERT INTO task_projects (name, description, sort, created_at) VALUES (?, ?, ?, ?) RETURNING *")
334
- .get(name, description, s, this.nowIso());
359
+ const lead = checkLeadInstance(input.leadInstance ?? null);
360
+ const row = this.db.prepare("INSERT INTO task_projects (name, description, sort, created_at, lead_instance) VALUES (?, ?, ?, ?, ?) RETURNING *")
361
+ .get(name, description, s, this.nowIso(), lead);
335
362
  this.projectsChanged = true;
336
363
  return toProject(row);
337
364
  });
@@ -346,8 +373,10 @@ export class TaskBoard {
346
373
  project.description = checkText("the description", patch.description, CARD_LIMITS.projectDescription);
347
374
  if (patch.archived !== undefined)
348
375
  project.archived = patch.archived;
349
- this.db.prepare("UPDATE task_projects SET name = ?, description = ?, archived = ? WHERE id = ?")
350
- .run(project.name, project.description, project.archived ? 1 : 0, project.id);
376
+ if (patch.leadInstance !== undefined)
377
+ project.leadInstance = checkLeadInstance(patch.leadInstance);
378
+ this.db.prepare("UPDATE task_projects SET name = ?, description = ?, archived = ?, lead_instance = ? WHERE id = ?")
379
+ .run(project.name, project.description, project.archived ? 1 : 0, project.leadInstance, project.id);
351
380
  this.projectsChanged = true;
352
381
  return project;
353
382
  });
@@ -733,6 +762,11 @@ export class TaskBoard {
733
762
  * (its own open cards, and the unassigned sub tasks of main tasks it coordinates, dependencies done) and what waits
734
763
  * on dependencies; comments it has not seen and work sent back; the main tasks it coordinates and how far along.
735
764
  */
765
+ /** Who holds a live card (open, in progress, blocked, waiting for the person): the instances, and any other name. */
766
+ liveAssignees() {
767
+ return this.db.prepare("SELECT DISTINCT assignee FROM tasks WHERE assignee IS NOT NULL AND status IN ('open', 'claimed', 'blocked', 'review') ORDER BY assignee").all()
768
+ .map((r) => r.assignee);
769
+ }
736
770
  myWork(name) {
737
771
  const mine = this.db.prepare(`SELECT * FROM tasks WHERE assignee = ? AND status IN ('open', 'claimed', 'blocked', 'review') ORDER BY ${STATUS_ORDER}, ${PRIORITY_ORDER}, sort`)
738
772
  .all(name).map(toCard);
@@ -766,6 +800,113 @@ export class TaskBoard {
766
800
  mains: coordinating.map((card) => ({ card, progress: this.progress(card) })),
767
801
  };
768
802
  }
803
+ /**
804
+ * What waits for the person (step 3: Needs you): cards in review that need their confirmation, and blocked cards —
805
+ * oldest first, each with when it started waiting (its last move to that status).
806
+ */
807
+ waitingForPerson() {
808
+ const rows = this.db.prepare("SELECT * FROM tasks WHERE (status = 'review' AND needs_review = 1) OR status = 'blocked' ORDER BY updated_at").all().map(toCard);
809
+ const moved = this.db.prepare("SELECT at FROM task_events WHERE task_id = ? AND type = 'moved' AND json_extract(data, '$.to') = ? ORDER BY id DESC LIMIT 1");
810
+ return rows.map((card) => {
811
+ const at = moved.get(card.uuid, card.status)?.at;
812
+ return { card, since: at ?? card.submittedAt ?? card.updatedAt };
813
+ }).sort((a, b) => a.since.localeCompare(b.since));
814
+ }
815
+ /** Cards by their numbers, in one read (the ones that exist; #2341 review: the cards a summary line named). */
816
+ cardsByCode(codes) {
817
+ const out = [];
818
+ for (let i = 0; i < codes.length; i += 500) {
819
+ const chunk = codes.slice(i, i + 500);
820
+ out.push(...this.db.prepare(`SELECT * FROM tasks WHERE code IN (${chunk.map(() => "?").join(", ")})`).all(...chunk).map(toCard));
821
+ }
822
+ return out;
823
+ }
824
+ /**
825
+ * myWork() for many names in one read (the dashboard's Instances list, GET /ui/tasks/work): the live cards, every
826
+ * card's light row (sub tasks, dependencies) and the live cards' unseen entries, each read once — three statements
827
+ * however many names and cards (#2316 review: N myWork() calls cost tens of ms on the fleet's event loop). Same
828
+ * answers as myWork() name by name; tests/task-board-work-2316.test.ts pins both.
829
+ */
830
+ workOf(names) {
831
+ const out = new Map();
832
+ if (!names.length)
833
+ return out;
834
+ const live = this.db.prepare(`SELECT * FROM tasks WHERE status IN ('open', 'claimed', 'blocked', 'review') ORDER BY ${STATUS_ORDER}, ${PRIORITY_ORDER}, sort`).all().map(toCard);
835
+ const all = new Map();
836
+ const subs = new Map();
837
+ for (const r of this.db.prepare("SELECT id, code, status, depends_on, parent_id FROM tasks").all()) {
838
+ const light = { code: r.code ?? r.id.slice(0, 8), status: r.status, deps: parseJsonArray(r.depends_on, []), parent: r.parent_id ?? null };
839
+ all.set(r.id, light);
840
+ if (light.parent && light.status !== "cancelled") {
841
+ const p = subs.get(light.parent) ?? { total: 0, open: 0, claimed: 0, blocked: 0, review: 0, done: 0 };
842
+ p.total++;
843
+ p[light.status]++;
844
+ subs.set(light.parent, p);
845
+ }
846
+ }
847
+ // Unseen entries of the live cards: comments and rejections by anyone but the card's assignee, after what it saw.
848
+ const fresh = new Map();
849
+ for (const r of this.db.prepare(`SELECT e.* FROM task_entries e JOIN tasks t ON e.task_id = t.id
850
+ WHERE t.status IN ('open', 'claimed', 'blocked', 'review') AND e.id > t.seen_entry_id AND e.kind IN ('comment', 'reject') AND e.author IS NOT t.assignee ORDER BY e.id`).all()) {
851
+ const list = fresh.get(r.task_id) ?? [];
852
+ list.push({ id: Number(r.id), cardId: all.get(r.task_id)?.code ?? r.task_id.slice(0, 8), kind: r.kind, author: r.author,
853
+ authorKind: r.author_kind, text: r.text, at: r.at });
854
+ fresh.set(r.task_id, list);
855
+ }
856
+ /** unmetDeps(), on the light rows: own dependencies that exist and are not done, then the main task's not yet named. */
857
+ const unmet = (deps, parent) => {
858
+ const own = deps.filter((d) => { const dep = all.get(d); return !!dep && dep.status !== "done"; });
859
+ const main = parent ? all.get(parent) : undefined;
860
+ return main ? [...own, ...unmet(main.deps, main.parent).filter((d) => !own.includes(d))] : own;
861
+ };
862
+ const rank = { urgent: 0, high: 1, normal: 2 };
863
+ const byPriority = (a, b) => (rank[a.priority] ?? 3) - (rank[b.priority] ?? 3) || a.sort - b.sort;
864
+ const openUnassigned = live.filter((c) => c.assignee === null && c.status === "open");
865
+ for (const name of new Set(names)) {
866
+ const mine = live.filter((c) => c.assignee === name);
867
+ const coordinating = mine.filter((c) => c.kind === "main" && (subs.get(c.uuid)?.total ?? 0) > 0);
868
+ const work = mine.filter((c) => !coordinating.includes(c));
869
+ const theirs = new Set(coordinating.map((c) => c.uuid));
870
+ const unassigned = coordinating.length ? openUnassigned.filter((c) => c.parent !== null && theirs.has(c.parent)).sort(byPriority) : [];
871
+ const candidates = [...work.filter((c) => c.status === "open"), ...unassigned].map((card) => ({ card, waitsFor: unmet(card.deps, card.parent).map((d) => all.get(d).code) }));
872
+ const newComments = [];
873
+ const sentBack = [];
874
+ for (const card of mine) {
875
+ const entries = fresh.get(card.uuid) ?? [];
876
+ const reject = [...entries].reverse().find((e) => e.kind === "reject");
877
+ if (reject)
878
+ sentBack.push({ card, entry: reject });
879
+ const comments = entries.filter((e) => e.kind === "comment");
880
+ if (comments.length)
881
+ newComments.push({ card, entries: comments });
882
+ }
883
+ out.set(name, {
884
+ doing: work.filter((c) => c.status === "claimed"),
885
+ blocked: work.filter((c) => c.status === "blocked"),
886
+ review: work.filter((c) => c.status === "review"),
887
+ ready: candidates.filter((x) => !x.waitsFor.length).map((x) => x.card),
888
+ waiting: candidates.filter((x) => x.waitsFor.length),
889
+ newComments,
890
+ sentBack,
891
+ mains: coordinating.map((card) => ({ card, progress: { ...subs.get(card.uuid) } })),
892
+ });
893
+ }
894
+ return out;
895
+ }
896
+ /**
897
+ * Step 3, the daily summary (task-board-summary.ts): what waits for the person now, what got done and what was
898
+ * opened in [since, until) (not cancelled), and every live main task's project and assignee.
899
+ */
900
+ summaryRead(since, until) {
901
+ const between = (sql) => this.db.prepare(sql).all(since, until).map(toCard);
902
+ return {
903
+ waiting: this.waitingForPerson().map((w) => w.card),
904
+ done: between("SELECT * FROM tasks WHERE status = 'done' AND done_at >= ? AND done_at < ? ORDER BY sort"),
905
+ created: between("SELECT * FROM tasks WHERE status != 'cancelled' AND created_at >= ? AND created_at < ? ORDER BY sort"),
906
+ mains: this.db.prepare("SELECT project_id, assignee FROM tasks WHERE kind = 'main' AND status != 'cancelled'").all()
907
+ .map((r) => ({ projectId: r.project_id ?? null, assignee: r.assignee ?? null })),
908
+ };
909
+ }
769
910
  // ── Writing: the board ──────────────────────────────────
770
911
  createCards(actor, input) {
771
912
  if (input.cards.length === 0)
@@ -1683,6 +1824,9 @@ export class TaskBoard {
1683
1824
  add(card, card.assignee, "cancelled", e.actor, why);
1684
1825
  continue;
1685
1826
  }
1827
+ // Its dispatcher tells the person it waits for them (not the board).
1828
+ if (d.to === "review" && !isPerson(e.actor) && requiresReview(card))
1829
+ add(card, dispatcherOf(), "to_confirm", e.actor);
1686
1830
  if (d.auto) {
1687
1831
  if (d.to === "review" && card.kind === "main" && wrapUpGap(card))
1688
1832
  add(card, card.assignee, "wrap_up", e.actor);
@@ -1737,6 +1881,9 @@ export class TaskBoard {
1737
1881
  return false;
1738
1882
  if (r === "decide")
1739
1883
  return handedOut && card.status === "blocked";
1884
+ // #2341 review: and still waiting for the person — its confirmation not switched off during the gather.
1885
+ if (r === "to_confirm")
1886
+ return handedOut && card.status === "review" && requiresReview(card);
1740
1887
  if (!own)
1741
1888
  return false;
1742
1889
  if (r === "wrap_up")