@cohortapp/agent-sdk 2.17.0 → 2.18.5

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 (531) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +18 -5
  3. package/README.md +1 -0
  4. package/bin/maestro.mjs +62 -0
  5. package/docs/guides/billing-console-keys.md +60 -0
  6. package/docs/guides/front-door-session.md +54 -9
  7. package/docs/guides/mac-mini.md +20 -25
  8. package/docs/guides/setup-wizard.md +1 -1
  9. package/docs/runbooks/fleet-rollout.md +156 -0
  10. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  11. package/lib/action-executor.js +19 -3
  12. package/lib/budget-guard.mjs +279 -3
  13. package/lib/channels/base-adapter.mjs +3 -1
  14. package/lib/channels/contract.mjs +2 -1
  15. package/lib/channels/inbox-item.mjs +8 -0
  16. package/lib/claude-bin.mjs +5 -6
  17. package/lib/cli/doctor-checks.mjs +141 -10
  18. package/lib/cli/global-setup-extras.mjs +5 -1
  19. package/lib/cli/inbox.mjs +100 -15
  20. package/lib/cli/seat-auth.mjs +463 -0
  21. package/lib/cli/session.mjs +80 -12
  22. package/lib/collective/capture-slots.mjs +234 -0
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/config.mjs +2 -0
  25. package/lib/collective/global-config.mjs +63 -1
  26. package/lib/collective/loop-guard.mjs +155 -0
  27. package/lib/collective/presence.mjs +142 -5
  28. package/lib/comms/send-gate.mjs +559 -1
  29. package/lib/diagnostics/alerts.mjs +49 -0
  30. package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
  31. package/lib/engine/agents/definitions.mjs +343 -0
  32. package/lib/engine/agents/persist.mjs +275 -0
  33. package/lib/engine/agents/runtime.mjs +748 -0
  34. package/lib/engine/agents/usage.mjs +95 -0
  35. package/lib/engine/auth-status.mjs +139 -0
  36. package/lib/engine/budget.mjs +194 -0
  37. package/lib/engine/cli.mjs +1204 -0
  38. package/lib/engine/commands/index.mjs +269 -0
  39. package/lib/engine/context/budget.mjs +219 -0
  40. package/lib/engine/context/cache.mjs +125 -0
  41. package/lib/engine/context/child-env.mjs +215 -0
  42. package/lib/engine/context/compaction.mjs +342 -0
  43. package/lib/engine/context/images.mjs +90 -0
  44. package/lib/engine/context/instructions.mjs +327 -0
  45. package/lib/engine/context/lazy-instructions.mjs +169 -0
  46. package/lib/engine/context/manager.mjs +182 -0
  47. package/lib/engine/context/real-path.mjs +91 -0
  48. package/lib/engine/context/secret-values.mjs +163 -0
  49. package/lib/engine/context/settings.mjs +274 -0
  50. package/lib/engine/context/stream-input.mjs +159 -0
  51. package/lib/engine/guard.mjs +152 -0
  52. package/lib/engine/hooks.mjs +713 -0
  53. package/lib/engine/loop.mjs +560 -0
  54. package/lib/engine/mcp/client.mjs +254 -0
  55. package/lib/engine/mcp/config.mjs +301 -0
  56. package/lib/engine/mcp/http.mjs +201 -0
  57. package/lib/engine/mcp/index.mjs +146 -0
  58. package/lib/engine/mcp/jsonrpc.mjs +147 -0
  59. package/lib/engine/mcp/naming.mjs +66 -0
  60. package/lib/engine/mcp/resources.mjs +89 -0
  61. package/lib/engine/mcp/results.mjs +133 -0
  62. package/lib/engine/mcp/stdio.mjs +137 -0
  63. package/lib/engine/mcp/supervisor.mjs +116 -0
  64. package/lib/engine/messages.mjs +104 -0
  65. package/lib/engine/output/json.mjs +164 -0
  66. package/lib/engine/output/stream-json.mjs +266 -0
  67. package/lib/engine/permissions.mjs +845 -0
  68. package/lib/engine/process-identity.mjs +164 -0
  69. package/lib/engine/process-tree.mjs +551 -0
  70. package/lib/engine/prompt.mjs +60 -0
  71. package/lib/engine/session/store.mjs +299 -0
  72. package/lib/engine/session-runtime/args.mjs +97 -0
  73. package/lib/engine/session-runtime/host.mjs +143 -0
  74. package/lib/engine/session-runtime/inbox.mjs +122 -0
  75. package/lib/engine/session-runtime/notifications.mjs +129 -0
  76. package/lib/engine/session-runtime/registry.mjs +328 -0
  77. package/lib/engine/session-runtime/runner.mjs +344 -0
  78. package/lib/engine/session-runtime/socket.mjs +212 -0
  79. package/lib/engine/session-runtime/wakeup.mjs +115 -0
  80. package/lib/engine/skills/index.mjs +321 -0
  81. package/lib/engine/tools/bash-background.mjs +533 -0
  82. package/lib/engine/tools/bash.mjs +216 -0
  83. package/lib/engine/tools/edit.mjs +97 -0
  84. package/lib/engine/tools/glob.mjs +81 -0
  85. package/lib/engine/tools/grep.mjs +224 -0
  86. package/lib/engine/tools/index.mjs +84 -0
  87. package/lib/engine/tools/list-agents.mjs +32 -0
  88. package/lib/engine/tools/ls.mjs +127 -0
  89. package/lib/engine/tools/monitor.mjs +82 -0
  90. package/lib/engine/tools/notebook-edit.mjs +218 -0
  91. package/lib/engine/tools/read.mjs +103 -0
  92. package/lib/engine/tools/schedule-wakeup.mjs +45 -0
  93. package/lib/engine/tools/schema.mjs +144 -0
  94. package/lib/engine/tools/send-message.mjs +77 -0
  95. package/lib/engine/tools/session.mjs +70 -0
  96. package/lib/engine/tools/todo.mjs +144 -0
  97. package/lib/engine/tools/toolsearch.mjs +217 -0
  98. package/lib/engine/tools/walk.mjs +193 -0
  99. package/lib/engine/tools/web-switch.mjs +31 -0
  100. package/lib/engine/tools/webfetch-html.mjs +387 -0
  101. package/lib/engine/tools/webfetch-net.mjs +340 -0
  102. package/lib/engine/tools/webfetch.mjs +198 -0
  103. package/lib/engine/tools/websearch.mjs +91 -0
  104. package/lib/engine/tools/workflow.mjs +95 -0
  105. package/lib/engine/tools/write.mjs +76 -0
  106. package/lib/engine/tui/line-editor.mjs +137 -0
  107. package/lib/engine/tui/render.mjs +86 -0
  108. package/lib/engine/tui/tui.mjs +274 -0
  109. package/lib/engine/wire/anthropic-messages.mjs +263 -0
  110. package/lib/engine/wire/effort.mjs +36 -0
  111. package/lib/engine/wire/errors.mjs +496 -0
  112. package/lib/engine/wire/http.mjs +441 -0
  113. package/lib/engine/wire/index.mjs +76 -0
  114. package/lib/engine/wire/openai-chat.mjs +332 -0
  115. package/lib/engine/wire/prompt-cache.mjs +79 -0
  116. package/lib/engine/wire/search.mjs +140 -0
  117. package/lib/engine/wire/sse.mjs +114 -0
  118. package/lib/engine/wire/stall.mjs +349 -0
  119. package/lib/engine/wire/token-provider.mjs +175 -0
  120. package/lib/engine/wire/usage.mjs +192 -0
  121. package/lib/engine/workflow/host.mjs +524 -0
  122. package/lib/engine/workflow/journal.mjs +188 -0
  123. package/lib/engine/workflow/json-schema.mjs +171 -0
  124. package/lib/engine/workflow/meta.mjs +329 -0
  125. package/lib/engine/workflow/notifications.mjs +52 -0
  126. package/lib/engine/workflow/runtime.mjs +447 -0
  127. package/lib/engine/workflow/sandbox.mjs +534 -0
  128. package/lib/engine/workflow/worker.mjs +141 -0
  129. package/lib/engine/workflow/worktree.mjs +74 -0
  130. package/lib/execution/disposition.mjs +1 -1
  131. package/lib/execution/intake.mjs +10 -0
  132. package/lib/execution/surface-policy.mjs +15 -0
  133. package/lib/learning/curator.mjs +8 -6
  134. package/lib/learning/reflect.mjs +8 -6
  135. package/lib/model-router/catalog/cohort.yaml +137 -0
  136. package/lib/model-router/catalog.mjs +118 -1
  137. package/lib/model-router/failover.mjs +67 -16
  138. package/lib/model-router/llm-task.mjs +39 -3
  139. package/lib/model-router/resolve.mjs +89 -3
  140. package/lib/model-router/spawn.mjs +46 -47
  141. package/lib/model-router/taxonomy.mjs +126 -4
  142. package/lib/org/cost-sync.mjs +141 -11
  143. package/lib/org/inbound/broadcast.mjs +289 -0
  144. package/lib/org/inbound/collective.mjs +375 -0
  145. package/lib/org/inbound/directedness.mjs +96 -8
  146. package/lib/org/inbound/facts.mjs +78 -2
  147. package/lib/org/inbound/project.mjs +22 -0
  148. package/lib/org/inbound/surfaces.mjs +14 -0
  149. package/lib/org/llm-token.mjs +879 -0
  150. package/lib/org/mesh.mjs +61 -0
  151. package/lib/org/messaging.mjs +3 -1
  152. package/lib/org/protocol.checksum +1 -1
  153. package/lib/org/protocol.mjs +15 -0
  154. package/lib/org/quota.mjs +520 -0
  155. package/lib/org/tool-surface.mjs +104 -16
  156. package/lib/org/ui-parity.mjs +16 -1
  157. package/lib/org/work-ledger.mjs +37 -6
  158. package/lib/rate-guard.mjs +114 -1
  159. package/lib/resource-governor.mjs +41 -6
  160. package/lib/runtime/adapter.mjs +833 -0
  161. package/lib/runtime/child-env.mjs +191 -0
  162. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  163. package/lib/runtime/seat-engine.mjs +162 -0
  164. package/lib/session/ask-ledger.mjs +271 -0
  165. package/lib/session/current-work.mjs +676 -0
  166. package/lib/session/feed-core.mjs +40 -3
  167. package/lib/session/launch-args.mjs +56 -4
  168. package/lib/session/status-summary.mjs +26 -9
  169. package/lib/session/upgrade-notice.mjs +42 -0
  170. package/lib/setup/claude-probe.mjs +117 -13
  171. package/lib/setup/enrich.mjs +13 -10
  172. package/lib/setup/sections/model.mjs +39 -13
  173. package/lib/telemetry/collect.mjs +229 -11
  174. package/lib/upgrade/ignored-drift.mjs +105 -0
  175. package/lib/voice/post-call-brief.mjs +30 -17
  176. package/package.json +13 -3
  177. package/plugins/maestro-skills/skills/board-work.md +5 -0
  178. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  179. package/plugins/maestro-skills/skills/main-session.md +18 -7
  180. package/scaffold/config/collective.yaml +7 -0
  181. package/scripts/ci/check-durable-write-seam.mjs +3 -1
  182. package/scripts/ci/check-tarball-fidelity.mjs +126 -2
  183. package/scripts/ci/run-tests.mjs +47 -19
  184. package/scripts/cohort-llm/api-key-helper.mjs +92 -0
  185. package/scripts/collective/hook-runner.mjs +142 -19
  186. package/scripts/continuous-monitor.sh +13 -0
  187. package/scripts/cost/track-claude-usage.mjs +15 -0
  188. package/scripts/daemon/agent-daemon.mjs +408 -20
  189. package/scripts/daemon/assurance.mjs +48 -12
  190. package/scripts/daemon/cadence-consumer.mjs +218 -68
  191. package/scripts/daemon/cadence-handlers.mjs +73 -4
  192. package/scripts/daemon/classifier.mjs +75 -26
  193. package/scripts/daemon/context-compiler.mjs +51 -37
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +595 -149
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/maestro-daemon.mjs +11 -0
  198. package/scripts/daemon/prompt-builder.mjs +24 -0
  199. package/scripts/daemon/responder.mjs +246 -79
  200. package/scripts/daemon/sdk-version.mjs +98 -16
  201. package/scripts/eval/probe-gateway.mjs +635 -0
  202. package/scripts/eval/replay/extract.mjs +270 -0
  203. package/scripts/eval/replay/grade.mjs +260 -0
  204. package/scripts/eval/replay/lib/config.mjs +50 -0
  205. package/scripts/eval/replay/lib/effects.mjs +65 -0
  206. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  207. package/scripts/eval/replay/lib/judge.mjs +72 -0
  208. package/scripts/eval/replay/lib/redact.mjs +136 -0
  209. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  210. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  211. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  212. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  213. package/scripts/eval/replay/report.mjs +185 -0
  214. package/scripts/eval/replay/run.mjs +404 -0
  215. package/scripts/fleet/rollout.mjs +1151 -0
  216. package/scripts/hooks/pre-send-audit.sh +36 -245
  217. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  218. package/scripts/hooks/validate-state-yaml.sh +190 -0
  219. package/scripts/huddle/huddle-llm.mjs +361 -0
  220. package/scripts/huddle/huddle-server.mjs +46 -121
  221. package/scripts/local-triggers/autoupdate.sh +465 -81
  222. package/scripts/local-triggers/run-trigger.sh +13 -0
  223. package/scripts/maintenance/pin-integrity.mjs +364 -0
  224. package/scripts/poll-slack-events.sh +41 -9
  225. package/scripts/poller/slack-socket-mode.mjs +28 -3
  226. package/scripts/session/supervisor.mjs +80 -13
  227. package/scripts/spawn-session.sh +13 -0
  228. package/bin/maestro.test.mjs +0 -1574
  229. package/lib/action-executor.test.mjs +0 -871
  230. package/lib/archetype.test.mjs +0 -132
  231. package/lib/assurance/plan-note.test.mjs +0 -234
  232. package/lib/assurance/room-budget.test.mjs +0 -486
  233. package/lib/assurance/tier.test.mjs +0 -174
  234. package/lib/autonomy.test.mjs +0 -66
  235. package/lib/backlog.test.mjs +0 -302
  236. package/lib/backup/policy.test.mjs +0 -305
  237. package/lib/budget-escalate.test.mjs +0 -232
  238. package/lib/budget-guard.envelope.test.mjs +0 -476
  239. package/lib/budget-guard.test.mjs +0 -427
  240. package/lib/cadence-bus-requeue.test.mjs +0 -83
  241. package/lib/cadence-bus-schedule.test.mjs +0 -194
  242. package/lib/cadence-bus.test.mjs +0 -720
  243. package/lib/cadences.test.mjs +0 -230
  244. package/lib/capability/inventory.test.mjs +0 -232
  245. package/lib/capability.test.mjs +0 -78
  246. package/lib/channels/base-adapter.test.mjs +0 -590
  247. package/lib/channels/channels.test.mjs +0 -371
  248. package/lib/channels/contract.test.mjs +0 -162
  249. package/lib/channels/inbox-item.test.mjs +0 -368
  250. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  251. package/lib/channels/pairing.test.mjs +0 -270
  252. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  253. package/lib/channels/slack-adapter.test.mjs +0 -212
  254. package/lib/channels/telegram-adapter.test.mjs +0 -306
  255. package/lib/channels/voice/adapter.test.mjs +0 -278
  256. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  257. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  258. package/lib/charter.test.mjs +0 -89
  259. package/lib/claude-bin.test.mjs +0 -131
  260. package/lib/cli/board.test.mjs +0 -227
  261. package/lib/cli/design.test.mjs +0 -270
  262. package/lib/cli/doctor-checks.test.mjs +0 -336
  263. package/lib/cli/global-setup-extras.test.mjs +0 -462
  264. package/lib/cli/inbox.test.mjs +0 -230
  265. package/lib/cli/session-ack.test.mjs +0 -63
  266. package/lib/cli/session.test.mjs +0 -613
  267. package/lib/collective/capture.test.mjs +0 -121
  268. package/lib/collective/cards.test.mjs +0 -114
  269. package/lib/collective/config.test.mjs +0 -123
  270. package/lib/collective/global-config.test.mjs +0 -220
  271. package/lib/collective/global-skills.test.mjs +0 -126
  272. package/lib/collective/presence.test.mjs +0 -95
  273. package/lib/collective/recall.test.mjs +0 -116
  274. package/lib/collective/vendor-skills.test.mjs +0 -306
  275. package/lib/comms/send-gate.test.mjs +0 -770
  276. package/lib/comms.test.mjs +0 -41
  277. package/lib/context/budget.test.mjs +0 -252
  278. package/lib/context/history-scope.test.mjs +0 -79
  279. package/lib/cost/ledger-row.test.mjs +0 -183
  280. package/lib/design/design-md.test.mjs +0 -318
  281. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  282. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  283. package/lib/design/fixtures/foundation.json +0 -133
  284. package/lib/design/refresh-gate.test.mjs +0 -144
  285. package/lib/design/write.test.mjs +0 -241
  286. package/lib/diagnostics/alerts.test.mjs +0 -318
  287. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  288. package/lib/diagnostics/counters.test.mjs +0 -206
  289. package/lib/diagnostics/events.test.mjs +0 -290
  290. package/lib/diagnostics/otel.test.mjs +0 -196
  291. package/lib/diagnostics/trace.test.mjs +0 -251
  292. package/lib/env-compat.test.mjs +0 -104
  293. package/lib/execution/disposition.test.mjs +0 -553
  294. package/lib/execution/drive.test.mjs +0 -270
  295. package/lib/execution/effects.test.mjs +0 -344
  296. package/lib/execution/intake.test.mjs +0 -389
  297. package/lib/execution/journal.test.mjs +0 -261
  298. package/lib/execution/match.test.mjs +0 -235
  299. package/lib/execution/pipeline.test.mjs +0 -392
  300. package/lib/execution/route.test.mjs +0 -186
  301. package/lib/execution/surface-policy.test.mjs +0 -162
  302. package/lib/fs-atomic.test.mjs +0 -72
  303. package/lib/fs-ownership.test.mjs +0 -158
  304. package/lib/goals/admission.test.mjs +0 -164
  305. package/lib/goals/classify.test.mjs +0 -167
  306. package/lib/goals/collaborate.test.mjs +0 -336
  307. package/lib/goals/gaps.test.mjs +0 -284
  308. package/lib/goals/loop.test.mjs +0 -845
  309. package/lib/hooks/bus.test.mjs +0 -387
  310. package/lib/identity/persona.test.mjs +0 -142
  311. package/lib/kpi-sensors.test.mjs +0 -278
  312. package/lib/kpi.test.mjs +0 -244
  313. package/lib/learning/config.test.mjs +0 -75
  314. package/lib/learning/counters.test.mjs +0 -69
  315. package/lib/learning/curator-consolidate.test.mjs +0 -238
  316. package/lib/learning/curator.test.mjs +0 -106
  317. package/lib/learning/reflect.test.mjs +0 -0
  318. package/lib/learning/session-index.test.mjs +0 -125
  319. package/lib/learning/skill-writer.test.mjs +0 -210
  320. package/lib/mandate/audit.test.mjs +0 -195
  321. package/lib/mandate/contract.test.mjs +0 -185
  322. package/lib/mandate/derive.test.mjs +0 -274
  323. package/lib/mandate/model.test.mjs +0 -164
  324. package/lib/mandate/refresh.test.mjs +0 -389
  325. package/lib/mcp/server.test.mjs +0 -426
  326. package/lib/model-router/auth-profiles.test.mjs +0 -580
  327. package/lib/model-router/catalog.test.mjs +0 -385
  328. package/lib/model-router/economics.test.mjs +0 -438
  329. package/lib/model-router/failover.test.mjs +0 -439
  330. package/lib/model-router/health.test.mjs +0 -338
  331. package/lib/model-router/integration-coverage.test.mjs +0 -831
  332. package/lib/model-router/integration.test.mjs +0 -564
  333. package/lib/model-router/ledger.test.mjs +0 -415
  334. package/lib/model-router/llm-task.test.mjs +0 -392
  335. package/lib/model-router/org-credentials.test.mjs +0 -265
  336. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  337. package/lib/model-router/reconcile.test.mjs +0 -316
  338. package/lib/model-router/repair.test.mjs +0 -180
  339. package/lib/model-router/spawn.test.mjs +0 -446
  340. package/lib/model-router/taxonomy.test.mjs +0 -410
  341. package/lib/model-router.test.mjs +0 -1207
  342. package/lib/org/activity.test.mjs +0 -134
  343. package/lib/org/approvals.test.mjs +0 -216
  344. package/lib/org/awareness.test.mjs +0 -159
  345. package/lib/org/board-mine-cache.test.mjs +0 -53
  346. package/lib/org/board.test.mjs +0 -187
  347. package/lib/org/bootstrap-context.test.mjs +0 -153
  348. package/lib/org/client.test.mjs +0 -1206
  349. package/lib/org/cohort-client.test.mjs +0 -126
  350. package/lib/org/cost-sync.test.mjs +0 -153
  351. package/lib/org/doctor.test.mjs +0 -346
  352. package/lib/org/engagement-ledger.test.mjs +0 -112
  353. package/lib/org/engagement.test.mjs +0 -739
  354. package/lib/org/handoff.test.mjs +0 -269
  355. package/lib/org/inbound/directedness.test.mjs +0 -668
  356. package/lib/org/inbound/facts.test.mjs +0 -471
  357. package/lib/org/inbound/hydrate.test.mjs +0 -908
  358. package/lib/org/inbound/index.test.mjs +0 -429
  359. package/lib/org/inbound/project.test.mjs +0 -287
  360. package/lib/org/integration-tools.test.mjs +0 -160
  361. package/lib/org/keys.test.mjs +0 -92
  362. package/lib/org/knowledge.test.mjs +0 -326
  363. package/lib/org/leases.test.mjs +0 -235
  364. package/lib/org/mesh-directives.test.mjs +0 -110
  365. package/lib/org/mesh-integration.test.mjs +0 -127
  366. package/lib/org/mesh.test.mjs +0 -400
  367. package/lib/org/messaging.test.mjs +0 -471
  368. package/lib/org/param-contract.test.mjs +0 -477
  369. package/lib/org/policy.test.mjs +0 -237
  370. package/lib/org/protocol.checksum.test.mjs +0 -90
  371. package/lib/org/protocol.test.mjs +0 -323
  372. package/lib/org/push.test.mjs +0 -792
  373. package/lib/org/registry.test.mjs +0 -100
  374. package/lib/org/resource-tools.test.mjs +0 -361
  375. package/lib/org/tool-access.test.mjs +0 -144
  376. package/lib/org/tool-surface-integration.test.mjs +0 -120
  377. package/lib/org/tool-surface.test.mjs +0 -1268
  378. package/lib/org/typing.test.mjs +0 -291
  379. package/lib/org/ui-parity.test.mjs +0 -560
  380. package/lib/org/verify.test.mjs +0 -194
  381. package/lib/org/work-ledger.test.mjs +0 -273
  382. package/lib/plan/adoption-e2e.test.mjs +0 -366
  383. package/lib/plan/budget-enforcement.test.mjs +0 -400
  384. package/lib/plan/compile.test.mjs +0 -382
  385. package/lib/plan/emit.test.mjs +0 -269
  386. package/lib/plan/explain.test.mjs +0 -188
  387. package/lib/prompts/parallelism.test.mjs +0 -177
  388. package/lib/rag/rag.test.mjs +0 -505
  389. package/lib/rate-guard.test.mjs +0 -272
  390. package/lib/reactive-gate.test.mjs +0 -57
  391. package/lib/render.test.mjs +0 -68
  392. package/lib/resource-governor.test.mjs +0 -488
  393. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  394. package/lib/scheduling/jitter.test.mjs +0 -140
  395. package/lib/secrets/broker.test.mjs +0 -280
  396. package/lib/secrets/providers.test.mjs +0 -274
  397. package/lib/security/audit-engine.test.mjs +0 -424
  398. package/lib/security/coerce-args.test.mjs +0 -281
  399. package/lib/security/dangerous-tools.test.mjs +0 -68
  400. package/lib/security/external-content.test.mjs +0 -84
  401. package/lib/security/redact.test.mjs +0 -441
  402. package/lib/security/secret-equal.test.mjs +0 -55
  403. package/lib/session/config.test.mjs +0 -92
  404. package/lib/session/feed-core.test.mjs +0 -198
  405. package/lib/session/first-run.test.mjs +0 -121
  406. package/lib/session/frontdoor.test.mjs +0 -205
  407. package/lib/session/handoffs.test.mjs +0 -183
  408. package/lib/session/identity.test.mjs +0 -180
  409. package/lib/session/inbox-claims.test.mjs +0 -286
  410. package/lib/session/launch-args.test.mjs +0 -157
  411. package/lib/session/liveness.test.mjs +0 -100
  412. package/lib/session/status-summary.test.mjs +0 -118
  413. package/lib/session-permissions.test.mjs +0 -120
  414. package/lib/setup/claude-probe.test.mjs +0 -187
  415. package/lib/setup/completeness.test.mjs +0 -110
  416. package/lib/setup/context-pack.test.mjs +0 -89
  417. package/lib/setup/enrich.test.mjs +0 -115
  418. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  419. package/lib/setup/integration.test.mjs +0 -162
  420. package/lib/setup/io.test.mjs +0 -77
  421. package/lib/setup/runner.test.mjs +0 -132
  422. package/lib/setup/sections/identity.test.mjs +0 -234
  423. package/lib/setup/sections/inventory.test.mjs +0 -198
  424. package/lib/setup/sections/learning.test.mjs +0 -81
  425. package/lib/setup/sections/mandate.test.mjs +0 -388
  426. package/lib/setup/sections/messaging.test.mjs +0 -127
  427. package/lib/setup/sections/model.test.mjs +0 -240
  428. package/lib/setup/sections/org.test.mjs +0 -346
  429. package/lib/setup/sections/orgmail.test.mjs +0 -118
  430. package/lib/setup/sections/recovery.test.mjs +0 -98
  431. package/lib/setup/sections/subagents.test.mjs +0 -429
  432. package/lib/setup/sections/verify.test.mjs +0 -175
  433. package/lib/setup/sot.test.mjs +0 -81
  434. package/lib/setup/state.test.mjs +0 -115
  435. package/lib/singleton.test.mjs +0 -151
  436. package/lib/subagents/cli.test.mjs +0 -389
  437. package/lib/subagents/client.test.mjs +0 -309
  438. package/lib/subagents/gap.test.mjs +0 -234
  439. package/lib/subagents/lock.test.mjs +0 -248
  440. package/lib/subagents/manifest.test.mjs +0 -175
  441. package/lib/subagents/refs.test.mjs +0 -204
  442. package/lib/subagents/resolve.test.mjs +0 -422
  443. package/lib/subagents/schema.test.mjs +0 -328
  444. package/lib/telemetry/alerts.test.mjs +0 -109
  445. package/lib/telemetry/collect.test.mjs +0 -1274
  446. package/lib/tool-definitions-integration.test.mjs +0 -83
  447. package/lib/tool-definitions.test.mjs +0 -437
  448. package/lib/upgrade/global-refresh.test.mjs +0 -65
  449. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  450. package/lib/upgrade/post-steps.test.mjs +0 -200
  451. package/lib/upgrade/verify.test.mjs +0 -164
  452. package/lib/util/fetch-timeout.test.mjs +0 -202
  453. package/lib/util/reconnect.test.mjs +0 -369
  454. package/lib/util/unhandled.test.mjs +0 -216
  455. package/lib/voice/outbound.test.mjs +0 -69
  456. package/lib/voice/session-rotation.test.mjs +0 -114
  457. package/lib/voice/stt.test.mjs +0 -226
  458. package/lib/voice/voice.test.mjs +0 -990
  459. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  460. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  461. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  462. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  463. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  464. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  465. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  466. package/scripts/ci/check.test.mjs +0 -194
  467. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  468. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  469. package/scripts/collective/hook-runner.test.mjs +0 -173
  470. package/scripts/cost/fleet-digest.test.mjs +0 -207
  471. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  472. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  473. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  474. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  475. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  476. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  477. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  478. package/scripts/daemon/assurance.test.mjs +0 -1791
  479. package/scripts/daemon/board-mirror.test.mjs +0 -165
  480. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  481. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  482. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  483. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  484. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  485. package/scripts/daemon/classifier.test.mjs +0 -266
  486. package/scripts/daemon/classify-kind.test.mjs +0 -40
  487. package/scripts/daemon/context-compiler.test.mjs +0 -406
  488. package/scripts/daemon/deliver.test.mjs +0 -564
  489. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  490. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  491. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  492. package/scripts/daemon/dispatcher-session-continuity.test.mjs +0 -365
  493. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  494. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  495. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  496. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  497. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  498. package/scripts/daemon/integration.test.mjs +0 -149
  499. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  500. package/scripts/daemon/lib/session-router.test.mjs +0 -554
  501. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  502. package/scripts/daemon/prompt-builder.test.mjs +0 -556
  503. package/scripts/daemon/responder-cost.test.mjs +0 -68
  504. package/scripts/daemon/responder-history.test.mjs +0 -221
  505. package/scripts/daemon/sdk-version.test.mjs +0 -31
  506. package/scripts/daemon/session-lock.test.mjs +0 -252
  507. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  508. package/scripts/daemon/typing-registry.test.mjs +0 -102
  509. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  510. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  511. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  512. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  513. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  514. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  515. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  516. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  517. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  518. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  519. package/scripts/poller-launchd/install.test.mjs +0 -243
  520. package/scripts/restore-from-backup.test.mjs +0 -181
  521. package/scripts/session/feed.test.mjs +0 -196
  522. package/scripts/session/supervisor-sh.test.mjs +0 -218
  523. package/scripts/session/supervisor.test.mjs +0 -482
  524. package/scripts/setup/configure-macos.test.mjs +0 -306
  525. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  526. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  527. package/scripts/setup/generate-capability.test.mjs +0 -134
  528. package/scripts/setup/init-agent.test.mjs +0 -370
  529. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  530. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  531. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -0,0 +1,289 @@
