@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
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { resolve, join } from "path";
20
20
  import { readdirSync, readFileSync, renameSync, mkdirSync, appendFileSync, writeFileSync, unlinkSync } from "fs";
21
+ import { createRequire } from "module";
21
22
 
22
23
  // Load .env before anything else. dotenv is present on every agent machine but
23
24
  // is not a packaged dependency, so we import it dynamically + best-effort: this
@@ -57,7 +58,14 @@ import { makeInboxScanPoller } from "../poller/inbox-scan-poller.mjs";
57
58
  import { existsSync as _existsSync } from "fs";
58
59
  import { isPriorityItem } from "../poller/utils.mjs";
59
60
  import { classifyItem, isDirectedAtAgent } from "./classifier.mjs";
60
- import { dispatch, getStatus, availableSlots, canDispatchBacklog, resetActiveSessions, yieldBacklogForReactive } from "./dispatcher.mjs";
61
+ // A human's roll-call to the whole room ("everyone, status?"). Every addressed
62
+ // seat answers for itself — so it is EXEMPT from the single-winner election,
63
+ // gets a deterministic classification (no LLM triage), no holding ack, and a
64
+ // per-seat jitter so thirteen answers do not land in the same second.
65
+ import { isRollCallItem, rollCallJitterMs } from "../../lib/org/inbound/broadcast.mjs";
66
+ import { itemCollectiveIntent } from "../../lib/org/inbound/collective.mjs";
67
+ import { dispatch, getStatus, availableSlots, canDispatchBacklog, resetActiveSessions, yieldBacklogForReactive, linkActiveSessionBoard } from "./dispatcher.mjs";
68
+ import { boardItemRefFor } from "../../lib/session/current-work.mjs";
61
69
  import { markReactiveStart, markReactiveEnd } from "../../lib/reactive-gate.mjs";
62
70
  import { buildPrompt } from "./prompt-builder.mjs";
63
71
  import { sendQuickResponse, sendHoldingMessage, isQuickReply } from "./responder.mjs";
@@ -93,7 +101,7 @@ const RUNG_MECHANISM = Object.fromEntries(RUNGS.map((r) => [r.id, r.mechanism]))
93
101
  // work and land it in the org's shared, ACL'd store so the fleet's memory
94
102
  // compounds. Fail-open + best-effort — never blocks or throws into the hot path;
95
103
  // local SQLite/interaction logs remain the fail-open cache.
96
- import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder, fetchSelfProfile, listMineResult as orgListMineResult } from "../../lib/org/client.mjs";
104
+ import { isEnabled as orgEnabled, loadOrgConfig, configFromAgent, electResponder, fetchSelfProfile, listMineResult as orgListMineResult, call as orgCall } from "../../lib/org/client.mjs";
97
105
  // board.mine cache (design §3.5, WP-M6): the daemon writes state/org/board-mine.json
98
106
  // every 5 min; the SessionStart primer, session_status and `maestro board mine`
99
107
  // read it. The when/what is pure (lib/org/board-mine-cache.mjs); the I/O is here.
@@ -109,6 +117,9 @@ import { recordWorkStep as orgRecordWorkStep, cohortChannelId, cohortMessageId }
109
117
  // it: a holding "let me look into it" must not go out when the reply itself can
110
118
  // never be generated (the storm's other half — a promise the seat cannot keep).
111
119
  import { resolveClaudeBin, isSafeExecutable } from "../../lib/claude-bin.mjs";
120
+ // W4 integration (conformance row 1): an engine-cohort seat never spawns claude, so its
121
+ // presence check is the engine entry point, not the claude binary.
122
+ import { readSeatEngine as _seatEngineForAvailability, seatGenerationAvailable as _seatGenerationAvailable, ENGINE_CLI_PATH as _ENGINE_CLI_PATH } from "../../lib/runtime/adapter.mjs";
112
123
  import { boardPriority } from "./board-mirror.mjs";
113
124
  // Observability spine (WS — diagnostics). mintTraceId + withTrace give each
114
125
  // inbound item ONE trace_id that deep callees inherit via AsyncLocalStorage;
@@ -139,6 +150,7 @@ import { processOne } from "../../lib/execution/pipeline.mjs";
139
150
  // gate is pure (lib/session/frontdoor.mjs); the assurance sweep reopens a
140
151
  // session claim that was neither replied nor done within 20 min.
141
152
  import { makeFrontDoorGate } from "../../lib/session/frontdoor.mjs";
153
+ import { CADENCE_REGISTRY } from "./cadence-handlers.mjs";
142
154
  import { sweepSessionInboxForDaemon } from "../../lib/session/inbox-claims.mjs";