1
+ /**
2
+ * lib/org/inbound/broadcast.mjs — "did a human just address EVERYONE?"
3
+ *
4
+ * PURE. No IO, no clock, no config. The one place the collective-address
5
+ * vocabulary lives on this side of the org.
6
+ *
7
+ * ── WHY THIS EXISTS ──
8
+ * On 2026-09-21 a human posted in #general: "Can everyone give me an update on
9
+ * what they're doing right now (across sessions on your machines)?". Thirteen
10
+ * agents were members. Zero replied. Not a regression — a standing gap: the
11
+ * addressing layer knew DMs, @mentions, threads and prose names, and had NO
12
+ * concept of a message addressed to the whole room. A space post with
13
+ * `mentionCount: 0` was dropped structurally (`no_mentions_not_threaded`)
14
+ * before its body was ever read, on every seat, including the ones that were
15
+ * up and healthy.
16
+ *
17
+ * ── THE RULE (implemented by `resolveMessaging` in directedness.mjs) ──
18
+ * A space post is a BROADCAST when ALL of:
19
+ * 1. the author is HUMAN — agents never broadcast to each other; a peer's
20
+ * "everyone please review" is ambient traffic, not a roll-call;
21
+ * 2. it is in a public/private SPACE this seat is a member of — never a DM
22
+ * (a DM is already addressed by construction and stays a DM even when it
23
+ * says "everyone");
24
+ * 3. it is not threaded — a thread reply keeps the existing thread rule;
25
+ * 4. the body matches one of {@link BROADCAST_PATTERNS}.
26
+ *
27
+ * ── THE TWIN ──
28
+ * hq carries a faithful port of this module at
29
+ * `src/server/llm-responder/broadcast.ts` for agents WITHOUT a machine (the
30
+ * LLM_RESPONDER path). The two are separate packages and cannot import each
31
+ * other, so BOTH pin the same {@link BROADCAST_CORPUS} in their tests. Change
32
+ * a pattern here and the hq twin's test goes red until it is changed there
33
+ * too — that is the drift guard, keep the corpus identical on both sides.
34
+ *
35
+ * @module lib/org/inbound/broadcast
36
+ */
37
+
38
+ "use strict";
39
+
40
+ /**
41
+ * The explicit collective addresses, in match order. Case-insensitive, anchored
42
+ * to word boundaries, so "all" alone, "you" alone, "you all right?" and
43
+ * "thanks all" do NOT match — only a phrase that unambiguously names the whole
44
+ * room does. `team,` is only a broadcast when it OPENS the message ("Team, quick
45
+ * check-in") — mid-sentence "the team" is a noun, not an address.
46
+ *
47
+ * Ordered most-common-first; order does not change the verdict, only which
48
+ * pattern is reported as the match.
49
+ *
50
+ * @type {readonly RegExp[]}
51
+ */
52
+ export const BROADCAST_PATTERNS = Object.freeze([
53
+ /\beveryone\b/i,
54
+ /\beverybody\b/i,
55
+ /\ball of you\b/i,
56
+ /\beach of you\b/i,
57
+ /\bthe whole team\b/i,
58
+ /^\s*team\s*,/i,
59
+ /(^|[^\w@])@here\b/i,
60
+ /(^|[^\w@])@channel\b/i,
61
+ /(^|[^\w@])@all\b/i,
62
+ /\ball agents\b/i,
63
+ /\ball colleagues\b/i,
64
+ /\broll[ -]?call\b/i,
65
+ /\bsound off\b/i,
66
+ /\bstatus check\b/i,
67
+ ]);
68
+
69
+ /**
70
+ * Is this message body an explicit address to the whole room?
71
+ *
72
+ * Pure text test — the author/room/thread conditions are the caller's
73
+ * (`resolveMessaging`); this answers ONLY condition 4.
74
+ *
75
+ * @param {unknown} body
76
+ * @returns {boolean}
77
+ */
78
+ export function isBroadcast(body) {
79
+ const text = typeof body === "string" ? body : "";
80
+ if (!text.trim()) return false;
81
+ return BROADCAST_PATTERNS.some((re) => re.test(text));
82
+ }
83
+
84
+ /**
85
+ * Which pattern matched, as a stable label for a log line / a test. Null when
86
+ * none did.
87
+ *
88
+ * @param {unknown} body
89
+ * @returns {string|null}
90
+ */
91
+ export function broadcastMatch(body) {
92
+ const text = typeof body === "string" ? body : "";
93
+ if (!text.trim()) return null;
94
+ for (const re of BROADCAST_PATTERNS) {
95
+ const m = re.exec(text);
96
+ if (m) return m[0].trim().toLowerCase();
97
+ }
98
+ return null;
99
+ }
100
+
101
+ /**
102
+ * THE SHARED CORPUS. Literal strings, pinned by directedness.test.mjs here and
103
+ * by hq's broadcast.test.ts. `positive` must all be broadcasts; `negative` must
104
+ * all NOT be. The first positive is the actual message that went unanswered.
105
+ */
106
+ export const BROADCAST_CORPUS = Object.freeze({
107
+ positive: Object.freeze([
108
+ "Can everyone give me an update on what they're doing right now (across sessions on your machines)?",
109
+ "did everyone see the deck?",
110
+ "Everybody: standup in 5",
111
+ "Quick one for all of you — what's blocking you today?",
112
+ "I'd like each of you to post your top priority",
113
+ "Thanks to the whole team for the push last night, what's next on your plates?",
114
+ "Team, quick check-in: what are you on?",
115
+ "@here who is around?",
116
+ "@channel status please",
117
+ "@all please confirm you can see this",
118
+ "all agents: report your current task",
119
+ "All colleagues, a quick update from your side please",
120
+ "roll call",
121
+ "Roll-call — sound off with what you're working on",
122
+ "status check: where is everyone at?",
123
+ ]),
124
+ negative: Object.freeze([
125
+ "all good here",
126
+ "thanks all",
127
+ "I'll take all three",
128
+ "you all right?",
129
+ "pushed the fix, ready for review",
130
+ "can you look at the deck?",
131
+ "the team offsite is on Thursday",
132
+ "email me at ops@all-hands.example",
133
+ "what is the status of the migration?",
134
+ "",
135
+ ]),
136
+ });
137
+
138
+ // ---------------------------------------------------------------------------
139
+ // Reply shaping — what a roll-call respondent is told
140
+ // ---------------------------------------------------------------------------
141
+
142
+ /** The MessageEvent / inbox `kind` and the verdict `reason` a broadcast carries. */
143
+ export const BROADCAST_KIND = "broadcast";
144
+ export const BROADCAST_REASON = "collective";
145
+
146
+ /**
147
+ * Is this flattened inbox item a roll-call delivery? Reads the two fields the
148
+ * projection carries across `eventToInboxItem` (`kind`, `direct_reason`), so a
149
+ * daemon can gate on it without re-parsing prose.
150
+ *
151
+ * @param {object} item
152
+ * @returns {boolean}
153
+ */
154
+ export function isRollCallItem(item) {
155
+ if (!item || typeof item !== "object") return false;
156
+ if (String(item.kind || "") === BROADCAST_KIND) return true;
157
+ if (String(item.direct_reason || "") === BROADCAST_REASON) return true;
158
+ const cohort = item.cohort && typeof item.cohort === "object" ? item.cohort : null;
159
+ return !!cohort && String(cohort.surface || "") === BROADCAST_KIND;
160
+ }
161
+
162
+ /** Hard ceiling on a roll-call reply, in lines. The prompt says it; the fleet keeps it. */
163
+ export const ROLL_CALL_MAX_LINES = 5;
164
+
165
+ /**
166
+ * The reply-shaping block injected into whichever prompt composes the answer —
167
+ * the daemon's quick-reply responder or a spawned session. One text, both
168
+ * paths, so a collective answer reads the same however it was produced. The
169
+ * FACTS the answer is composed from are the current-work block
170
+ * (lib/session/current-work), which both paths already carry.
171
+ *
172
+ * ONE TEXT, THREE INTENTS (`collective.mjs`). The discipline is shared —
173
+ * answer for yourself, do not speak for anyone else, do not @mention, do not
174
+ * acknowledge — and exactly one line differs, because exactly one thing
175
+ * differs:
176
+ *
177
+ * roll-call → report on yourself: what you are doing, queued, blocked.
178
+ * work → say what YOUR slice is and by when. The ask is already a row
179
+ * on the board (one per addressee), so the reply is a plan, not
180
+ * a status line — and telling the session the row exists is what
181
+ * stops it writing "I'll open a task for this".
182
+ * ambient → the room was addressed and nothing was asked. Answer in your
183
+ * own lane and do NOT mint work from it, which is the prompt-side
184
+ * half of the same guard `HEDGE_PATTERNS` enforces in code.
185
+ *
186
+ * Word-for-word hq's `src/server/llm-responder/broadcast.ts`, so a seat
187
+ * answered by its own daemon and a seat answered by hq read the same in the
188
+ * room.
189
+ *
190
+ * @param {{intent?: string, respondents?: number|null}} [o] `respondents` — how
191
+ * many AI members the org has (an upper bound on who else answers; the room
192
+ * roster is not visible to a seat). Omitted or non-finite ⇒ "several".
193
+ * @returns {string}
194
+ */
195
+ export function collectiveInstructions(o = {}) {
196
+ const n = Number.isFinite(o.respondents) && o.respondents > 0 ? o.respondents : null;
197
+ const who = n ? `one of up to ${n} colleagues` : "one of several colleagues";
198
+ const intent = o.intent || "roll-call";
199
+ const banner = intent === "roll-call" ? "ROLL-CALL" : "ADDRESSED TO EVERYONE";
200
+ const framing =
201
+ intent === "work"
202
+ ? `A human asked the whole room to do something, not you specifically. You are ${who} who will answer the same message. Every addressed member owns its own slice; nobody answers for the group.`
203
+ : `A human addressed the whole room, not you specifically. You are ${who} who will answer the same message. Every addressed member answers for itself; nobody answers for the group.`;
204
+ const content =
205
+ intent === "work"
206
+ ? `- This ask is already on the board as YOUR OWN item, so do not open another one. Reply in at most ${ROLL_CALL_MAX_LINES} short lines, about your slice only: what you will do about it, and by when. Facts, not adjectives.`
207
+ : intent === "ambient"
208
+ ? `- Nothing was asked of you here, so do not turn it into a task. Reply in at most ${ROLL_CALL_MAX_LINES} short lines, from your own lane only. Facts, not adjectives.`
209
+ : `- Reply in at most ${ROLL_CALL_MAX_LINES} short lines, about YOURSELF only: what you are doing right now, what is queued, what is blocked. Facts, not adjectives.`;
210
+ return [
211
+ `===== ${banner} =====`,
212
+ framing,
213
+ content,
214
+ "- Do NOT summarise, list, guess at or speak for anyone else's status.",
215
+ "- Do NOT @mention anyone. Do not address other agents. Do not ask the room a question back.",
216
+ "- Do NOT post an acknowledgement, a holding line or a 'checking now' — say nothing until the answer itself is ready, then post it once.",
217
+ "- Do not restate the question and do not open with a greeting; the first line is already the answer.",
218
+ `===== END ${banner} =====`,
219
+ ].join("\n");
220
+ }
221
+
222
+ /**
223
+ * The roll-call arm of {@link collectiveInstructions}, kept as its own export
224
+ * because it is what every existing call site and test names. Byte-identical to
225
+ * the text this module has always produced.
226
+ *
227
+ * @param {{respondents?: number|null}} [o]
228
+ * @returns {string}
229
+ */
230
+ export function rollCallInstructions(o = {}) {
231
+ return collectiveInstructions({ ...o, intent: "roll-call" });
232
+ }
233
+
234
+ // WHAT THE SEAT IS DOING is not rendered here. This module briefly carried a
235
+ // `renderSelfActivity` (status by session name, peers by name, handoffs,
236
+ // every board row, the daemon's active sessions) that the quick reply injected
237
+ // NEXT TO lib/session/current-work's block — two accounts of one state, and
238
+ // the one here quoted session names. The single renderer is
239
+ // lib/session/current-work (`currentWorkBlock`, audience- and barrier-scoped);
240
+ // what this one carried that it lacked moved there. Do not add a second.
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Rate shaping — 13 replies must not land in the same second
244
+ // ---------------------------------------------------------------------------
245
+
246
+ /** The jitter window a roll-call reply is spread across, per channel. */
247
+ export const ROLL_CALL_JITTER_MAX_MS = 20_000;
248
+
249
+ /** FNV-1a 32-bit over a string. Deterministic, dependency-free. */
250
+ function fnv1a(s) {
251
+ let h = 0x811c9dc5;
252
+ for (let i = 0; i < s.length; i += 1) {
253
+ h ^= s.charCodeAt(i);
254
+ h = Math.imul(h, 0x01000193) >>> 0;
255
+ }
256
+ return h >>> 0;
257
+ }
258
+
259
+ /**
260
+ * How long THIS seat waits before posting its roll-call answer. Deterministic
261
+ * per (seat, channel, message): the same seat answering the same roll-call
262
+ * after a crash waits the same amount, and thirteen seats spread across the
263
+ * window instead of stacking on the second the event landed. Pure — no
264
+ * randomness — so a test can pin it.
265
+ *
266
+ * @param {{seat?:string, channelId?:string, messageId?:string, maxMs?:number}} o
267
+ * @returns {number} milliseconds in [0, maxMs)
268
+ */
269
+ export function rollCallJitterMs(o = {}) {
270
+ const max = Number.isFinite(o.maxMs) && o.maxMs >= 0 ? o.maxMs : ROLL_CALL_JITTER_MAX_MS;
271
+ if (max === 0) return 0;
272
+ const key = `${o.seat || ""}|${o.channelId || ""}|${o.messageId || ""}`;
273
+ return fnv1a(key) % max;
274
+ }
275
+
276
+ export default {
277
+ BROADCAST_PATTERNS,
278
+ BROADCAST_CORPUS,
279
+ BROADCAST_KIND,
280
+ BROADCAST_REASON,
281
+ ROLL_CALL_MAX_LINES,
282
+ ROLL_CALL_JITTER_MAX_MS,
283
+ isBroadcast,
284
+ broadcastMatch,
285
+ isRollCallItem,
286
+ collectiveInstructions,
287
+ rollCallInstructions,
288
+ rollCallJitterMs,
289
+ };
@@ -0,0 +1,375 @@
1
+ /**
2
+ * lib/org/inbound/collective.mjs — a message addressed to EVERYONE: is it work?
3
+ *
4
+ * PURE. No IO, no clock, no config. The twin of hq's
5
+ * `src/server/work/collective.ts`.
6
+ *
7
+ * ── THE QUESTION THIS ANSWERS, AND THE ONE IT DOES NOT ──
8
+ * `broadcast.mjs#isBroadcast` answers ADDRESSING: did a human name the whole
9
+ * room? This module answers what comes next, and only that: GIVEN that the
10
+ * room was addressed collectively, is the message a ROLL-CALL (report on
11
+ * yourself), WORK (go and do a thing), or AMBIENT (neither)?
12
+ *
13
+ * It deliberately does NOT re-test the address — `directedness.mjs#
14
+ * isBroadcastFor` already settled that, and asking twice is how two places
15
+ * come to disagree about one question.
16
+ *
17
+ * ── WHY IT EXISTS ──
18
+ * 2026-09-21 fixed the first half: "Can everyone give me an update?" drew zero
19
+ * replies from thirteen members. The half it did not fix: "Everyone, please
20
+ * review the binder by Friday" drew replies and NO BOARD ROWS, on both planes.
21
+ * Work addressed to everyone did no work.
22
+ *
23
+ * ── THE RULE, IN THE CODE THAT IMPLEMENTS IT ──
24
+ *
25
+ * 1. ROLL-CALL AND WORK ARE NOT MUTUALLY EXCLUSIVE TEXT, so they are not
26
+ * tested in sequence. "Everyone, please review the binder and give me an
27
+ * update when it's done" is BOTH shapes in one sentence, and a first-match
28
+ * ladder gets it wrong whichever order it is written in. Instead the
29
+ * roll-call phrases are EXCISED from the body and the work gate runs on
30
+ * what is left: if the residue still asks for work, it is work; if the work
31
+ * reading only ever came from the status phrasing, it is a roll-call. That
32
+ * is also what keeps "can everyone give me an UPDATE" from being work —
33
+ * `update` is an action verb, and without the excision the plainest
34
+ * roll-call in the corpus would open thirteen board rows.
35
+ *
36
+ * 2. A HEDGE DEFUSES WORK. "everyone should take a look sometime", "feel free
37
+ * to review when you get a chance", "no rush" — a board that mints thirteen
38
+ * rows for each of those is a board the owner stops opening within a week.
39
+ * A hedge is read BEFORE the verb, so no amount of imperative grammar beats
40
+ * "if anyone has a minute".
41
+ *
42
+ * 3. THE WORK TEST IS hq's. `asksForWork` / `hasSubstance` below are a VERBATIM
43
+ * port of `src/server/work/threshold.ts` — not a second opinion about what
44
+ * work looks like. A collective ask and a direct ask are the same ask with a
45
+ * different audience.
46
+ *
47
+ * ── WHAT THIS FILE MAY AND MAY NOT DECIDE ──
48
+ * It shapes a PROMPT and routes a FAST PATH. It NEVER decides whether a board
49
+ * row exists. That judgement stays single-implementation on the server:
50
+ * `work-ledger.mjs` sends `collective: true` plus the body over `board.track`,
51
+ * and hq's `assessAskWithAudience` runs its own copy of this classifier. A
52
+ * daemon that got the verdict wrong here therefore loses a nicely-shaped
53
+ * prompt, never a row it should not have opened.
54
+ *
55
+ * ── THE TWIN ──
56
+ * Both sides pin {@link COLLECTIVE_CORPUS}, byte-for-byte. Change a pattern
57
+ * here and hq's `collective.test.ts` goes red until it is changed there too —
58
+ * the same drift guard BROADCAST_CORPUS already runs for the address.
59
+ *
60
+ * @module lib/org/inbound/collective
61
+ */
62
+
63
+ "use strict";
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // VERBATIM PORT of hq src/server/work/threshold.ts. Do not "improve" either
67
+ // copy alone: the corpus below is what holds them together, and the board gate
68
+ // that matters runs hq's copy.
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /** Shortest ask we will treat as an ask at all. */
72
+ const MIN_SUBSTANCE_CHARS = 12;
73
+
74
+ /** A chase or a pleasantry — a reaction to work, never a request for it. */
75
+ const NO_SUBSTANCE =
76
+ /^(?:[\s?!.…]*|\?+|pls|please|ping|bump|any\s+updates?|updates?|status|well\?|and\?|thanks?|thank\s+you|ty|thx|cheers|ok(?:ay)?|kk|got\s+it|sure|nice|great|cool|perfect|lovely|yes|yep|no|nope|nvm|never\s+mind|👍|🙏|🎉)[\s?!.…]*$/i;
77
+
78
+ /** An explicit "do not action this" prefix, honoured before anything else. */
79
+ const NO_ACTION_PREFIX = /^\s*(?:fyi\b|no\s+action\b|just\s+so\s+you\s+know\b)/i;
80
+
81
+ /**
82
+ * Verbs that name WORK. Kept to things that produce an artifact or change the
83
+ * world — deliberately NOT "explain", "tell", "what", "why", which are answered
84
+ * in the turn and leave the reply as their own artifact.
85
+ */
86
+ const ACTION_VERB =
87
+ "(?:fix|ship|build|write|draft|send|create|make|set\\s+up|update|upgrade|migrate|deploy|" +
88
+ "investigate|look\\s+into|dig\\s+into|sort\\s+out|figure\\s+out|chase|review|prepare|prep|" +
89
+ "schedule|book|publish|add|remove|delete|refactor|debug|push|merge|implement|plan|research|" +
90
+ "analyse|analyze|compile|produce|generate|organise|organize|follow\\s+up|resolve|land|" +
91
+ "roll\\s+out|audit|clean\\s+up|tidy|rename|document|write\\s+up|pull\\s+together|put\\s+together|" +
92
+ "sort|handle|action|own|drive|unblock|test|verify|check\\s+into|reconcile)";
93
+
94
+ /** An imperative: the verb opens a sentence ("fix these issues and push them"). */
95
+ const IMPERATIVE = new RegExp(
96
+ `(?:^|[.!?;,\\n]\\s*|\\b(?:and|then|also|please|pls|first|next|now)\\s+)` +
97
+ `(?:(?:also|then|now|and|please|pls|first|next)\\s+){0,2}${ACTION_VERB}\\b`,
98
+ "i",
99
+ );
100
+
101
+ /** A polite request wrapper ("could you look into…", "I need you to draft…"). */
102
+ const REQUEST = new RegExp(
103
+ `\\b(?:can|could|would|will|please|pls|need\\s+(?:you\\s+)?to|needs?\\s+to|want\\s+you\\s+to|` +
104
+ `like\\s+you\\s+to|should\\s+you|mind)\\b[^.!?\\n]{0,40}?\\b${ACTION_VERB}\\b`,
105
+ "i",
106
+ );
107
+
108
+ /**
109
+ * Does this text ask the agent to DO something (as opposed to answer something)?
110
+ * @param {unknown} body
111
+ * @returns {boolean}
112
+ */
113
+ export function asksForWork(body) {
114
+ const text = String(body == null ? "" : body).trim();
115
+ if (!text) return false;
116
+ if (NO_ACTION_PREFIX.test(text)) return false;
117
+ return IMPERATIVE.test(text) || REQUEST.test(text);
118
+ }
119
+
120
+ /**
121
+ * Is there enough here to be an ask at all?
122
+ * @param {unknown} body
123
+ * @returns {boolean}
124
+ */
125
+ export function hasSubstance(body) {
126
+ const text = String(body == null ? "" : body).trim();
127
+ if (!text) return false;
128
+ if (NO_SUBSTANCE.test(text)) return false;
129
+ return text.length >= MIN_SUBSTANCE_CHARS;
130
+ }
131
+
132
+ // ---------------------------------------------------------------------------
133
+ // The collective classifier
134
+ // ---------------------------------------------------------------------------
135
+
136
+ /**
137
+ * @typedef {"roll-call"|"work"|"ambient"} CollectiveIntent
138
+ */
139
+
140
+ /** The three intents, in the order a reader should meet them. */
141
+ export const COLLECTIVE_INTENTS = Object.freeze(["roll-call", "work", "ambient"]);
142
+
143
+ /**
144
+ * Phrases that ask each addressee to REPORT ON ITSELF. Excised from the body
145
+ * before the work gate runs (see rule 1), so a status ask that happens to use
146
+ * an action verb — "give me an UPDATE", "REVIEW your plate" — cannot read as a
147
+ * request to go and do something.
148
+ *
149
+ * Every one is anchored to a self-reference (`you`, `your`, `their`,
150
+ * `everyone`) or is a named ritual (roll call, standup, sound off). A bare
151
+ * "status" or "update" is NOT here: "update the binder" is work.
152
+ *
153
+ * @type {readonly RegExp[]}
154
+ */
155
+ export const ROLL_CALL_PATTERNS = Object.freeze([
156
+ /\broll[ -]?call\b/gi,
157
+ /\bsound off\b/gi,
158
+ /\bstatus check\b/gi,
159
+ /\bstand[ -]?up\b/gi,
160
+ /\bcheck[ -]?in\b/gi,
161
+ /\bwho(?:'s| is| has| are)\b[^.!?\n]{0,24}\b(?:around|online|here|available|free|working)\b/gi,
162
+ /\bwhat\b[^.!?\n]{0,16}\b(?:you|they)(?:'re| are)?\b[^.!?\n]{0,12}\b(?:working on|doing|up to|on right now)\b/gi,
163
+ /\b(?:give|post|share|send|drop|leave)\b[^.!?\n]{0,24}\b(?:an?\s+|your\s+|their\s+)?(?:update|status)\b/gi,
164
+ /\b(?:update|status)\b[^.!?\n]{0,24}\bfrom (?:your|their|each|you)\b/gi,
165
+ /\b(?:update|status)s?\s*(?:please|pls|report)\b/gi,
166
+ /\bupdate on what\b/gi,
167
+ /\bwhat(?:'s| is)\b[^.!?\n]{0,16}\bblocking (?:you|everyone|anyone)\b/gi,
168
+ /\bwhat(?:'s| is) next on (?:your|their)\b/gi,
169
+ /\b(?:your|their) (?:top |current )?(?:priority|priorities|task|tasks|plate|plates|lane|lanes)\b/gi,
170
+ /\breport (?:your|their|back with your)\b/gi,
171
+ /\bconfirm you can (?:see|read|hear)\b/gi,
172
+ /\bwhere (?:is|are) (?:everyone|everybody|we all)\b/gi,
173
+ /\bdid (?:everyone|everybody|you all|all of you)\b/gi,
174
+ ]);
175
+
176
+ /**
177
+ * Phrases that make a work-shaped sentence NOT an assignment. Read before the
178
+ * verb: this is the guard against "everyone should take a look sometime"
179
+ * minting thirteen rows, and it is deliberately the widest net here, because
180
+ * the asymmetry is real — a missed row costs one invisible ask, a spurious row
181
+ * costs thirteen and the owner's attention for the board itself.
182
+ *
183
+ * @type {readonly RegExp[]}
184
+ */
185
+ export const HEDGE_PATTERNS = Object.freeze([
186
+ /\bsometime\b/i,
187
+ /\bat some point\b/i,
188
+ /\bwhen(?:ever)? (?:you|anyone|someone|folks|people)\b[^.!?\n]{0,20}\b(?:chance|moment|time|minute|min|sec|second|bandwidth|capacity)\b/i,
189
+ /\bno (?:rush|hurry|pressure|urgency|action|need)\b/i,
190
+ /\bif (?:anyone|anybody|someone|you)\b[^.!?\n]{0,24}\b(?:time|chance|moment|minute|min|sec|bandwidth|capacity|curious|interested|keen)\b/i,
191
+ /\bfeel free\b/i,
192
+ /\bno action (?:needed|required|necessary)\b/i,
193
+ /\bnothing (?:to do|needed|required)\b/i,
194
+ /\b(?:purely |just )?(?:fyi|for (?:your )?(?:info|information|awareness|visibility))\b/i,
195
+ /\bmight (?:want|be worth|be of interest)\b/i,
196
+ /\bworth a (?:look|read|skim)\b/i,
197
+ /\boptional\b/i,
198
+ /\bwhenever\b/i,
199
+ /\bif you(?:'re| are) (?:curious|interested|bored)\b/i,
200
+ ]);
201
+
202
+ /**
203
+ * Does this body hedge hard enough that no verb in it is an assignment?
204
+ * @param {unknown} body
205
+ * @returns {boolean}
206
+ */
207
+ export function isHedged(body) {
208
+ const text = typeof body === "string" ? body : "";
209
+ if (!text.trim()) return false;
210
+ return HEDGE_PATTERNS.some((re) => re.test(text));
211
+ }
212
+
213
+ /**
214
+ * The body with every roll-call phrase blanked out. Exported for the tests and
215
+ * for a log line: "we called this a roll-call, and here is what was left after
216
+ * the status phrasing came out" is the one sentence that makes a wrong verdict
217
+ * diagnosable without a debugger.
218
+ *
219
+ * @param {unknown} body
220
+ * @returns {string}
221
+ */
222
+ export function withoutRollCallPhrases(body) {
223
+ let text = typeof body === "string" ? body : "";
224
+ if (!text.trim()) return "";
225
+ for (const re of ROLL_CALL_PATTERNS) {
226
+ // Every pattern is /g; `replace` with a global regex is stateless per call
227
+ // (it resets lastIndex itself), so these are safe as module constants —
228
+ // `test()` on a /g regex would NOT be, which is why nothing here calls it.
229
+ text = text.replace(re, " ");
230
+ }
231
+ return text;
232
+ }
233
+
234
+ /**
235
+ * Did any roll-call phrase appear at all?
236
+ * @param {unknown} body
237
+ * @returns {boolean}
238
+ */
239
+ export function hasRollCallPhrase(body) {
240
+ const text = typeof body === "string" ? body : "";
241
+ if (!text.trim()) return false;
242
+ return withoutRollCallPhrases(text) !== text;
243
+ }
244
+
245
+ /**
246
+ * THE decision. Pure, total, and the only implementation on this plane.
247
+ *
248
+ * Call it ONLY for a message already known to be collectively addressed; for
249
+ * anything else the answer is meaningless (a 1:1 DM saying "review the binder"
250
+ * is directed work and the ordinary path owns it).
251
+ *
252
+ * @param {unknown} body
253
+ * @returns {{intent: CollectiveIntent, reason: string}}
254
+ */
255
+ export function collectiveIntent(body) {
256
+ const text = (typeof body === "string" ? body : "").trim();
257
+ if (!text) return { intent: "ambient", reason: "empty" };
258
+
259
+ const rollCallPhrase = hasRollCallPhrase(text);
260
+
261
+ // A hedge beats every verb in the sentence — but NOT a roll-call. "everyone
262
+ // sound off whenever you get a sec" is still a roll-call; it just is not
263
+ // work, and a roll-call was never going to open a row anyway.
264
+ if (isHedged(text)) {
265
+ return rollCallPhrase
266
+ ? { intent: "roll-call", reason: "roll-call-phrase" }
267
+ : { intent: "ambient", reason: "hedged" };
268
+ }
269
+
270
+ const residue = rollCallPhrase ? withoutRollCallPhrases(text) : text;
271
+ if (hasSubstance(residue) && asksForWork(residue)) {
272
+ return { intent: "work", reason: "asks-for-work" };
273
+ }
274
+
275
+ if (rollCallPhrase) return { intent: "roll-call", reason: "roll-call-phrase" };
276
+ return { intent: "ambient", reason: "no-work-asked" };
277
+ }
278
+
279
+ /**
280
+ * Convenience: is this collective message WORK?
281
+ * @param {unknown} body
282
+ * @returns {boolean}
283
+ */
284
+ export function isCollectiveWork(body) {
285
+ return collectiveIntent(body).intent === "work";
286
+ }
287
+
288
+ /**
289
+ * The intent of a flattened INBOX ITEM that the inbound join already decided is
290
+ * a broadcast (`isRollCallItem`). Returns null for anything that is not one —
291
+ * a DM, an @mention, a task comment — because the classifier's answer is only
292
+ * meaningful once the collective address is established.
293
+ *
294
+ * Lives here rather than in broadcast.mjs so that module stays what it is: the
295
+ * ADDRESS, with no opinion about what the address asked for.
296
+ *
297
+ * @param {object} item flattened inbox item
298
+ * @param {(item:object)=>boolean} isCollectiveItem usually `isRollCallItem`
299
+ * @returns {CollectiveIntent|null}
300
+ */
301
+ export function itemCollectiveIntent(item, isCollectiveItem) {
302
+ if (typeof isCollectiveItem !== "function" || !isCollectiveItem(item)) return null;
303
+ const body = item && (item.content || item.text || item.subject);
304
+ return collectiveIntent(body).intent;
305
+ }
306
+
307
+ /**
308
+ * THE SHARED CORPUS. Literal strings, pinned by `collective.test.mjs` here and
309
+ * by hq's `src/server/work/collective.test.ts`. Every string is one a human
310
+ * could plausibly type into #general; every one already satisfies
311
+ * `isBroadcast`, so the only thing each pins is the intent.
312
+ *
313
+ * Same contract as BROADCAST_CORPUS: change a pattern on either plane and the
314
+ * other plane's test goes red until the two agree again.
315
+ */
316
+ export const COLLECTIVE_CORPUS = Object.freeze({
317
+ /** WORK: every addressee opens a row of its own. */
318
+ work: Object.freeze([
319
+ "Everyone, please review the binder by Friday",
320
+ "Everybody: please update your KPI rows in the mandate before end of week",
321
+ "@here we need to draft the board pack today",
322
+ "@channel please push your branches before the 18:00 freeze",
323
+ "All of you: please audit your channel memberships this week",
324
+ "Team, please prepare your section of the deck before Thursday",
325
+ "the whole team needs to review the binder before Friday",
326
+ "Everyone, please review the binder and give me an update when it's done",
327
+ "@all please fix the failing checks on your own repos today",
328
+ ]),
329
+ /** ROLL-CALL: every addressee answers for itself; no rows. */
330
+ rollCall: Object.freeze([
331
+ "Can everyone give me an update on what they're doing right now (across sessions on your machines)?",
332
+ "did everyone see the deck?",
333
+ "Everybody: standup in 5",
334
+ "Quick one for all of you — what's blocking you today?",
335
+ "I'd like each of you to post your top priority",
336
+ "Thanks to the whole team for the push last night, what's next on your plates?",
337
+ "Team, quick check-in: what are you on?",
338
+ "@here who is around?",
339
+ "@channel status please",
340
+ "@all please confirm you can see this",
341
+ "all agents: report your current task",
342
+ "All colleagues, a quick update from your side please",
343
+ "roll call",
344
+ "Roll-call — sound off with what you're working on",
345
+ "status check: where is everyone at?",
346
+ "everyone sound off whenever you get a sec",
347
+ ]),
348
+ /** AMBIENT: addressed to the room, asking for nothing. No rows. */
349
+ ambient: Object.freeze([
350
+ "everyone should take a look at the binder sometime",
351
+ "Everyone, feel free to review the binder when you get a chance",
352
+ "FYI everyone — the binder is up in the drive",
353
+ "@here the deploy finished, no action needed",
354
+ "Everyone, the offsite is on Thursday at the usual place",
355
+ "@channel just publishing the notes for visibility",
356
+ "Everybody: nice work this week, genuinely",
357
+ "everyone might want to review the new brand kit at some point",
358
+ "@all optional: the reading group meets at 4",
359
+ ]),
360
+ });
361
+
362
+ export default {
363
+ COLLECTIVE_INTENTS,
364
+ COLLECTIVE_CORPUS,
365
+ ROLL_CALL_PATTERNS,
366
+ HEDGE_PATTERNS,
367
+ asksForWork,
368
+ hasSubstance,
369
+ isHedged,
370
+ withoutRollCallPhrases,
371
+ hasRollCallPhrase,
372
+ collectiveIntent,
373
+ isCollectiveWork,
374
+ itemCollectiveIntent,
375
+ };