143
155
  import { defaultEffects, scheduleToQueue } from "../../lib/execution/effects.mjs";
144
156
  import { loadReactObligations } from "../../lib/execution/match.mjs";
@@ -459,13 +471,17 @@ export function enrichItem(item, service) {
459
471
  * @returns {boolean}
460
472
  */
461
473
  function claudeAvailable() {
462
- try {
463
- const bin = resolveClaudeBin();
464
- if (!bin || bin === "claude") return false; // bare fallback = nothing found
465
- return isSafeExecutable(bin);
466
- } catch {
467
- return false;
468
- }
474
+ let engine = "claude";
475
+ try { engine = _seatEngineForAvailability(AGENT_REPO_DIR); } catch { engine = "claude"; /* unreadable config: the default engine */ }
476
+ return _seatGenerationAvailable({
477
+ engine,
478
+ claudeBinOk: () => {
479
+ const bin = resolveClaudeBin();
480
+ if (!bin || bin === "claude") return false; // bare fallback = nothing found
481
+ return isSafeExecutable(bin);
482
+ },
483
+ engineCliPresent: () => _existsSync(_ENGINE_CLI_PATH),
484
+ });
469
485
  }
470
486
 
471
487
  /**
@@ -553,15 +569,113 @@ const FASTPATH_WORK_RE = /\b(analy[sz]e|create|build|writ(e|ing)|draft|prepare|r
553
569
  // A DM that reads like a question / greeting / quick ask — safe to answer directly.
554
570
  const FASTPATH_ASK_RE = /(\?|^(what|how|when|where|who|why|which|is |are |was |were |do |did |does |can |could |would |will |have |has |should |hey|hi |hello|thanks|thank you|yo |quick (q|question)|you (there|around|up)))/i;
555
571
 
572
+ /**
573
+ * Resolve the sender_privilege band handed to the classifier.
574
+ *
575
+ * A populated band from upstream wins. The inbox-item writer defaults a
576
+ * missing band to the string "unknown" (lib/channels/inbox-item.mjs), so
577
+ * "unknown" is treated as a miss, and only then does the CEO priority signal
578
+ * decide between "ceo" and "unknown".
579
+ *
580
+ * The previous inline form was
581
+ * `item.sender_privilege || item.priority_signals?.from_ceo ? "ceo" : "unknown"`
582
+ * which parses as `(a || b) ? "ceo" : "unknown"`: every non-empty band —
583
+ * including the default "unknown" — resolved to "ceo", and the classifier
584
+ * escalated the item as if the CEO had sent it.
585
+ *
586
+ * @param {object} item
587
+ * @returns {string}
588
+ */
589
+ export function resolveSenderPrivilege(item) {
590
+ const band = item?.sender_privilege;
591
+ if (band && band !== "unknown") return band;
592
+ return item?.priority_signals?.from_ceo ? "ceo" : "unknown";
593
+ }
594
+
556
595
  /**
557
596
  * Decide whether a directed DM is a "plainly simple question" that can skip the
558
597
  * LLM classifier and be answered directly. Returns a default classResult (the
559
598
  * quick-reply shape) when it qualifies, else null (→ full classify path).
560
599
  * Deliberately CONSERVATIVE: 1:1 DM, short, question-shaped, no work-verbs.
561
- * `deps.fastPathClassify` can override (tests); `deps.disableFastPath` disables.
600
+ * `deps.fastPathClassify` can override (tests); `deps.disableFastPath` (or
601
+ * DAEMON_DISABLE_FASTPATH=1) disables the DM heuristic — NOT the roll-call
602
+ * branch, which is deterministic and is checked before the switch.
603
+ */
604
+ /**
605
+ * The identity string that makes this seat's roll-call jitter DIFFERENT from
606
+ * every other seat's: the org member slug (COHORT_AGENT_ID / config
607
+ * org.cohort.agentId), falling back to the configured first name. Empty only
608
+ * on an unconfigured seat, where every seat would share a key — harmless
609
+ * (they still wait), just not spread.
562
610
  */
611
+ function rollCallSeatKey(deps = {}) {
612
+ try {
613
+ const cfg = deps.orgCfg !== undefined ? deps.orgCfg : daemonOrgCfg();
614
+ return String(
615
+ process.env.COHORT_AGENT_ID ||
616
+ (cfg && cfg.org && cfg.org.cohort && cfg.org.cohort.agentId) ||
617
+ (loadAgent().firstName || ""),
618
+ ).trim().toLowerCase();
619
+ } catch {
620
+ return "";
621
+ }
622
+ }
623
+
563
624
  export function fastPathClassify(item, isDm, deps = {}) {
564
625
  if (deps && typeof deps.fastPathClassify === "function") return deps.fastPathClassify(item, isDm);
626
+ // A COLLECTIVE ADDRESS is classified deterministically. The inbound join
627
+ // already proved the four broadcast conditions (human author, my room, not
628
+ // threaded, collective address) before it stamped the kind, and the answer
629
+ // shape is fixed (≤ 5 lines, for myself). Sending it through the LLM
630
+ // classifier would only add ~5-8 s and a chance of "archive" — the exact
631
+ // silence this surface exists to end, and one that would now also swallow a
632
+ // work ask carrying a deadline. `answerable + respond` routes it to the
633
+ // in-process quick-reply responder, which is where the collective block is
634
+ // injected.
635
+ //
636
+ // This branch sits ABOVE the fast-path kill switches on purpose. The
637
+ // switches (`deps.disableFastPath`, DAEMON_DISABLE_FASTPATH=1) exist to
638
+ // force a plain DM through the full LLM classify path when the heuristic
639
+ // regexes below misjudge one; a roll-call is not a heuristic call — its
640
+ // conditions were proven upstream — so routing it to the classifier under
641
+ // the switch would re-open the "archive" hole the branch closes. Pinned by
642
+ // the "ROLL-CALL: survives the fast-path kill switch" test.
643
+ if (isRollCallItem(item)) {
644
+ // WHICH KIND of collective address this is (`lib/org/inbound/collective.mjs`,
645
+ // the twin of hq's `work/collective.ts`). It does NOT change the ROUTE —
646
+ // every one of the three still takes the quick path — it changes the
647
+ // prompt block and the summary line.
648
+ //
649
+ // ── WHY A WORK BROADCAST DOES NOT SPAWN THIRTEEN SESSIONS ─────────────
650
+ //
651
+ // "Everyone, please review the binder by Friday" is real work for thirteen
652
+ // seats, and the tempting move is to dispatch a session per seat. That is
653
+ // thirteen fifteen-to-forty-minute Claude runs fired by one sentence, on
654
+ // thirteen machines, inside one poll tick — the storm shape this daemon has
655
+ // been burned by before, bought for an ask with a Friday deadline.
656
+ //
657
+ // The proportionate answer is the one the org already has: the quick reply
658
+ // says what this seat will do and by when, and the SAME quick path's
659
+ // `trackWorkStep` opens the seat's OWN board row (hq's gate accepts it now
660
+ // that the audience travels as a fact — see lib/org/work-ledger.mjs). The
661
+ // row is then pulled through the ordinary board-work lane, which is rate-
662
+ // controlled, resumable, and visible. A broadcast makes work VISIBLE AND
663
+ // OWNED here; it does not make thirteen machines start at once.
664
+ const intent = itemCollectiveIntent(item, isRollCallItem) || "roll-call";
665
+ const label = intent === "work" ? "Everyone-work" : intent === "ambient" ? "Everyone" : "Roll-call";
666
+ return {
667
+ priority: "normal",
668
+ action: "respond",
669
+ model: "sonnet",
670
+ summary: `${label}: ${String(item.content || item.subject || "").slice(0, 80)}`,
671
+ category: "action_required",
672
+ directed_at_agent: true,
673
+ answerable: true,
674
+ fast_path: true,
675
+ roll_call: true,
676
+ collective_intent: intent,
677
+ };
678
+ }
565
679
  if (deps && deps.disableFastPath) return null;
566
680
  if (process.env.DAEMON_DISABLE_FASTPATH === "1") return null;
567
681
  if (!isDm) return null; // 1:1 DMs only (never group/ambient)
@@ -627,12 +741,12 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
627
741
  let classResult = fastPathClassify(item, isDm, deps);
628
742
  if (classResult) {
629
743
  recordClassification(true);
630
- console.log(`[daemon] fast-path DM — skipped classify (${classResult.model}): ${classResult.summary}`);
744
+ console.log(`[daemon] fast-path ${classResult.roll_call ? "roll-call" : "DM"} — skipped classify (${classResult.model}): ${classResult.summary}`);
631
745
  } else {
632
746
  // Classify via Haiku CLI
633
747
  classResult = await _classify({
634
748
  sender: item.sender || "unknown",
635
- sender_privilege: item.sender_privilege || item.priority_signals?.from_ceo ? "ceo" : "unknown",
749
+ sender_privilege: resolveSenderPrivilege(item),
636
750
  channel: item.channel || "unknown",
637
751
  service,
638
752
  content: item.content || item.subject || "",
@@ -681,7 +795,15 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
681
795
  // respond-to-all. The directed fast path is untouched, so an org outage can
682
796
  // never drop a genuinely-directed DM. Slack keeps its own local gate below,
683
797
  // where no org roster exists to elect against.
684
- if (service === "cohort" && !isDm && !isDirectedAtAgent(item)) {
798
+ //
799
+ // A ROLL-CALL IS EXEMPT. The election elects ONE responder for an ambient
800
+ // message; a human's "everyone, status?" is the opposite — every addressed
801
+ // member owes its own line, and reducing it to a single winner would leave
802
+ // twelve seats silent on a message that named all thirteen. The inbound
803
+ // join proved the address (human author, my room, collective marker), so
804
+ // this is not ambient traffic and there is nothing to hold an election over.
805
+ const rollCall = isRollCallItem(item);
806
+ if (service === "cohort" && !isDm && !rollCall && !isDirectedAtAgent(item)) {
685
807
  const verdict = await _electResponderVerdict(item);
686
808
  if (!verdict.electedMe) {
687
809
  console.log(`[daemon] Election: not elected to respond (${verdict.reason}) — staying silent for ${item.sender} in ${item.channel}`);
@@ -834,6 +956,26 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
834
956
  // path does not have, so a quick reply would be a wrong answer delivered
835
957
  // fast. With no routing decision (an item the ladder could not place) this
836
958
  // is exactly today's behaviour.
959
+ // ROLL-CALL RATE SHAPING. Thirteen seats received the same event in the
960
+ // same poll tick; without this their answers land in the same second and
961
+ // the room reads as a machine chorus. Each seat waits a deterministic slice
962
+ // of a 0-20 s window keyed on (seat, channel, message) — the same seat
963
+ // re-answering after a crash waits the same amount, and no two seats share
964
+ // a key. Applied to BOTH reply paths (quick reply here, session below).
965
+ let rollCallDelayMs = 0;
966
+ if (rollCall) {
967
+ rollCallDelayMs = rollCallJitterMs({
968
+ seat: rollCallSeatKey(deps),
969
+ channelId: item.channel_id || item.channel || "",
970
+ messageId: cohortMessageId(item) || item.id || "",
971
+ });
972
+ const _sleep = deps.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
973
+ if (rollCallDelayMs > 0) {
974
+ console.log(`[daemon] roll-call: waiting ${rollCallDelayMs}ms before answering in ${item.channel} (rate shaping)`);
975
+ await _sleep(rollCallDelayMs);
976
+ }
977
+ }
978
+
837
979
  if (_isQuickReply(classResult, routed)) {
838
980
  console.log(`[daemon] Quick reply path for ${item.sender} (${classResult.model})`);
839
981
  // Free the subscription NOW: evict any already-running backlog sessions
@@ -859,6 +1001,29 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
859
1001
  // created and closed in the same instant claims the ask was delivered by
860
1002
  // a message; leaving it open is the truthful reading, and the stable
861
1003
  // askKey means whatever really finishes it closes THIS row.
1004
+ //
1005
+ // THIS IS ALSO THE COLLECTIVE PATH. A work-shaped broadcast is answered
1006
+ // here and gets its row here — one per addressee, keyed on the calling
1007
+ // seat server-side, tagged into the channel exactly once across all of
1008
+ // them. A roll-call reaches the same call and is refused by name
1009
+ // (`collective-roll-call`), which is the gate working, not a miss.
1010
+ //
1011
+ // ── AND IT IS ONLY ONE OF THE TWO PLANES. SAY SO. ─────────────────
1012
+ //
1013
+ // Everything in this function is downstream of `answerItem`, which is
1014
+ // only ever reached through `pollService` — and `pollService` returns 0
1015
+ // at its third line (`if (!fd.dispatch) return 0`) whenever
1016
+ // `makeFrontDoorGate` says a live main session owns this service's
1017
+ // inbox. That is the NORMAL state of a healthy seat, and it is the same
1018
+ // fork the 13-day board blackout hid behind: a test that calls
1019
+ // `answerItem` directly is green over a path production does not take.
1020
+ //
1021
+ // On a front-door seat the item never reaches here at all. It becomes
1022
+ // an `inbound` feed line (lib/session/feed-core.mjs), which is why that
1023
+ // line now carries `collective` — the session reads the same verdict
1024
+ // this branch computed and opens its own row per `inbound-triage`
1025
+ // §"Addressed to the whole room". Two planes, one classifier, and
1026
+ // neither of them silently inert.
862
1027
  void trackWorkStep({
863
1028
  item,
864
1029
  stage: "accepted",
@@ -1038,7 +1203,9 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
1038
1203
  // (the old board mirror) came to exist. Chained rather than fired in
1039
1204
  // parallel because `triage` is not terminal: an `accepted` that commits
1040
1205
  // AFTER a `working` drags the row back out of `running` for good.
1041
- void trackWorkStep({
1206
+ const _trackWorkStep = deps.trackWorkStep || trackWorkStep;
1207
+ const _linkSessionToBoardRow = deps.linkSessionToBoardRow || linkSessionToBoardRow;
1208
+ void _trackWorkStep({
1042
1209
  item,
1043
1210
  stage: "accepted",
1044
1211
  deferred: true,
@@ -1046,13 +1213,23 @@ export async function answerItem(item, service, itemId, trace_id, deps = {}, rou
1046
1213
  note: classResult && classResult.summary ? `Picked this up. ${classResult.summary}` : "Picked this up — starting work now.",
1047
1214
  source: { service, trace_id, classified: String(classResult && classResult.action) },
1048
1215
  }).then(() =>
1049
- trackWorkStep({
1216
+ _trackWorkStep({
1050
1217
  item,
1051
1218
  stage: "working",
1052
1219
  deferred: true,
1053
1220
  source: { service, trace_id },
1054
1221
  }),
1055
- );
1222
+ ).then((res) => {
1223
+ // THE LINK BACK. hq's row id reaches the live session's active.json entry,
1224
+ // so a "what are you doing right now?" reply can cite the board item a
1225
+ // person could open — instead of a session that knows it is working and
1226
+ // a board that knows what on, with nothing joining the two.
1227
+ _linkSessionToBoardRow(item, res);
1228
+ }).catch((err) => {
1229
+ // The chain is fire-and-forget; a rejection here must be a log line, not
1230
+ // an unhandled rejection that takes the daemon down.
1231
+ console.warn(`[board] track chain failed: ${err && err.message}`);
1232
+ });
1056
1233
 
1057
1234
  // Build prompt with holding message context and dispatch
1058
1235
  const prompt = await buildPrompt(item, classResult, {
@@ -1796,6 +1973,92 @@ export function _resetDaemonOrgCfgCache() {
1796
1973
  _cachedDaemonOrgCfg = undefined;
1797
1974
  }
1798
1975
 
1976
+ /**
1977
+ * Startup guard: a cadence with a producer but no prompt file dead-letters on
1978
+ * every tick, identically, forever. A cadence resolves to `mode: escalate` /
1979
+ * `guarded` with a prompt path via config/.cadence-registry.json, or via the
1980
+ * "conventional" schedules/triggers/<id>.md that cadence-consumer.mjs's
1981
+ * getCadenceDef() falls back to for an unknown id; when that file is absent
1982
+ * the tick can never be rendered, and the DLQ refills — 42 times on the seat
1983
+ * this was written on (ib-2026-09-03-eight-cadences-fire-with-no-prompt-file).
1984
+ * Ported from that seat's fork 2026-09-22.
1985
+ *
1986
+ * CADENCE_REGISTRY (the hardcoded, code-reviewed map in cadence-handlers.mjs)
1987
+ * is deliberately EXCLUDED: getCadenceDef() checks it first and it wins, so an
1988
+ * entry there with no `prompt` (skill-curator, nightly-backup — inline handlers
1989
+ * or guards documented as "never escalates") is by design, not a defect.
1990
+ * Checking config/cadences.yaml directly would false-positive on exactly these,
1991
+ * since that compiled file still carries a `prompt:` for every id.
1992
+ *
1993
+ * Sources checked (both are inputs to what actually resolves at tick time):
1994
+ * - config/.cadence-registry.json — the compiled config-driven registry
1995
+ * getCadenceDef() reads once CADENCE_REGISTRY misses.
1996
+ * - config/plan.yaml — active SCHEDULE obligations feed resolveCadences()'s
1997
+ * obligations leg directly (lib/cadences.mjs), independent of the JSON
1998
+ * registry, which is how a cadence kept firing with no plist at all.
1999
+ *
2000
+ * Fail-open throughout: a read/parse failure logs a warning and is skipped,
2001
+ * never blocks daemon startup. Logs once, at boot; returns the ids so a test
2002
+ * (and doctor) can read the answer without the log.
2003
+ * @param {{agentRoot?:string, registry?:object}} [deps] test seams
2004
+ * @returns {string[]} cadence ids whose prompt file is missing
2005
+ */
2006
+ export function assertCadencePromptsExist(deps = {}) {
2007
+ const root = deps.agentRoot || AGENT_REPO_DIR;
2008
+ const hardcoded = deps.registry || CADENCE_REGISTRY;
2009
+ const candidates = new Map(); // id -> prompt path (repo-relative)
2010
+
2011
+ try {
2012
+ const registryPath = join(root, "config", ".cadence-registry.json");
2013
+ if (_existsSync(registryPath)) {
2014
+ const registry = JSON.parse(readFileSync(registryPath, "utf-8")) || {};
2015
+ for (const [id, entry] of Object.entries(registry)) {
2016
+ if (hardcoded[id]) continue; // hardcoded handler wins — trust it
2017
+ candidates.set(id, (entry && entry.prompt) || `schedules/triggers/${id}.md`);
2018
+ }
2019
+ }
2020
+ } catch (err) {
2021
+ console.warn(`[daemon] cadence prompt assertion: could not read .cadence-registry.json (${err && err.message ? err.message : err})`);
2022
+ }
2023
+
2024
+ try {
2025
+ const planPath = join(root, "config", "plan.yaml");
2026
+ if (_existsSync(planPath)) {
2027
+ const plan = yamlLoadSync(readFileSync(planPath, "utf-8"));
2028
+ for (const ob of Array.isArray(plan && plan.obligations) ? plan.obligations : []) {
2029
+ if (ob.kind !== "SCHEDULE" || !ob.cadence_id) continue;
2030
+ if (ob.status && ob.status !== "active") continue;
2031
+ if (hardcoded[ob.cadence_id]) continue; // hardcoded handler wins — trust it
2032
+ candidates.set(ob.cadence_id, ob.prompt || `schedules/triggers/${ob.cadence_id}.md`);
2033
+ }
2034
+ }
2035
+ } catch (err) {
2036
+ console.warn(`[daemon] cadence prompt assertion: could not read plan.yaml (${err && err.message ? err.message : err})`);
2037
+ }
2038
+
2039
+ const missing = [];
2040
+ for (const [id, prompt] of candidates) {
2041
+ if (!_existsSync(join(root, prompt))) missing.push(id);
2042
+ }
2043
+ if (missing.length > 0) {
2044
+ console.warn(
2045
+ `[daemon] ${missing.length} cadence(s) have a producer but no prompt file — every tick will dead-letter: ${missing.sort().join(", ")}`
2046
+ );
2047
+ }
2048
+ return missing;
2049
+ }
2050
+
2051
+ let _yamlLoadSyncCache = null;
2052
+ function yamlLoadSync(text) {
2053
+ if (!_yamlLoadSyncCache) {
2054
+ // require() rather than a dynamic import keeps this assertion synchronous
2055
+ // so it can run inline at the top of main(), before the first poll —
2056
+ // js-yaml ships a CJS build so this resolves cleanly.
2057
+ _yamlLoadSyncCache = createRequire(import.meta.url)("js-yaml");
2058
+ }
2059
+ return _yamlLoadSyncCache.load(text);
2060
+ }
2061
+
1799
2062
  /**
1800
2063
  * Refresh state/org/board-mine.json from hq `board.mine` when the cache is
1801
2064
  * 5 min old (or absent). Fail-open end to end: an un-enrolled seat writes
@@ -1987,6 +2250,87 @@ export async function trackWorkStep(a = {}) {
1987
2250
  }
1988
2251
  }
1989
2252
 
2253
+ /**
2254
+ * Join hq's board row to the daemon session working the same ask (dispatcher
2255
+ * `linkActiveSessionBoard`), so the current-work block names the item. Pure
2256
+ * over its arguments except for the one active.json rewrite the dispatcher
2257
+ * does; fail-open; `linkImpl` is the test seam.
2258
+ *
2259
+ * @param {object} item the inbox item the row was tracked for
2260
+ * @param {object} res the `working` trackWorkStep result
2261
+ * @param {{linkImpl?:Function}} [deps]
2262
+ * @returns {{linked:boolean, sessionId:string|null, parked:boolean}|null} null when there was nothing to link
2263
+ */
2264
+ export function linkSessionToBoardRow(item, res, deps = {}) {
2265
+ try {
2266
+ if (!res || !res.tracked || !res.taskId) return null;
2267
+ const channelId = cohortChannelId(item);
2268
+ const messageId = cohortMessageId(item);
2269
+ if (!channelId || !messageId) return null;
2270
+ const impl = deps.linkImpl || linkActiveSessionBoard;
2271
+ const out = impl({ channelId, messageId }, { taskId: res.taskId, col: res.col || null });
2272
+ if (out && (out.linked || out.parked)) {
2273
+ console.log(`[board] task ${res.taskId} ${out.linked ? `linked to session ${out.sessionId}` : "parked until its session registers"}`);
2274
+ }
2275
+ return out || null;
2276
+ } catch (err) {
2277
+ console.warn(`[board] link to session failed: ${err && err.message}`);
2278
+ return null;
2279
+ }
2280
+ }
2281
+
2282
+ /**
2283
+ * WORK VISIBILITY for a BACKLOG session that is for a BOARD ITEM. An inbox ask
2284
+ * gets accepted→working→done/failed through `trackWorkStep` (board.track,
2285
+ * keyed on the message). A backlog row the execution ladder queued for a
2286
+ * `task_assigned` / `task_comment` event is about an item that ALREADY EXISTS
2287
+ * on a board and was assigned to this agent — `board.track` has no message to
2288
+ * key on and `board.claim` refuses an item that is already assigned. What the
2289
+ * item's thread CAN carry is a comment: one when the session starts on it,
2290
+ * one when the session ends, each naming the session ref so the row and the
2291
+ * session log join. `board.comment` dedupes nothing server-side, so the
2292
+ * idempotency key is (item, stage, session).
2293
+ *
2294
+ * Fire-and-forget, fail-open, never silent — same contract as trackWorkStep.
2295
+ *
2296
+ * @param {object} a - { item (queue row), stage: "working"|"done"|"failed", sessionRef, note?, cfg?, callImpl? }
2297
+ * @returns {Promise<{tracked:boolean, reason:string, itemId?:string|null}>}
2298
+ */
2299
+ export async function trackBoardItemStep(a = {}) {
2300
+ try {
2301
+ const ref = boardItemRefFor(a.item);
2302
+ if (!ref) return { tracked: false, reason: "not-a-board-item" };
2303
+ const stage = String(a.stage || "").trim();
2304
+ if (!["working", "done", "failed"].includes(stage)) return { tracked: false, reason: "bad-stage" };
2305
+ const cfg = a.cfg !== undefined ? a.cfg : daemonOrgCfg();
2306
+ if (!orgEnabled(cfg)) return { tracked: false, reason: "org-disabled" };
2307
+ const conn = configFromAgent(cfg);
2308
+ const sessionRef = String(a.sessionRef || "").trim() || "unknown";
2309
+ const lead =
2310
+ stage === "working" ? "Started on this." :
2311
+ stage === "done" ? "Finished a working session on this." :
2312
+ "A working session on this ended without completing; it is still owed.";
2313
+ const body = `${lead}${a.note ? ` ${String(a.note).slice(0, 2000)}` : ""} (session ${sessionRef})`;
2314
+ const impl = a.callImpl || orgCall;
2315
+ const res = await impl("board.comment", { itemId: ref.itemId, body }, {
2316
+ base: conn.base,
2317
+ token: conn.token,
2318
+ fetchImpl: a.fetchImpl,
2319
+ idempotencyKey: `board.comment:${ref.itemId}:${stage}:${sessionRef}`,
2320
+ });
2321
+ if (!res || res.error) {
2322
+ const reason = res && res.error ? `${res.error.code || "error"}: ${res.error.message || ""}` : "no-response";
2323
+ console.log(`[board] item ${ref.itemId} ${stage} not tracked: ${reason.slice(0, 200)}`);
2324
+ return { tracked: false, reason: reason.slice(0, 200), itemId: ref.itemId };
2325
+ }
2326
+ console.log(`[board] item ${ref.itemId} ${stage} — commented (session ${sessionRef})`);
2327
+ return { tracked: true, reason: "ok", itemId: ref.itemId };
2328
+ } catch (err) {
2329
+ console.warn(`[board] item track failed (${a.stage}): ${err && err.message}`);
2330
+ return { tracked: false, reason: "error" };
2331
+ }
2332
+ }
2333
+
1990
2334
  // A distillation longer than this is truncated before it lands in the store —
1991
2335
  // the shared memory holds a salient one-liner, never a transcript.
1992
2336
  const SESSION_DISTILL_MAX_CHARS = 400;
@@ -2224,9 +2568,34 @@ export function recoverInFlight() {
2224
2568
  // Backlog Sweep
2225
2569
  // ---------------------------------------------------------------------------
2226
2570
 
2227
- async function sweepBacklog() {
2228
- const slots = availableSlots();
2571
+ /**
2572
+ * Drain the backlog queues into sessions. `deps` are test seams only —
2573
+ * `{dispatch, buildPrompt, trackBoardItemStep, availableSlots}` — so the
2574
+ * board-item bookkeeping around a dispatch can be pinned without a child
2575
+ * process or an hq.
2576
+ */
2577
+ export async function sweepBacklog(deps = {}) {
2578
+ const dispatchImpl = deps.dispatch || dispatch;
2579
+ const buildPromptImpl = deps.buildPrompt || buildPrompt;
2580
+ const trackBoardItem = deps.trackBoardItemStep || trackBoardItemStep;
2581
+ const slots = (deps.availableSlots || availableSlots)();
2229
2582
  if (slots <= 0) return; // No capacity
2583
+ // FRONT DOOR (design §3.1, ported from a seat fork 2026-09-22): while the
2584
+ // main session is live it works the board itself (main-session skill, idle
2585
+ // loop). A daemon backlog session on the same rows holds the seat's pen and
2586
+ // voice beside it — four such sessions edited a pack and DM'd a dark seat on
2587
+ // one seat on 9 Sep 2026 while the front door was open. pollService already
2588
+ // steps back from the INBOX on this gate; the backlog sweep is the other
2589
+ // lane that spawns in the seat's name, and it must step back on the same
2590
+ // signal. Fail-open on a reader error (the gate's worst case is the old
2591
+ // behaviour: the daemon sweeps). `deps.frontDoorGate` is the test seam.
2592
+ {
2593
+ const fd = (deps.frontDoorGate || _frontDoorGate).check("cohort");
2594
+ if (!fd.dispatch) {
2595
+ if (fd.changed) console.log(`[daemon] front door live (${fd.reason}) — backlog belongs to the main session; sweep skipped`);
2596
+ return;
2597
+ }
2598
+ }
2230
2599
 
2231
2600
  try {
2232
2601
  const queueDir = join(AGENT_REPO_DIR, "state", "queues");
@@ -2352,7 +2721,7 @@ async function sweepBacklog() {
2352
2721
  );
2353
2722
  }
2354
2723
 
2355
- const prompt = await buildPrompt(null, classResult, {
2724
+ const prompt = await buildPromptImpl(null, classResult, {
2356
2725
  type: "backlog",
2357
2726
  queueItem,
2358
2727
  });
@@ -2361,8 +2730,26 @@ async function sweepBacklog() {
2361
2730
  // they get the same outcome discipline. This onClose does NOT touch the
2362
2731
  // backlog finalize path (the session updates its own queue item) — it is
2363
2732
  // purely the outcome sweep, gated off by default.
2364
- dispatch(prompt, queueItem, classResult, "backlog", {
2733
+ // "Started on this" is said when a child has STARTED — from the
2734
+ // dispatcher's `onSpawn`, with the session id it minted — never at the
2735
+ // call site: `dispatch()` returns silently on rate-limit, governor
2736
+ // QUEUE/DEFER, the backlog cap and a denied claim, and a comment fired
2737
+ // here would have told the board about a session that did not exist
2738
+ // until some later sweep succeeded (if it did). Both comments carry the
2739
+ // real session id, so a retried row that genuinely starts again says so.
2740
+ let startRef = null;
2741
+ dispatchImpl(prompt, queueItem, classResult, "backlog", {
2742
+ onSpawn: ({ sessionId }) => {
2743
+ startRef = String(sessionId || "");
2744
+ void trackBoardItem({ item: queueItem, stage: "working", sessionRef: startRef, note: classResult.summary });
2745
+ },
2365
2746
  onClose: ({ ok, sessionId, stdout }) => {
2747
+ void trackBoardItem({
2748
+ item: queueItem,
2749
+ stage: ok ? "done" : "failed",
2750
+ sessionRef: String(sessionId || startRef || "unknown"),
2751
+ note: ok ? sessionCompletionNote(stdout) : null,
2752
+ });
2366
2753
  if (!ok) return;
2367
2754
  Promise.resolve(sweepOutcomesForSession({ sessionId, stdout, item: queueItem, classResult }))
2368
2755
  .catch(() => { /* fail-open */ });
@@ -2432,6 +2819,7 @@ async function main() {
2432
2819
  // that doctor/operators read. subscribe + bump are both fail-open; an init
2433
2820
  // failure here can never stop the daemon coming up.
2434
2821
  registerHookSubscribers();
2822
+ assertCadencePromptsExist();
2435
2823
 
2436
2824
  // Check for emergency stop
2437
2825
  try {