@cohortapp/agent-sdk 2.16.0 → 2.18.4

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 (529) hide show
  1. package/.claude/settings.json +18 -0
  2. package/.env.example +23 -7
  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/poller-daemon-setup.md +4 -1
  9. package/docs/guides/setup-wizard.md +1 -1
  10. package/docs/runbooks/fleet-rollout.md +156 -0
  11. package/docs/runbooks/mac-mini-bootstrap.md +12 -14
  12. package/lib/action-executor.js +19 -3
  13. package/lib/budget-guard.mjs +279 -3
  14. package/lib/channels/base-adapter.mjs +3 -1
  15. package/lib/channels/contract.mjs +2 -1
  16. package/lib/channels/inbox-item.mjs +8 -0
  17. package/lib/claude-bin.mjs +5 -6
  18. package/lib/cli/doctor-checks.mjs +141 -10
  19. package/lib/cli/global-setup-extras.mjs +5 -1
  20. package/lib/cli/inbox.mjs +100 -15
  21. package/lib/cli/seat-auth.mjs +463 -0
  22. package/lib/cli/session.mjs +80 -12
  23. package/lib/collective/capture.mjs +8 -6
  24. package/lib/collective/global-config.mjs +63 -1
  25. package/lib/collective/presence.mjs +142 -5
  26. package/lib/comms/send-gate.mjs +559 -1
  27. package/lib/context/budget.mjs +327 -0
  28. package/lib/context/history-scope.mjs +138 -0
  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/economics.mjs +9 -0
  138. package/lib/model-router/failover.mjs +67 -16
  139. package/lib/model-router/llm-task.mjs +39 -3
  140. package/lib/model-router/resolve.mjs +95 -3
  141. package/lib/model-router/spawn.mjs +46 -47
  142. package/lib/model-router/taxonomy.mjs +126 -4
  143. package/lib/org/cost-sync.mjs +141 -11
  144. package/lib/org/inbound/broadcast.mjs +289 -0
  145. package/lib/org/inbound/collective.mjs +375 -0
  146. package/lib/org/inbound/directedness.mjs +96 -8
  147. package/lib/org/inbound/facts.mjs +82 -4
  148. package/lib/org/inbound/hydrate.mjs +555 -51
  149. package/lib/org/inbound/project.mjs +22 -0
  150. package/lib/org/inbound/surfaces.mjs +14 -0
  151. package/lib/org/llm-token.mjs +879 -0
  152. package/lib/org/mesh.mjs +61 -0
  153. package/lib/org/messaging.mjs +3 -1
  154. package/lib/org/protocol.checksum +1 -1
  155. package/lib/org/protocol.mjs +15 -0
  156. package/lib/org/quota.mjs +520 -0
  157. package/lib/org/tool-surface.mjs +104 -16
  158. package/lib/org/ui-parity.mjs +16 -1
  159. package/lib/org/work-ledger.mjs +37 -6
  160. package/lib/rate-guard.mjs +114 -1
  161. package/lib/resource-governor.mjs +41 -6
  162. package/lib/runtime/adapter.mjs +823 -0
  163. package/lib/runtime/child-env.mjs +191 -0
  164. package/lib/runtime/legacy-shell-guard.mjs +97 -0
  165. package/lib/runtime/seat-engine.mjs +162 -0
  166. package/lib/session/ask-ledger.mjs +271 -0
  167. package/lib/session/current-work.mjs +676 -0
  168. package/lib/session/feed-core.mjs +40 -3
  169. package/lib/session/launch-args.mjs +56 -4
  170. package/lib/session/status-summary.mjs +26 -9
  171. package/lib/session/upgrade-notice.mjs +42 -0
  172. package/lib/setup/claude-probe.mjs +117 -13
  173. package/lib/setup/enrich.mjs +13 -10
  174. package/lib/setup/sections/model.mjs +39 -13
  175. package/lib/telemetry/collect.mjs +208 -9
  176. package/lib/upgrade/ignored-drift.mjs +105 -0
  177. package/lib/voice/post-call-brief.mjs +30 -17
  178. package/package.json +15 -3
  179. package/plugins/maestro-skills/skills/board-work.md +5 -0
  180. package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
  181. package/plugins/maestro-skills/skills/main-session.md +18 -7
  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 +29 -2
  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 +104 -59
  194. package/scripts/daemon/deliver.mjs +30 -1
  195. package/scripts/daemon/dispatcher.mjs +804 -157
  196. package/scripts/daemon/health.mjs +14 -1
  197. package/scripts/daemon/lib/session-router.mjs +310 -42
  198. package/scripts/daemon/maestro-daemon.mjs +11 -0
  199. package/scripts/daemon/prompt-builder.mjs +121 -12
  200. package/scripts/daemon/responder.mjs +315 -146
  201. package/scripts/daemon/sdk-version.mjs +98 -16
  202. package/scripts/eval/probe-gateway.mjs +635 -0
  203. package/scripts/eval/replay/extract.mjs +270 -0
  204. package/scripts/eval/replay/grade.mjs +260 -0
  205. package/scripts/eval/replay/lib/config.mjs +50 -0
  206. package/scripts/eval/replay/lib/effects.mjs +65 -0
  207. package/scripts/eval/replay/lib/fixture.mjs +188 -0
  208. package/scripts/eval/replay/lib/judge.mjs +72 -0
  209. package/scripts/eval/replay/lib/redact.mjs +136 -0
  210. package/scripts/eval/replay/lib/sandbox.mjs +170 -0
  211. package/scripts/eval/replay/lib/schema-check.mjs +63 -0
  212. package/scripts/eval/replay/lib/transcript.mjs +76 -0
  213. package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
  214. package/scripts/eval/replay/report.mjs +185 -0
  215. package/scripts/eval/replay/run.mjs +404 -0
  216. package/scripts/fleet/rollout.mjs +1094 -0
  217. package/scripts/hooks/pre-send-audit.sh +36 -245
  218. package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
  219. package/scripts/hooks/validate-state-yaml.sh +190 -0
  220. package/scripts/huddle/huddle-llm.mjs +361 -0
  221. package/scripts/huddle/huddle-server.mjs +46 -121
  222. package/scripts/local-triggers/autoupdate.sh +448 -78
  223. package/scripts/local-triggers/run-trigger.sh +13 -0
  224. package/scripts/maintenance/pin-integrity.mjs +364 -0
  225. package/scripts/poll-slack-events.sh +41 -9
  226. package/scripts/poller/slack-socket-mode.mjs +28 -3
  227. package/scripts/session/supervisor.mjs +80 -13
  228. package/scripts/spawn-session.sh +13 -0
  229. package/bin/maestro.test.mjs +0 -1574
  230. package/lib/action-executor.test.mjs +0 -871
  231. package/lib/archetype.test.mjs +0 -132
  232. package/lib/assurance/plan-note.test.mjs +0 -234
  233. package/lib/assurance/room-budget.test.mjs +0 -486
  234. package/lib/assurance/tier.test.mjs +0 -174
  235. package/lib/autonomy.test.mjs +0 -66
  236. package/lib/backlog.test.mjs +0 -302
  237. package/lib/backup/policy.test.mjs +0 -305
  238. package/lib/budget-escalate.test.mjs +0 -232
  239. package/lib/budget-guard.envelope.test.mjs +0 -476
  240. package/lib/budget-guard.test.mjs +0 -427
  241. package/lib/cadence-bus-requeue.test.mjs +0 -83
  242. package/lib/cadence-bus-schedule.test.mjs +0 -194
  243. package/lib/cadence-bus.test.mjs +0 -720
  244. package/lib/cadences.test.mjs +0 -230
  245. package/lib/capability/inventory.test.mjs +0 -232
  246. package/lib/capability.test.mjs +0 -78
  247. package/lib/channels/base-adapter.test.mjs +0 -590
  248. package/lib/channels/channels.test.mjs +0 -371
  249. package/lib/channels/contract.test.mjs +0 -162
  250. package/lib/channels/inbox-item.test.mjs +0 -368
  251. package/lib/channels/orgmail/adapter.test.mjs +0 -448
  252. package/lib/channels/pairing.test.mjs +0 -270
  253. package/lib/channels/repeat-suppressor.test.mjs +0 -134
  254. package/lib/channels/slack-adapter.test.mjs +0 -212
  255. package/lib/channels/telegram-adapter.test.mjs +0 -306
  256. package/lib/channels/voice/adapter.test.mjs +0 -278
  257. package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
  258. package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
  259. package/lib/charter.test.mjs +0 -89
  260. package/lib/claude-bin.test.mjs +0 -131
  261. package/lib/cli/board.test.mjs +0 -227
  262. package/lib/cli/design.test.mjs +0 -270
  263. package/lib/cli/doctor-checks.test.mjs +0 -336
  264. package/lib/cli/global-setup-extras.test.mjs +0 -462
  265. package/lib/cli/inbox.test.mjs +0 -230
  266. package/lib/cli/session-ack.test.mjs +0 -63
  267. package/lib/cli/session.test.mjs +0 -613
  268. package/lib/collective/capture.test.mjs +0 -121
  269. package/lib/collective/cards.test.mjs +0 -114
  270. package/lib/collective/config.test.mjs +0 -123
  271. package/lib/collective/global-config.test.mjs +0 -220
  272. package/lib/collective/global-skills.test.mjs +0 -126
  273. package/lib/collective/presence.test.mjs +0 -95
  274. package/lib/collective/recall.test.mjs +0 -116
  275. package/lib/collective/vendor-skills.test.mjs +0 -306
  276. package/lib/comms/send-gate.test.mjs +0 -770
  277. package/lib/comms.test.mjs +0 -41
  278. package/lib/cost/ledger-row.test.mjs +0 -183
  279. package/lib/design/design-md.test.mjs +0 -318
  280. package/lib/design/fixtures/DESIGN.golden.md +0 -238
  281. package/lib/design/fixtures/PRODUCT.golden.md +0 -67
  282. package/lib/design/fixtures/foundation.json +0 -133
  283. package/lib/design/refresh-gate.test.mjs +0 -144
  284. package/lib/design/write.test.mjs +0 -241
  285. package/lib/diagnostics/alerts.test.mjs +0 -318
  286. package/lib/diagnostics/backup-freshness.test.mjs +0 -185
  287. package/lib/diagnostics/counters.test.mjs +0 -206
  288. package/lib/diagnostics/events.test.mjs +0 -290
  289. package/lib/diagnostics/otel.test.mjs +0 -196
  290. package/lib/diagnostics/trace.test.mjs +0 -251
  291. package/lib/env-compat.test.mjs +0 -104
  292. package/lib/execution/disposition.test.mjs +0 -553
  293. package/lib/execution/drive.test.mjs +0 -270
  294. package/lib/execution/effects.test.mjs +0 -344
  295. package/lib/execution/intake.test.mjs +0 -389
  296. package/lib/execution/journal.test.mjs +0 -261
  297. package/lib/execution/match.test.mjs +0 -235
  298. package/lib/execution/pipeline.test.mjs +0 -392
  299. package/lib/execution/route.test.mjs +0 -186
  300. package/lib/execution/surface-policy.test.mjs +0 -162
  301. package/lib/fs-atomic.test.mjs +0 -72
  302. package/lib/fs-ownership.test.mjs +0 -158
  303. package/lib/goals/admission.test.mjs +0 -164
  304. package/lib/goals/classify.test.mjs +0 -167
  305. package/lib/goals/collaborate.test.mjs +0 -336
  306. package/lib/goals/gaps.test.mjs +0 -284
  307. package/lib/goals/loop.test.mjs +0 -845
  308. package/lib/hooks/bus.test.mjs +0 -387
  309. package/lib/identity/persona.test.mjs +0 -142
  310. package/lib/kpi-sensors.test.mjs +0 -278
  311. package/lib/kpi.test.mjs +0 -244
  312. package/lib/learning/config.test.mjs +0 -75
  313. package/lib/learning/counters.test.mjs +0 -69
  314. package/lib/learning/curator-consolidate.test.mjs +0 -238
  315. package/lib/learning/curator.test.mjs +0 -106
  316. package/lib/learning/reflect.test.mjs +0 -0
  317. package/lib/learning/session-index.test.mjs +0 -125
  318. package/lib/learning/skill-writer.test.mjs +0 -210
  319. package/lib/mandate/audit.test.mjs +0 -195
  320. package/lib/mandate/contract.test.mjs +0 -185
  321. package/lib/mandate/derive.test.mjs +0 -274
  322. package/lib/mandate/model.test.mjs +0 -164
  323. package/lib/mandate/refresh.test.mjs +0 -389
  324. package/lib/mcp/server.test.mjs +0 -426
  325. package/lib/model-router/auth-profiles.test.mjs +0 -580
  326. package/lib/model-router/catalog.test.mjs +0 -385
  327. package/lib/model-router/economics.test.mjs +0 -438
  328. package/lib/model-router/failover.test.mjs +0 -439
  329. package/lib/model-router/health.test.mjs +0 -338
  330. package/lib/model-router/integration-coverage.test.mjs +0 -831
  331. package/lib/model-router/integration.test.mjs +0 -564
  332. package/lib/model-router/ledger.test.mjs +0 -415
  333. package/lib/model-router/llm-task.test.mjs +0 -392
  334. package/lib/model-router/org-credentials.test.mjs +0 -265
  335. package/lib/model-router/pricing-refresh.test.mjs +0 -286
  336. package/lib/model-router/reconcile.test.mjs +0 -316
  337. package/lib/model-router/repair.test.mjs +0 -180
  338. package/lib/model-router/spawn.test.mjs +0 -446
  339. package/lib/model-router/taxonomy.test.mjs +0 -410
  340. package/lib/model-router.test.mjs +0 -1207
  341. package/lib/org/activity.test.mjs +0 -134
  342. package/lib/org/approvals.test.mjs +0 -216
  343. package/lib/org/awareness.test.mjs +0 -159
  344. package/lib/org/board-mine-cache.test.mjs +0 -53
  345. package/lib/org/board.test.mjs +0 -187
  346. package/lib/org/bootstrap-context.test.mjs +0 -153
  347. package/lib/org/client.test.mjs +0 -1206
  348. package/lib/org/cohort-client.test.mjs +0 -126
  349. package/lib/org/cost-sync.test.mjs +0 -153
  350. package/lib/org/doctor.test.mjs +0 -346
  351. package/lib/org/engagement-ledger.test.mjs +0 -112
  352. package/lib/org/engagement.test.mjs +0 -739
  353. package/lib/org/handoff.test.mjs +0 -269
  354. package/lib/org/inbound/directedness.test.mjs +0 -668
  355. package/lib/org/inbound/facts.test.mjs +0 -471
  356. package/lib/org/inbound/hydrate.test.mjs +0 -453
  357. package/lib/org/inbound/index.test.mjs +0 -429
  358. package/lib/org/inbound/project.test.mjs +0 -287
  359. package/lib/org/integration-tools.test.mjs +0 -160
  360. package/lib/org/keys.test.mjs +0 -92
  361. package/lib/org/knowledge.test.mjs +0 -326
  362. package/lib/org/leases.test.mjs +0 -235
  363. package/lib/org/mesh-directives.test.mjs +0 -110
  364. package/lib/org/mesh-integration.test.mjs +0 -127
  365. package/lib/org/mesh.test.mjs +0 -400
  366. package/lib/org/messaging.test.mjs +0 -471
  367. package/lib/org/param-contract.test.mjs +0 -477
  368. package/lib/org/policy.test.mjs +0 -237
  369. package/lib/org/protocol.checksum.test.mjs +0 -90
  370. package/lib/org/protocol.test.mjs +0 -323
  371. package/lib/org/push.test.mjs +0 -792
  372. package/lib/org/registry.test.mjs +0 -100
  373. package/lib/org/resource-tools.test.mjs +0 -361
  374. package/lib/org/tool-access.test.mjs +0 -144
  375. package/lib/org/tool-surface-integration.test.mjs +0 -120
  376. package/lib/org/tool-surface.test.mjs +0 -1268
  377. package/lib/org/typing.test.mjs +0 -291
  378. package/lib/org/ui-parity.test.mjs +0 -560
  379. package/lib/org/verify.test.mjs +0 -194
  380. package/lib/org/work-ledger.test.mjs +0 -273
  381. package/lib/plan/adoption-e2e.test.mjs +0 -366
  382. package/lib/plan/budget-enforcement.test.mjs +0 -400
  383. package/lib/plan/compile.test.mjs +0 -382
  384. package/lib/plan/emit.test.mjs +0 -269
  385. package/lib/plan/explain.test.mjs +0 -188
  386. package/lib/prompts/parallelism.test.mjs +0 -177
  387. package/lib/rag/rag.test.mjs +0 -505
  388. package/lib/rate-guard.test.mjs +0 -272
  389. package/lib/reactive-gate.test.mjs +0 -57
  390. package/lib/render.test.mjs +0 -68
  391. package/lib/resource-governor.test.mjs +0 -488
  392. package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
  393. package/lib/scheduling/jitter.test.mjs +0 -140
  394. package/lib/secrets/broker.test.mjs +0 -280
  395. package/lib/secrets/providers.test.mjs +0 -274
  396. package/lib/security/audit-engine.test.mjs +0 -424
  397. package/lib/security/coerce-args.test.mjs +0 -281
  398. package/lib/security/dangerous-tools.test.mjs +0 -68
  399. package/lib/security/external-content.test.mjs +0 -84
  400. package/lib/security/redact.test.mjs +0 -441
  401. package/lib/security/secret-equal.test.mjs +0 -55
  402. package/lib/session/config.test.mjs +0 -92
  403. package/lib/session/feed-core.test.mjs +0 -198
  404. package/lib/session/first-run.test.mjs +0 -121
  405. package/lib/session/frontdoor.test.mjs +0 -205
  406. package/lib/session/handoffs.test.mjs +0 -183
  407. package/lib/session/identity.test.mjs +0 -180
  408. package/lib/session/inbox-claims.test.mjs +0 -286
  409. package/lib/session/launch-args.test.mjs +0 -157
  410. package/lib/session/liveness.test.mjs +0 -100
  411. package/lib/session/status-summary.test.mjs +0 -118
  412. package/lib/session-permissions.test.mjs +0 -120
  413. package/lib/setup/claude-probe.test.mjs +0 -187
  414. package/lib/setup/completeness.test.mjs +0 -110
  415. package/lib/setup/context-pack.test.mjs +0 -89
  416. package/lib/setup/enrich.test.mjs +0 -115
  417. package/lib/setup/enroll-from-cohort.test.mjs +0 -300
  418. package/lib/setup/integration.test.mjs +0 -162
  419. package/lib/setup/io.test.mjs +0 -77
  420. package/lib/setup/runner.test.mjs +0 -132
  421. package/lib/setup/sections/identity.test.mjs +0 -234
  422. package/lib/setup/sections/inventory.test.mjs +0 -198
  423. package/lib/setup/sections/learning.test.mjs +0 -81
  424. package/lib/setup/sections/mandate.test.mjs +0 -388
  425. package/lib/setup/sections/messaging.test.mjs +0 -127
  426. package/lib/setup/sections/model.test.mjs +0 -240
  427. package/lib/setup/sections/org.test.mjs +0 -346
  428. package/lib/setup/sections/orgmail.test.mjs +0 -118
  429. package/lib/setup/sections/recovery.test.mjs +0 -98
  430. package/lib/setup/sections/subagents.test.mjs +0 -429
  431. package/lib/setup/sections/verify.test.mjs +0 -175
  432. package/lib/setup/sot.test.mjs +0 -81
  433. package/lib/setup/state.test.mjs +0 -115
  434. package/lib/singleton.test.mjs +0 -151
  435. package/lib/subagents/cli.test.mjs +0 -389
  436. package/lib/subagents/client.test.mjs +0 -309
  437. package/lib/subagents/gap.test.mjs +0 -234
  438. package/lib/subagents/lock.test.mjs +0 -248
  439. package/lib/subagents/manifest.test.mjs +0 -175
  440. package/lib/subagents/refs.test.mjs +0 -204
  441. package/lib/subagents/resolve.test.mjs +0 -422
  442. package/lib/subagents/schema.test.mjs +0 -328
  443. package/lib/telemetry/alerts.test.mjs +0 -109
  444. package/lib/telemetry/collect.test.mjs +0 -1274
  445. package/lib/tool-definitions-integration.test.mjs +0 -83
  446. package/lib/tool-definitions.test.mjs +0 -437
  447. package/lib/upgrade/global-refresh.test.mjs +0 -65
  448. package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
  449. package/lib/upgrade/post-steps.test.mjs +0 -200
  450. package/lib/upgrade/verify.test.mjs +0 -164
  451. package/lib/util/fetch-timeout.test.mjs +0 -202
  452. package/lib/util/reconnect.test.mjs +0 -369
  453. package/lib/util/unhandled.test.mjs +0 -216
  454. package/lib/voice/outbound.test.mjs +0 -69
  455. package/lib/voice/session-rotation.test.mjs +0 -114
  456. package/lib/voice/stt.test.mjs +0 -226
  457. package/lib/voice/voice.test.mjs +0 -990
  458. package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
  459. package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
  460. package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
  461. package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
  462. package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
  463. package/scripts/ci/check-skill-packs.test.mjs +0 -495
  464. package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
  465. package/scripts/ci/check.test.mjs +0 -194
  466. package/scripts/ci/conformance-org-api.test.mjs +0 -425
  467. package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
  468. package/scripts/collective/hook-runner.test.mjs +0 -173
  469. package/scripts/cost/fleet-digest.test.mjs +0 -207
  470. package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
  471. package/scripts/cost/track-claude-usage.test.mjs +0 -148
  472. package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
  473. package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
  474. package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
  475. package/scripts/daemon/agent-daemon.test.mjs +0 -995
  476. package/scripts/daemon/assurance-e2e.test.mjs +0 -613
  477. package/scripts/daemon/assurance.test.mjs +0 -1791
  478. package/scripts/daemon/board-mirror.test.mjs +0 -165
  479. package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
  480. package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
  481. package/scripts/daemon/cadence-consumer.test.mjs +0 -776
  482. package/scripts/daemon/cadence-handlers.test.mjs +0 -837
  483. package/scripts/daemon/classifier-identity.test.mjs +0 -137
  484. package/scripts/daemon/classifier.test.mjs +0 -266
  485. package/scripts/daemon/classify-kind.test.mjs +0 -40
  486. package/scripts/daemon/context-compiler.test.mjs +0 -300
  487. package/scripts/daemon/deliver.test.mjs +0 -564
  488. package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
  489. package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
  490. package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
  491. package/scripts/daemon/execution-ladder.test.mjs +0 -470
  492. package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
  493. package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
  494. package/scripts/daemon/inbox-deferral.test.mjs +0 -336
  495. package/scripts/daemon/inbox-wake.test.mjs +0 -199
  496. package/scripts/daemon/integration.test.mjs +0 -149
  497. package/scripts/daemon/lib/self-echo.test.mjs +0 -153
  498. package/scripts/daemon/lib/session-router.test.mjs +0 -295
  499. package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
  500. package/scripts/daemon/prompt-builder.test.mjs +0 -344
  501. package/scripts/daemon/responder-cost.test.mjs +0 -68
  502. package/scripts/daemon/responder-history.test.mjs +0 -185
  503. package/scripts/daemon/sdk-version.test.mjs +0 -31
  504. package/scripts/daemon/session-lock.test.mjs +0 -252
  505. package/scripts/daemon/session-outcomes.test.mjs +0 -533
  506. package/scripts/daemon/typing-registry.test.mjs +0 -102
  507. package/scripts/hooks/pre-send-audit.test.mjs +0 -354
  508. package/scripts/huddle/huddle-prompt.test.mjs +0 -176
  509. package/scripts/local-triggers/autoupdate.test.mjs +0 -518
  510. package/scripts/local-triggers/generate-plists.test.mjs +0 -456
  511. package/scripts/media-generation/brand-clause.test.mjs +0 -135
  512. package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
  513. package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
  514. package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
  515. package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
  516. package/scripts/poller/slack-socket-mode.test.mjs +0 -805
  517. package/scripts/poller-launchd/install.test.mjs +0 -243
  518. package/scripts/restore-from-backup.test.mjs +0 -181
  519. package/scripts/session/feed.test.mjs +0 -196
  520. package/scripts/session/supervisor-sh.test.mjs +0 -218
  521. package/scripts/session/supervisor.test.mjs +0 -482
  522. package/scripts/setup/configure-macos.test.mjs +0 -306
  523. package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
  524. package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
  525. package/scripts/setup/generate-capability.test.mjs +0 -134
  526. package/scripts/setup/init-agent.test.mjs +0 -370
  527. package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
  528. package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
  529. package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
@@ -1,995 +0,0 @@
1
- /**
2
- * agent-daemon.test.mjs — F1/H2 message-loss window (task #16, 0.8 follow-up).
3
- *
4
- * Before this fix, processItem() called markProcessed() (rename → .processed)
5
- * IMMEDIATELY after a fire-and-forget dispatch(). A crash before the spawned
6
- * session closed left the user's inbox item `.processed` with redelivery
7
- * blocked — the message was silently lost.
8
- *
9
- * The fix defers the finalize to the dispatch onClose SUCCESS path and records a
10
- * durable in-flight admission so a restart re-delivers an un-finalized item:
11
- * • clean close → markProcessed() + clearInFlight() (finalize)
12
- * • non-zero / spawn error → clearInFlight() + releaseLock() (re-deliverable)
13
- * • crash mid-flight → admission survives; recoverInFlight() (startup) releases
14
- * the per-item lock so the still-un-`.processed` file is re-polled.
15
- *
16
- * These tests drive the REAL daemon helpers (recordInFlight / clearInFlight /
17
- * markProcessed / recoverInFlight) and the REAL per-item lock from
18
- * session-lock.mjs, asserting the on-disk state that the poller would observe.
19
- *
20
- * The daemon module statically pulls in the agent-only `imapflow` dep (via the
21
- * gmail poller) which isn't packaged in CI, so we register a tiny in-process
22
- * ESM loader that stubs `imapflow` to a no-op class. Everything else in the
23
- * daemon's import graph resolves normally, so the code under test is the real
24
- * implementation, not a re-creation.
25
- */
26
-
27
- import { test, before } from "node:test";
28
- import assert from "node:assert/strict";
29
- import { register } from "node:module";
30
- import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, rmSync } from "node:fs";
31
- import { join } from "node:path";
32
- import { tmpdir } from "node:os";
33
-
34
- // --- stub the agent-only `imapflow` dep so the daemon module can be imported ---
35
- const loaderSrc = `
36
- export async function resolve(specifier, context, next) {
37
- if (specifier === "imapflow") return { url: "imapflow-stub:main", shortCircuit: true };
38
- return next(specifier, context);
39
- }
40
- export async function load(url, context, next) {
41
- if (url === "imapflow-stub:main") return { format: "module", shortCircuit: true, source: "export class ImapFlow {}" };
42
- return next(url, context);
43
- }
44
- `;
45
- register(`data:text/javascript,${encodeURIComponent(loaderSrc)}`);
46
-
47
- // One shared AGENT_DIR for the whole file: session-lock.mjs (imported transitively
48
- // by the daemon) reads AGENT_DIR at module-eval time and is cached by URL, so all
49
- // imports must agree on the dir. Per-test isolation comes from unique item ids and
50
- // a clean-between-tests of the relevant state subdirs.
51
- const AGENT_DIR = mkdtempSync(join(tmpdir(), "maestro-agent-daemon-"));
52
- process.env.AGENT_DIR = AGENT_DIR;
53
- process.env.MAESTRO_DAEMON_NO_AUTOSTART = "1"; // import without spinning the poll loops
54
-
55
- let daemon; // the real daemon module (recordInFlight/clearInFlight/markProcessed/recoverInFlight)
56
- let lock; // the real session-lock module (acquireLock/readLock)
57
- let bus; // the real hook-bus module (getHookBus/resetHookBus) — shared singleton
58
- let counters; // the real counters module (snapshot/resetMem) — shared transitive import
59
-
60
- before(async () => {
61
- mkdirSync(join(AGENT_DIR, "state", "inbox", "slack"), { recursive: true });
62
- mkdirSync(join(AGENT_DIR, "state", "sessions", "locks"), { recursive: true });
63
- const dURL = new URL("./agent-daemon.mjs", import.meta.url);
64
- daemon = await import(`${dURL.href}?f1h2=${Math.random()}`);
65
- lock = await import("./session-lock.mjs");
66
- // These are the SAME singletons the daemon reaches transitively (imported by
67
- // bare specifier, so the module cache shares them) — that's what lets the test
68
- // emit on the same bus the daemon subscribed to and read the same counters.
69
- bus = await import("../../lib/hooks/bus.mjs");
70
- counters = await import("../../lib/diagnostics/counters.mjs");
71
- });
72
-
73
- // Read the diagnostic-event rows the daemon appended to its tmp
74
- // logs/diagnostics/<UTC-date>.jsonl stream. [] when none written.
75
- function readDiagnostics() {
76
- const date = new Date().toISOString().slice(0, 10);
77
- const path = join(AGENT_DIR, "logs", "diagnostics", `${date}.jsonl`);
78
- let raw = "";
79
- try { raw = readFileSync(path, "utf-8"); } catch { return []; }
80
- return raw.split("\n").filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
81
- }
82
-
83
- function slackInbox() { return join(AGENT_DIR, "state", "inbox", "slack"); }
84
- function inflightDir() { return join(AGENT_DIR, "state", "inbox-inflight"); }
85
-
86
- function resetState() {
87
- for (const d of [slackInbox(), inflightDir(), join(AGENT_DIR, "state", "sessions", "locks")]) {
88
- try { for (const f of readdirSync(d)) rmSync(join(d, f), { force: true }); } catch { /* */ }
89
- }
90
- }
91
-
92
- /**
93
- * Write a live (un-.processed) poller-style inbox file for an item, returning
94
- * the filename. Mirrors the real poller naming (`<ts>-<item.id>.yaml`) so that
95
- * markProcessed's id/raw_ref substring match finds it, exactly as in production.
96
- */
97
- function writeLiveItem(item) {
98
- const name = `2026-06-10T00-00-00Z-${item.id}.yaml`;
99
- const body = [
100
- `id: "${item.id}"`,
101
- `service: "slack"`,
102
- `channel_id: "${item.channel || "D1"}"`,
103
- `timestamp: "2026-06-10T00:00:00Z"`,
104
- `content: |`,
105
- ` hello`,
106
- "",
107
- ].join("\n");
108
- writeFileSync(join(slackInbox(), name), body);
109
- return name;
110
- }
111
-
112
- function inboxFiles() { return readdirSync(slackInbox()).sort(); }
113
- function inflightFiles() { try { return readdirSync(inflightDir()).filter((f) => f.endsWith(".json")); } catch { return []; } }
114
-
115
- // ---------------------------------------------------------------------------
116
- // 1) Crash between dispatch and close → item is RE-DELIVERABLE (not .processed),
117
- // and recoverInFlight() releases the gating lock so the next poll re-delivers.
118
- // ---------------------------------------------------------------------------
119
-
120
- test("F1/H2: a crash between dispatch and close leaves the item re-deliverable (not .processed)", () => {
121
- resetState();
122
- const item = { id: "MSG-CRASH", raw_ref: "slack:C1:1001", channel: "D1", sender: "ceo" };
123
- const name = writeLiveItem(item);
124
-
125
- // Simulate processItem up to (but not past) the dispatch: the poll filter holds
126
- // the per-item lock, and the daemon records the durable in-flight admission.
127
- const acq = lock.acquireLock(item.raw_ref, { sender: item.sender, channel: item.channel });
128
- assert.equal(acq.acquired, true, "poll filter acquired the per-item lock");
129
- daemon.recordInFlight(item, "slack");
130
-
131
- // --- CRASH HERE: no onClose ran, so markProcessed was never called. ---
132
- // The user's message must NOT be .processed (the whole bug).
133
- assert.deepEqual(inboxFiles(), [name], "file is still live (un-.processed) after a mid-flight crash");
134
- assert.equal(inflightFiles().length, 1, "the in-flight admission survives the crash");
135
- // The held lock would block a naive re-poll until the 2h stale TTL — that is
136
- // exactly what recovery must clear.
137
- assert.equal(lock.acquireLock(item.raw_ref).acquired, false, "lock still held → re-poll blocked pre-recovery");
138
-
139
- // --- RESTART: recoverInFlight() releases the lock so the next poll re-delivers. ---
140
- const n = daemon.recoverInFlight();
141
- assert.equal(n, 1, "one in-flight admission recovered");
142
- assert.equal(inflightFiles().length, 0, "admission cleared after recovery");
143
- assert.deepEqual(inboxFiles(), [name], "the message is STILL on disk, un-.processed → re-deliverable");
144
- assert.equal(lock.acquireLock(item.raw_ref).acquired, true, "the gating lock was released → next poll re-delivers");
145
- });
146
-
147
- // ---------------------------------------------------------------------------
148
- // 2) A clean close finalizes the item (markProcessed) and clears the admission.
149
- // ---------------------------------------------------------------------------
150
-
151
- test("F1/H2: a clean close finalizes the item (markProcessed) and clears the in-flight admission", () => {
152
- resetState();
153
- const item = { id: "MSG-OK", raw_ref: "slack:C2:2002", channel: "D2", sender: "ceo" };
154
- const name = writeLiveItem(item);
155
- lock.acquireLock(item.raw_ref, { sender: item.sender });
156
- daemon.recordInFlight(item, "slack");
157
- assert.equal(inflightFiles().length, 1);
158
-
159
- // onClose({ok:true}) success path (the exact wiring processItem installs).
160
- daemon.markProcessed(item, "slack");
161
- daemon.clearInFlight(item);
162
-
163
- assert.deepEqual(inboxFiles(), [`${name}.processed`], "clean close finalized the item to .processed");
164
- assert.equal(inflightFiles().length, 0, "admission cleared on finalize");
165
- // A clean close finalized work that already completed — recovery is a no-op,
166
- // and there is no live file left for the poller to re-deliver.
167
- assert.equal(daemon.recoverInFlight(), 0, "nothing to recover after a clean finalize");
168
- });
169
-
170
- // ---------------------------------------------------------------------------
171
- // 3) A non-zero close / spawn error leaves the item re-deliverable and releases
172
- // the lock immediately (no need to wait for restart).
173
- // ---------------------------------------------------------------------------
174
-
175
- test("F1/H2: a failed close leaves the item re-deliverable and releases the lock now", () => {
176
- resetState();
177
- const item = { id: "MSG-FAIL", raw_ref: "slack:C3:3003", channel: "D3", sender: "ceo" };
178
- const name = writeLiveItem(item);
179
- lock.acquireLock(item.raw_ref, { sender: item.sender });
180
- daemon.recordInFlight(item, "slack");
181
-
182
- // onClose({ok:false}) failure path: clear admission + release lock, but DO NOT
183
- // markProcessed — the message stays re-deliverable.
184
- daemon.clearInFlight(item);
185
- lock.releaseLock(item.raw_ref);
186
-
187
- assert.deepEqual(inboxFiles(), [name], "failed session left the item live (un-.processed)");
188
- assert.equal(inflightFiles().length, 0, "admission cleared on failure");
189
- assert.equal(lock.acquireLock(item.raw_ref).acquired, true, "lock released → next poll re-delivers immediately");
190
- });
191
-
192
- // ---------------------------------------------------------------------------
193
- // 4) recoverInFlight() is a clean no-op when nothing is pending, and never
194
- // touches already-.processed files.
195
- // ---------------------------------------------------------------------------
196
-
197
- test("F1/H2: recoverInFlight is a no-op with no pending admissions", () => {
198
- resetState();
199
- assert.equal(daemon.recoverInFlight(), 0, "no admissions → recovered 0");
200
- });
201
-
202
- test("F1/H2: recordInFlight is a no-op for an item with no lock key (e.g. backlog)", () => {
203
- resetState();
204
- daemon.recordInFlight({ title: "deep work" }, "backlog");
205
- assert.equal(inflightFiles().length, 0, "no raw_ref/id → no admission written (backlog items are durable in queue files)");
206
- });
207
-
208
- // ===========================================================================
209
- // DIAG — the observability spine wired into the LIVE inbound pipeline.
210
- // ===========================================================================
211
- //
212
- // processItem() mints ONE trace_id per item and emits item_received → … → sent
213
- // (on a clean finalize) all under it. The daemon also registers the ONE real
214
- // hook-bus subscriber (onGuardFail → counters.bump("guard.fail")) so the bus is
215
- // live. STRICT additive: emitEvent + counters.bump never throw, and the dispatch
216
- // /classify/send semantics are untouched (the heavy collaborators are injected
217
- // here ONLY so the test drives the real instrumented body without spawning a
218
- // `claude` child — production passes no deps).
219
-
220
- function clearDiagnostics() {
221
- for (const sub of ["diagnostics", join("diagnostics", "counters")]) {
222
- const d = join(AGENT_DIR, "logs", sub);
223
- try { for (const f of readdirSync(d)) { if (f.endsWith(".jsonl")) rmSync(join(d, f), { force: true }); } } catch { /* */ }
224
- }
225
- try { counters.resetMem(); } catch { /* */ }
226
- }
227
-
228
- // ---------------------------------------------------------------------------
229
- // 1) A processed item emits item_received → … → sent with ONE stable trace_id.
230
- // ---------------------------------------------------------------------------
231
-
232
- test("DIAG: a processed item emits item_received → sent with ONE stable trace_id (the daemon's minted id)", async () => {
233
- resetState();
234
- clearDiagnostics();
235
- // A DM (channel_id starts with "D") so it always passes the directed-message
236
- // gate — keeps the test on the dispatch path deterministically.
237
- const item = { id: "MSG-TRACE", raw_ref: "slack:D1:1", channel: "dm/ceo", channel_id: "D1", sender: "ceo", content: "please draft the board memo" };
238
- writeLiveItem(item);
239
-
240
- // Inject the heavy collaborators so the REAL processItem body runs (trace mint,
241
- // item_received emit, dispatch onClose → sent emit) without a claude spawn:
242
- // - classify → "respond"/opus so it takes the holding+session (dispatch) path,
243
- // NOT the quick-reply path.
244
- // - isQuickReply → false to keep it on the dispatch path deterministically.
245
- // - sendHoldingMessage → no-op (no Slack post).
246
- // - dispatch → synchronously invoke onClose({ok:true}) to finalize.
247
- let dispatchedItem = null;
248
- await daemon.processItem(item, "slack", {
249
- classify: async () => ({ priority: "critical", action: "respond", model: "opus", summary: "draft board memo", category: "action_required", directed_at_agent: true }),
250
- isQuickReply: () => false,
251
- sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
252
- dispatch: (_prompt, it, _cr, _src, opts) => { dispatchedItem = it; opts.onClose({ ok: true, code: 0 }); },
253
- });
254
-
255
- // The daemon set the minted id on the item and carried it onto the dispatch.
256
- assert.ok(typeof item.trace_id === "string" && item.trace_id.length === 26, "a 26-char ULID trace_id was minted onto the item");
257
- assert.equal(dispatchedItem.trace_id, item.trace_id, "the dispatch received the SAME trace_id");
258
-
259
- const rows = readDiagnostics();
260
- const received = rows.filter((r) => r.type === "item_received");
261
- const sent = rows.filter((r) => r.type === "sent");
262
- assert.equal(received.length, 1, "exactly one item_received");
263
- assert.equal(sent.length, 1, "exactly one sent on clean finalize");
264
- // The single-trace-id invariant: the first hop and the last hop share one id.
265
- assert.equal(received[0].trace_id, item.trace_id, "item_received carries the minted trace_id");
266
- assert.equal(sent[0].trace_id, item.trace_id, "sent carries the SAME trace_id (end-to-end correlation)");
267
- // The in-flight admission also carries the trace (cheap ledger correlation).
268
- assert.equal(sent[0].attrs.item_id, item.raw_ref, "sent records the item id");
269
- });
270
-
271
- test("DIAG: a failed (ok:false) finalize emits error (not sent), still under the one trace_id", async () => {
272
- resetState();
273
- clearDiagnostics();
274
- const item = { id: "MSG-TRACE-FAIL", raw_ref: "slack:D2:1", channel: "dm/ceo", channel_id: "D2", sender: "ceo", content: "do the thing" };
275
- writeLiveItem(item);
276
-
277
- await daemon.processItem(item, "slack", {
278
- classify: async () => ({ priority: "high", action: "respond", model: "opus", summary: "x", category: "action_required", directed_at_agent: true }),
279
- isQuickReply: () => false,
280
- sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
281
- dispatch: (_p, _it, _cr, _s, opts) => opts.onClose({ ok: false, code: 1 }),
282
- });
283
-
284
- const rows = readDiagnostics();
285
- assert.equal(rows.filter((r) => r.type === "item_received").length, 1, "item_received emitted");
286
- assert.equal(rows.filter((r) => r.type === "sent").length, 0, "a failed finalize must NOT emit sent");
287
- const err = rows.filter((r) => r.type === "error");
288
- assert.equal(err.length, 1, "a failed finalize emits an error hop");
289
- assert.equal(err[0].trace_id, item.trace_id, "the error hop is under the same interaction trace_id");
290
- });
291
-
292
- // ---------------------------------------------------------------------------
293
- // 2) A send-gate block bumps the counter (responder's real instrumented path).
294
- // ---------------------------------------------------------------------------
295
-
296
- test("DIAG: a send-gate block bumps the send.blocked counter (responder validation path)", async () => {
297
- clearDiagnostics();
298
- const responder = await import("./responder.mjs");
299
- // Force the generator to return text the REAL validateQuickReply blocks (AI
300
- // self-disclosure) — no claude spawn, no Slack post. The block path must bump.
301
- const restore = responder.setGenerateResponseForTests(async () => "Hi — I am an AI assistant, happy to help.");
302
- try {
303
- const before = (counters.snapshot()["send.blocked"]) || 0;
304
- const res = await responder.sendQuickResponse(
305
- { sender: "ceo", service: "slack", channel: "D1", content: "hello" },
306
- { priority: "normal", action: "respond", model: "sonnet", summary: "hi" }
307
- );
308
- assert.equal(res.sent, false, "the AI-disclosure reply was blocked, not sent");
309
- assert.equal(res.blocked, true);
310
- const after = (counters.snapshot()["send.blocked"]) || 0;
311
- assert.equal(after, before + 1, "the send-gate block bumped send.blocked by exactly one (durable JSONL)");
312
- } finally { restore(); }
313
- });
314
-
315
- // ---------------------------------------------------------------------------
316
- // 3) The daemon's ONE real onGuardFail subscriber fires (the bus is live).
317
- // ---------------------------------------------------------------------------
318
-
319
- test("DIAG: registerHookSubscribers wires a real onGuardFail subscriber that bumps guard.fail", async () => {
320
- clearDiagnostics();
321
- // The daemon registers exactly one onGuardFail subscriber. It's idempotent, so
322
- // calling it here (the test driver) is safe even though main() also would.
323
- assert.equal(daemon.registerHookSubscribers(), true, "subscriber registered");
324
- const hb = bus.getHookBus();
325
- assert.ok(hb.count("onGuardFail") >= 1, "the bus has a live onGuardFail subscriber (not dead)");
326
-
327
- const before = (counters.snapshot()["guard.fail"]) || 0;
328
- // Emit a guard-fail (as the responder's send-gate fail-open path does). The
329
- // real subscriber must fire and bump the durable counter.
330
- const decision = await hb.emit("onGuardFail", { guard: "send_gate", reason: "policy unreadable", failed_open: true });
331
- assert.equal(decision.cancelled, false, "onGuardFail is observe-only (never cancels)");
332
- assert.equal(decision.errors, 0, "the subscriber did not throw");
333
-
334
- const after = (counters.snapshot()["guard.fail"]) || 0;
335
- assert.equal(after, before + 1, "the onGuardFail subscriber bumped guard.fail by exactly one");
336
- });
337
-
338
- test("DIAG: the responder's send-gate fail-open emits onGuardFail onto the live bus → guard.fail bumps", async () => {
339
- clearDiagnostics();
340
- daemon.registerHookSubscribers(); // ensure the real subscriber is live
341
- const responder = await import("./responder.mjs");
342
- // Generate benign text (passes validation), but force the Slack send-gate
343
- // screen to throw a NON-block infra error so the responder fails OPEN and
344
- // emits onGuardFail. We can't reach sendSlackMessage without a token, so we
345
- // assert the wiring via the bus directly using the same emit the responder
346
- // makes — the prior test proves the subscriber; here we prove a real producer
347
- // shape lands on the same singleton bus the daemon subscribed to.
348
- const hb = bus.getHookBus();
349
- const before = (counters.snapshot()["guard.fail"]) || 0;
350
- await hb.emit("onGuardFail", { guard: "send_gate", channel: "slack", recipient: "D1", reason: "gate module unreadable", failed_open: true });
351
- const after = (counters.snapshot()["guard.fail"]) || 0;
352
- assert.equal(after, before + 1, "a send-gate fail-open shape bumps guard.fail through the live subscriber");
353
- // Tidy: drop the test reference so it doesn't leak the import.
354
- void responder;
355
- });
356
-
357
- // ---------------------------------------------------------------------------
358
- // Post-session org-memory write-back (SP4) — after a clean turn, a bounded
359
- // distillation is authored to the shared store via remember(). Fail-open when
360
- // the org integration is disabled; never throws.
361
- // ---------------------------------------------------------------------------
362
-
363
- const ENABLED_ORG_CFG = { org: { cohort: { enabled: true, base: "https://org.example" } } };
364
-
365
- test("MEMORY: sessionDistillation produces a bounded one-line record", () => {
366
- const line = daemon.sessionDistillation(
367
- { sender: "Alice Smith", subject: "Q2 renewal" },
368
- { action: "respond", summary: "Acme renewal terms and next steps" },
369
- { agent: { firstName: "Jordan" } }
370
- );
371
- assert.ok(line, "should produce a distillation");
372
- assert.ok(line.includes("Jordan"), "names the agent");
373
- assert.ok(line.includes("respond"), "names the action");
374
- assert.ok(line.includes("Alice Smith"), "names the sender");
375
- assert.ok(line.includes("Acme renewal"), "carries the topic");
376
- assert.ok(line.length <= 401, `bounded length, got ${line.length}`);
377
- });
378
-
379
- test("MEMORY: sessionDistillation returns null when there is nothing to remember", () => {
380
- const line = daemon.sessionDistillation({}, {}, { agent: { firstName: "Jordan" } });
381
- assert.equal(line, null, "no sender + no topic ⇒ null");
382
- });
383
-
384
- test("MEMORY: writeBackSession authors to the store when the org integration is enabled", async () => {
385
- const calls = [];
386
- const res = await daemon.writeBackSession(
387
- { sender: "Alice Smith", subject: "Q2 renewal" },
388
- { action: "respond", summary: "Acme renewal terms" },
389
- {
390
- cfg: ENABLED_ORG_CFG,
391
- agent: { firstName: "Jordan" },
392
- sessionId: "sess-xyz",
393
- rememberImpl: async (text, opts) => { calls.push({ text, opts }); return { ok: true, mode: "append", id: "k1" }; },
394
- }
395
- );
396
- assert.equal(res.ok, true, "write reported ok");
397
- assert.equal(calls.length, 1, "remember() called exactly once");
398
- assert.ok(calls[0].text.includes("Jordan"), "authored the distillation text");
399
- assert.equal(calls[0].opts.cfg, ENABLED_ORG_CFG, "passed the org cfg through");
400
- assert.equal(calls[0].opts.provenance.sessionId, "sess-xyz", "carried session provenance");
401
- });
402
-
403
- test("MEMORY: writeBackSession is a fail-open no-op when the org integration is disabled", async () => {
404
- let called = false;
405
- const res = await daemon.writeBackSession(
406
- { sender: "Alice Smith" },
407
- { action: "respond", summary: "x" },
408
- { cfg: {}, rememberImpl: async () => { called = true; return { ok: true }; } }
409
- );
410
- assert.equal(res.skipped, true, "skipped when disabled");
411
- assert.equal(called, false, "remember() not called when disabled");
412
- });
413
-
414
- test("MEMORY: writeBackSession never throws even if remember() rejects", async () => {
415
- const res = await daemon.writeBackSession(
416
- { sender: "Alice Smith", subject: "s" },
417
- { action: "respond", summary: "y" },
418
- { cfg: ENABLED_ORG_CFG, rememberImpl: async () => { throw new Error("network down"); } }
419
- );
420
- assert.equal(res.ok, false, "fail-open result");
421
- assert.equal(res.skipped, true, "reported skipped on throw");
422
- });
423
-
424
- // ---------------------------------------------------------------------------
425
- // Post-session OUTCOME DISCIPLINE — the daemon's parity with hq's meeting chair.
426
- // A turn that committed to next steps must not leave them in a session log; the
427
- // close path lands them through the org protocol. OFF unless the flag is set.
428
- // The parsing/landing itself is covered by session-outcomes.test.mjs — these
429
- // tests assert the DAEMON's wiring: identity + config injection, and fail-open.
430
- // ---------------------------------------------------------------------------
431
-
432
- test("OUTCOMES: the daemon hook lands a turn's commitments through the org protocol", async () => {
433
- const created = [];
434
- const remembered = [];
435
- const res = await daemon.sweepOutcomesForSession({
436
- sessionId: "s-daemon-1",
437
- resultText: "Handled it.\n\nOUTCOMES:\n- Send the revised deck to Alice — due: Monday\n",
438
- enabled: true,
439
- cfg: ENABLED_ORG_CFG,
440
- agent: { firstName: "Robin" },
441
- item: {
442
- service: "slack",
443
- channel: "general",
444
- channel_id: "C0GENERAL",
445
- // The PUBLIC signal. A `C…` id alone proves nothing — Slack mints them
446
- // for private channels too — so the gate needs the conversation type.
447
- channel_type: "channel",
448
- sender: "Alice Smith",
449
- subject: "Deck review",
450
- },
451
- classResult: { action: "respond", summary: "deck review" },
452
- createItemImpl: async (item) => { created.push(item); return { ok: true }; },
453
- rememberImpl: async (text, opts) => { remembered.push({ text, opts }); return { ok: true }; },
454
- });
455
- assert.equal(res.created, 1, "the commitment became a board item");
456
- assert.equal(created[0].title, "Send the revised deck to Alice");
457
- assert.ok(created[0].detail.includes("Robin"), "the daemon supplied the agent identity");
458
- assert.equal(remembered.length, 1, "and one knowledge episode records the set");
459
- });
460
-
461
- test("OUTCOMES: a private DM's commitments never leave the DM", async () => {
462
- // CONFIDENTIALITY. A landed outcome is an ORG-VISIBLE artifact: board.create
463
- // with no workstreamId is channel-less, and the episode's knowledge.append
464
- // carries no group (so hq resolves it to the `org` group). Landing a DM's
465
- // commitments would republish a one-to-one conversation — including its topic
466
- // line — to every member of the org. The gate is fail-closed.
467
- let touched = false;
468
- const res = await daemon.sweepOutcomesForSession({
469
- sessionId: "s-daemon-dm",
470
- resultText: "OUTCOMES:\n- Send Alice the confidential term sheet — due: Monday\n",
471
- enabled: true,
472
- cfg: ENABLED_ORG_CFG,
473
- agent: { firstName: "Robin" },
474
- item: {
475
- service: "slack",
476
- channel: "dm/alice-smith",
477
- channel_id: "D01ALICE",
478
- is_dm: true,
479
- sender: "Alice Smith",
480
- subject: "DM from Alice Smith",
481
- },
482
- classResult: { action: "respond", summary: "confidential term sheet" },
483
- createItemImpl: async () => { touched = true; return { ok: true }; },
484
- rememberImpl: async () => { touched = true; return { ok: true }; },
485
- });
486
- assert.equal(res.created, 0);
487
- assert.equal(res.reason, "source-dm");
488
- assert.equal(touched, false, "no board row and no episode for a DM turn");
489
- });
490
-
491
- test("OUTCOMES: the daemon hook is OFF unless DAEMON_SESSION_OUTCOMES=1", async () => {
492
- let touched = false;
493
- const prior = process.env.DAEMON_SESSION_OUTCOMES;
494
- try {
495
- delete process.env.DAEMON_SESSION_OUTCOMES;
496
- const res = await daemon.sweepOutcomesForSession({
497
- sessionId: "s-daemon-2",
498
- resultText: "OUTCOMES:\n- Do the thing",
499
- cfg: ENABLED_ORG_CFG,
500
- agent: { firstName: "Robin" },
501
- createItemImpl: async () => { touched = true; return { ok: true }; },
502
- rememberImpl: async () => { touched = true; return { ok: true }; },
503
- });
504
- assert.equal(res.skipped, true);
505
- assert.equal(res.reason, "disabled");
506
- assert.equal(touched, false, "nothing is written while the flag is off");
507
- } finally {
508
- if (prior === undefined) delete process.env.DAEMON_SESSION_OUTCOMES;
509
- else process.env.DAEMON_SESSION_OUTCOMES = prior;
510
- }
511
- });
512
-
513
- test("OUTCOMES: the daemon hook never throws into the dispatch path", async () => {
514
- const res = await daemon.sweepOutcomesForSession({
515
- sessionId: "s-daemon-3",
516
- stdout: "{{{ not json",
517
- enabled: true,
518
- cfg: ENABLED_ORG_CFG,
519
- agent: { firstName: "Robin" },
520
- createItemImpl: async () => { throw new Error("network down"); },
521
- rememberImpl: async () => { throw new Error("network down"); },
522
- });
523
- assert.ok(res && typeof res.created === "number", "benign result, no throw");
524
- assert.equal(res.created, 0);
525
- });
526
-
527
- // ===========================================================================
528
- // ELECTION — the multi-AI storm fix. An UNDIRECTED message in a cohort channel
529
- // defers the should-I-respond decision to hq's electResponder; this seat
530
- // answers IFF elected, and a failed/empty election FAILS SAFE (stays silent).
531
- // A DIRECT address (@mention / DM) skips the election entirely. And a holding
532
- // ack is never posted when the reply session cannot run (claude unavailable).
533
- //
534
- // These drive the REAL answerItem() with the hq election call + the claude
535
- // liveness probe stubbed via deps — the same injection seam the DIAG tests use.
536
- // ===========================================================================
537
-
538
- // An org config that configFromAgent() reads: enabled, with this seat's own
539
- // member slug so the verdict can tell "elected me" from "elected someone else".
540
- const ELECTION_ORG_CFG = {
541
- org: { cohort: { enabled: true, base: "https://hq.example", orgId: "org_x", agentId: "robin" } },
542
- };
543
-
544
- // A cohort CHANNEL item (not a DM): service+channel_id give cohortChannelId,
545
- // raw_ref's third segment gives cohortMessageId. Content is deliberately generic
546
- // so isDirectedAtAgent() is false and the election path is taken.
547
- function cohortChannelItem(overrides = {}) {
548
- return {
549
- id: overrides.id || "MSG-EL",
550
- raw_ref: overrides.raw_ref || "cohort:message:msg-el:0",
551
- service: "cohort",
552
- channel: "channel/roadmap",
553
- channel_id: overrides.channel_id || "chan-el",
554
- is_dm: false,
555
- sender: "alice",
556
- content: overrides.content || "what should our Q3 priorities be?",
557
- };
558
- }
559
-
560
- const ELECTED_CLASS = {
561
- priority: "high", action: "respond", model: "opus",
562
- summary: "roadmap discussion", category: "action_required", directed_at_agent: false,
563
- };
564
-
565
- test("ELECTION: the elected seat responds (dispatches) and the hq call carried {messageId, channelId}", async () => {
566
- resetState();
567
- delete process.env.COHORT_AGENT_ID; // let the cfg's agentId be the seat's slug
568
- const cls = await import("./classifier.mjs");
569
- cls._resetAgentRegistry();
570
-
571
- const item = cohortChannelItem({ id: "MSG-EL-YES", raw_ref: "cohort:message:msg-yes:0", channel_id: "chan-yes" });
572
- const electionCalls = [];
573
- let dispatched = false;
574
-
575
- const res = await daemon.answerItem(item, "cohort", item.raw_ref, "trace-el-yes", {
576
- classify: async () => ({ ...ELECTED_CLASS, summary: "elected-yes topic" }),
577
- isQuickReply: () => false,
578
- sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
579
- dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
580
- claudeAvailable: () => true,
581
- orgCfg: ELECTION_ORG_CFG,
582
- electResponder: async (params) => {
583
- electionCalls.push(params);
584
- return { ok: true, result: { election: "group", mode: "single", responders: [{ slug: "robin", dimension: "mandate" }], deferredToHuman: false } };
585
- },
586
- });
587
-
588
- assert.equal(electionCalls.length, 1, "the election was consulted exactly once");
589
- assert.deepEqual(electionCalls[0], { messageId: "msg-yes", channelId: "chan-yes" }, "hq got the message + channel identity");
590
- assert.equal(dispatched, true, "the elected seat proceeds to dispatch a reply session");
591
- assert.equal(res.path, "session", "answerItem took the session path");
592
- });
593
-
594
- test("ELECTION: a NON-elected seat stays silent on the same message (no dispatch)", async () => {
595
- resetState();
596
- delete process.env.COHORT_AGENT_ID;
597
- const cls = await import("./classifier.mjs");
598
- cls._resetAgentRegistry();
599
-
600
- const item = cohortChannelItem({ id: "MSG-EL-NO", raw_ref: "cohort:message:msg-no:0", channel_id: "chan-no" });
601
- const electionCalls = [];
602
- let dispatched = false;
603
- let acked = false;
604
-
605
- const res = await daemon.answerItem(item, "cohort", item.raw_ref, "trace-el-no", {
606
- classify: async () => ({ ...ELECTED_CLASS, summary: "not-elected topic" }),
607
- isQuickReply: () => false,
608
- sendHoldingMessage: async () => { acked = true; return { sent: true, holdingText: "on it" }; },
609
- dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
610
- claudeAvailable: () => true,
611
- orgCfg: ELECTION_ORG_CFG,
612
- // hq elected a DIFFERENT seat — this one is not in the responders list.
613
- electResponder: async (params) => {
614
- electionCalls.push(params);
615
- return { ok: true, result: { election: "group", mode: "single", responders: [{ slug: "kai", dimension: "mandate" }], deferredToHuman: false } };
616
- },
617
- });
618
-
619
- assert.equal(electionCalls.length, 1, "the election was consulted");
620
- assert.equal(dispatched, false, "a non-elected seat must NOT dispatch");
621
- assert.equal(acked, false, "a non-elected seat must NOT post a holding ack");
622
- assert.equal(res.path, "filtered", "answerItem filtered the item");
623
- assert.equal(res.reason, "not_elected", "…because it was not elected");
624
- });
625
-
626
- test("ELECTION: an election ERROR fails safe — the seat stays silent (never respond-to-all)", async () => {
627
- resetState();
628
- delete process.env.COHORT_AGENT_ID;
629
- const cls = await import("./classifier.mjs");
630
- cls._resetAgentRegistry();
631
-
632
- const item = cohortChannelItem({ id: "MSG-EL-ERR", raw_ref: "cohort:message:msg-err:0", channel_id: "chan-err" });
633
- let dispatched = false;
634
-
635
- const res = await daemon.answerItem(item, "cohort", item.raw_ref, "trace-el-err", {
636
- classify: async () => ({ ...ELECTED_CLASS, summary: "election-error topic" }),
637
- isQuickReply: () => false,
638
- sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
639
- dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
640
- claudeAvailable: () => true,
641
- orgCfg: ELECTION_ORG_CFG,
642
- // hq is unreachable / throwing — the INVERSION: this must resolve to silence.
643
- electResponder: async () => { throw new Error("hq unreachable"); },
644
- });
645
-
646
- assert.equal(dispatched, false, "an unreachable election must NOT fall back to responding");
647
- assert.equal(res.path, "filtered", "answerItem filtered the item");
648
- assert.equal(res.reason, "not_elected", "election failure ⇒ treated as not-elected (silent)");
649
- });
650
-
651
- test("ELECTION: an @mention/named-address responds WITHOUT calling electResponder (fast path)", async () => {
652
- resetState();
653
- delete process.env.COHORT_AGENT_ID;
654
- // Give this seat a distinctive name and point isDirectedAtAgent at it.
655
- mkdirSync(join(AGENT_DIR, "config"), { recursive: true });
656
- writeFileSync(join(AGENT_DIR, "config", "agent.json"), JSON.stringify({ firstName: "Zephyr", slackMemberId: "" }));
657
- const cls = await import("./classifier.mjs");
658
- cls._resetAgentRegistry();
659
-
660
- const item = cohortChannelItem({
661
- id: "MSG-EL-MENTION", raw_ref: "cohort:message:msg-mention:0", channel_id: "chan-mention",
662
- content: "hey zephyr, can you own the Q3 rollout?",
663
- });
664
- const electionCalls = [];
665
- let dispatched = false;
666
-
667
- const res = await daemon.answerItem(item, "cohort", item.raw_ref, "trace-el-mention", {
668
- classify: async () => ({ ...ELECTED_CLASS, summary: "mention topic", directed_at_agent: true }),
669
- isQuickReply: () => false,
670
- sendHoldingMessage: async () => ({ sent: false, holdingText: null }),
671
- dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
672
- claudeAvailable: () => true,
673
- orgCfg: ELECTION_ORG_CFG,
674
- electResponder: async (params) => { electionCalls.push(params); return { ok: true, result: { responders: [] } }; },
675
- });
676
-
677
- assert.equal(electionCalls.length, 0, "a directly-addressed message does NOT hold an election");
678
- assert.equal(dispatched, true, "the addressed seat answers on its local fast path");
679
- assert.equal(res.path, "session", "answerItem took the session path");
680
-
681
- // Tidy: drop the identity so later tests re-read a nameless seat.
682
- rmSync(join(AGENT_DIR, "config", "agent.json"), { force: true });
683
- cls._resetAgentRegistry();
684
- });
685
-
686
- test("FAIL-SAFE: an ask the reply session cannot answer is never promised one (claude unavailable)", async () => {
687
- resetState();
688
- // A slack DM — always directed, so this isolates the ack gate from the election.
689
- // is_dm is normally set by enrichItem; answerItem is called directly here, so
690
- // set it on the item as the poller/enrichment would.
691
- // Distinct channel + subject per run so the request-claim from one run does not
692
- // deny the other (the claim key is recipient + subject + action_type).
693
- //
694
- // WHAT THIS PINS SINCE 2026-09-12. Nothing is said at open time on any path
695
- // any more — `openAndAcknowledge` writes the debt and returns — so the old
696
- // control ("with claude available a holding ack IS attempted") no longer
697
- // describes the system. The fail-safe itself is unchanged and is what this
698
- // asserts: when the CLI that would answer cannot start, the debt is opened
699
- // with the interim FORBIDDEN on the record, so no sweep in any later tick or
700
- // process can post a promise the seat cannot keep.
701
- const assurance = await import("./assurance.mjs");
702
- const mkItem = (tag) => ({ id: `MSG-ACK-${tag}`, raw_ref: `slack:DACK${tag}:1`, service: "slack", channel: "dm/ceo", channel_id: `DACK${tag}0001`, is_dm: true, sender: "ceo", content: "please draft the memo" });
703
- const baseDeps = (spy, claudeUp, tag) => ({
704
- classify: async () => ({ priority: "critical", action: "respond", model: "opus", summary: `draft memo ${tag}`, category: "action_required", directed_at_agent: true }),
705
- isQuickReply: () => false,
706
- sendHoldingMessage: async () => { spy.calls++; return { sent: true, holdingText: "on it" }; },
707
- dispatch: (_p, _it, _cr, _s, opts) => { spy.dispatched = true; opts.onClose({ ok: true, code: 0 }); },
708
- claudeAvailable: () => claudeUp,
709
- });
710
-
711
- // CONTROL: claude available ⇒ the debt is opened and an interim is PERMITTED
712
- // later, by the sweep, after ACK_AFTER_MS and inside the room's budget.
713
- const up = { calls: 0, dispatched: false };
714
- await daemon.answerItem(mkItem("UP"), "slack", "slack:DACKUP:1", "trace-ack-up", baseDeps(up, true, "UP"));
715
- assert.equal(up.calls, 0, "nothing is said at open time on ANY path — the interim belongs to the sweep");
716
- assert.equal(up.dispatched, true, "control: the session is dispatched");
717
- const upRec = assurance.readObligation("slack:DACKUP:1");
718
- assert.ok(upRec, "the debt exists");
719
- assert.notEqual(upRec.interimForbidden, true, "and an interim is permitted for it");
720
-
721
- // TREATMENT: claude unavailable ⇒ the interim is forbidden on the record, but
722
- // the session still runs (the assurance sweep owns the outcome; the human
723
- // just isn't promised a reply the seat cannot produce).
724
- const down = { calls: 0, dispatched: false };
725
- const res = await daemon.answerItem(mkItem("DN"), "slack", "slack:DACKDN:1", "trace-ack-down", baseDeps(down, false, "DN"));
726
- assert.equal(down.calls, 0, "with claude unavailable, no interim is ever posted for this ask");
727
- assert.equal(down.dispatched, true, "the session is still dispatched");
728
- assert.equal(res.path, "session", "answerItem took the session path");
729
- const downRec = assurance.readObligation("slack:DACKDN:1");
730
- assert.ok(downRec, "the debt is opened all the same — an ask we cannot promise is exactly the one an operator must see");
731
- assert.equal(downRec.interimForbidden, true, "durable on the record, because the sweep runs in another tick");
732
- });
733
-
734
- // ===========================================================================
735
- // is_dm END-TO-END — the Cohort channel_kind is the authoritative signal.
736
- // ===========================================================================
737
-
738
- test("enrichItem: Cohort channel_kind decides is_dm (DM yes, GROUP_DM no)", () => {
739
- // A 1:1 DM skips the election gate; a GROUP_DM is multi-participant and must
740
- // go through it. Cohort ids are cuids, so the kind is the only reliable signal.
741
- const dm = daemon.enrichItem({ channel: "channel/x", channel_id: "cuid_abc", channel_kind: "DM" }, "cohort");
742
- assert.equal(dm.is_dm, true, "channel_kind DM → is_dm true (skips election)");
743
- const group = daemon.enrichItem({ channel: "channel/x", channel_id: "cuid_abc", channel_kind: "GROUP_DM" }, "cohort");
744
- assert.equal(group.is_dm, false, "channel_kind GROUP_DM → is_dm false (goes through election)");
745
- const pub = daemon.enrichItem({ channel: "channel/x", channel_id: "cuid_abc", channel_kind: "public" }, "cohort");
746
- assert.equal(pub.is_dm, false, "any other explicit kind → not a 1:1 DM");
747
- // No kind → fall back to the Slack-style id/label prefix heuristics.
748
- assert.equal(daemon.enrichItem({ channel: "dm/ceo", channel_id: "D123" }, "slack").is_dm, true);
749
- assert.equal(daemon.enrichItem({ channel: "channel/roadmap", channel_id: "C123" }, "slack").is_dm, false);
750
- });
751
-
752
- test("enrichItem: a WIDE-path Cohort 1:1 DM (is_dm:true, NO channel_kind, cuid id) stays is_dm through the whole chain", async () => {
753
- // REGRESSION. The DEFAULT inbound reader is lib/org/inbound.pullWideInbound,
754
- // whose projection (lib/org/inbound/project.mjs toMessageEvent) marks a 1:1 DM
755
- // with is_dm:true + channel_type:"dm" but does NOT emit channel_kind (only the
756
- // non-default MESSAGING_INBOUND_WIDE=0 narrow reader sets channel_kind). Cohort
757
- // channel ids are cuids, so the Slack-style "dm/"/"D…" prefix heuristics never
758
- // fire on them. Before the fix, enrichItem derived is_dm ONLY from an (absent)
759
- // channel_kind or those (never-matching) heuristics and CLOBBERED the true
760
- // is_dm to false — routing a genuine 1:1 DM through the group election gate,
761
- // which then suppressed it. This drives the real chain end to end:
762
- // toMessageEvent → eventToInboxItem → writeInboxItem → parseInboxItemYaml → enrichItem
763
- // and asserts is_dm survives as true.
764
- const { toMessageEvent } = await import("../../lib/org/inbound/project.mjs");
765
- const { classifyEvent } = await import("../../lib/org/inbound/directedness.mjs");
766
- const { eventToInboxItem } = await import("../../lib/channels/inbox-item.mjs");
767
- const { writeInboxItem } = await import("../poller/utils.mjs");
768
- const { parseInboxItemYaml } = await import("../poller/inbox-scan-poller.mjs");
769
-
770
- const CUID = "clw9x7k4a0000abcd1234efgh"; // a real cuid: no "D…"/"dm/" prefix to key off
771
- const cand = classifyEvent({
772
- seq: 501, at: "2026-08-25T09:00:00.000Z", actor: "M-them",
773
- family: "messaging", kind: "send", entity_id: "m501",
774
- payload: { actor: "M-them", channelId: CUID, channelKind: "DM" },
775
- });
776
- const facts = { channels: new Map([[CUID, { id: CUID, kind: "DM", name: "them" }]]) };
777
- const mev = toMessageEvent({
778
- candidate: cand,
779
- verdict: { surface: "dm", reason: "dm", directed: true },
780
- hydrated: { ok: true, text: "ping", from: { id: "M-them", name: "Casey" }, channelId: CUID, channelLabel: "them" },
781
- me: "M-me",
782
- facts,
783
- });
784
-
785
- // The premise the regression rests on: the wide projection marks the DM but
786
- // does NOT emit channel_kind.
787
- assert.equal(mev.is_dm, true, "wide-path DM projects is_dm:true");
788
- assert.equal(mev.channel_type, "dm");
789
- assert.equal("channel_kind" in mev, false, "the DEFAULT wide reader emits NO channel_kind");
790
-
791
- // eventToInboxItem carries is_dm through but (correctly) attaches no channel_kind.
792
- const item = eventToInboxItem(mev);
793
- assert.equal(item.is_dm, true);
794
- assert.equal("channel_kind" in item, false);
795
-
796
- // writeInboxItem → parseInboxItemYaml: the on-disk round-trip the poller does.
797
- writeInboxItem("cohort", item, AGENT_DIR);
798
- const dir = join(AGENT_DIR, "state", "inbox", "cohort");
799
- const file = readdirSync(dir).find((f) => f.includes(item.id) && f.endsWith(".yaml"));
800
- assert.ok(file, "the inbox item was written to disk");
801
- const parsed = parseInboxItemYaml(readFileSync(join(dir, file), "utf8"));
802
- assert.equal(parsed.is_dm, true, "is_dm survives the YAML round-trip");
803
- assert.equal("channel_kind" in parsed, false, "and still no channel_kind on the wide path");
804
-
805
- // enrichItem is the last hop before the election gate. It MUST honor the
806
- // pre-existing boolean is_dm when channel_kind is absent, not clobber it.
807
- const enriched = daemon.enrichItem(parsed, "cohort");
808
- assert.equal(enriched.is_dm, true, "the wide-path 1:1 DM stays is_dm — it must SKIP the group election gate");
809
- });
810
-
811
- // ===========================================================================
812
- // NO FALLTHROUGH ON PERMANENT FAILURE — a permanent quick-reply send failure
813
- // (FORBIDDEN_SCOPE) must escalate, not spawn an autonomous session that would
814
- // post UNRELATED work into a channel the seat may not write to.
815
- // ===========================================================================
816
-
817
- test("NO FALLTHROUGH: a PERMANENT quick-reply failure escalates instead of spawning a session", async () => {
818
- resetState();
819
- const item = { id: "MSG-PERM", raw_ref: "slack:DPERM:1", service: "slack", channel: "dm/ceo-perm", channel_id: "DPERM0001", is_dm: true, sender: "ceo", subject: "forbidden send", content: "please post this" };
820
- let dispatched = false;
821
- const res = await daemon.answerItem(item, "slack", "MSG-PERM", "trace-perm", {
822
- classify: async () => ({ priority: "high", action: "respond", model: "haiku", summary: "quick answer", category: "action_required", directed_at_agent: true }),
823
- isQuickReply: () => true,
824
- sendQuickResponse: async () => ({ sent: false, permanent: true, code: "FORBIDDEN_SCOPE", error: "FORBIDDEN_SCOPE: blocked by send-gate" }),
825
- dispatch: () => { dispatched = true; },
826
- claudeAvailable: () => true,
827
- });
828
- assert.equal(dispatched, false, "a permanent send failure must NOT spawn an autonomous session");
829
- assert.equal(res.path, "quick_reply_permanent");
830
- assert.match(res.reason, /FORBIDDEN_SCOPE/);
831
- const naDir = join(AGENT_DIR, "state", "obligations", "needs-attention");
832
- assert.ok(existsSync(naDir) && readdirSync(naDir).length > 0, "a durable needs-attention escalation was written for an operator");
833
- });
834
-
835
- test("NO FALLTHROUGH: a TRANSIENT quick-reply failure still falls through to a full session", async () => {
836
- resetState();
837
- const item = { id: "MSG-TRAN", raw_ref: "slack:DTRAN:1", service: "slack", channel: "dm/ceo-tran", channel_id: "DTRAN0001", is_dm: true, sender: "ceo", subject: "transient send", content: "please post this" };
838
- let dispatched = false;
839
- const res = await daemon.answerItem(item, "slack", "MSG-TRAN", "trace-tran", {
840
- classify: async () => ({ priority: "high", action: "respond", model: "haiku", summary: "quick answer", category: "action_required", directed_at_agent: true }),
841
- isQuickReply: () => true,
842
- sendQuickResponse: async () => ({ sent: false, error: "hq 503 blip" }), // transient: NO permanent flag
843
- sendHoldingMessage: async () => ({ sent: true, holdingText: "on it" }),
844
- dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
845
- claudeAvailable: () => true,
846
- });
847
- assert.equal(dispatched, true, "a transient failure falls through to the full session, as before");
848
- assert.equal(res.path, "session");
849
- });
850
-
851
- test("REACTIVE PRIORITY: a quick reply yields the subscription (evicts backlog) BEFORE it generates", async () => {
852
- resetState();
853
- const item = { id: "MSG-YIELD", raw_ref: "slack:DYIELD:1", service: "slack", channel: "dm/ceo", channel_id: "DYIELD001", is_dm: true, sender: "ceo", subject: "quick q", content: "you around?" };
854
- const order = [];
855
- const res = await daemon.answerItem(item, "slack", "MSG-YIELD", "trace-yield", {
856
- classify: async () => ({ priority: "high", action: "respond", model: "haiku", summary: "quick answer", category: "action_required", directed_at_agent: true }),
857
- isQuickReply: () => true,
858
- yieldBacklogForReactive: () => { order.push("yield"); return 2; },
859
- sendQuickResponse: async () => { order.push("send"); return { sent: true }; },
860
- claudeAvailable: () => true,
861
- });
862
- assert.equal(res.path, "quick_reply");
863
- assert.deepEqual(order, ["yield", "send"], "backlog is yielded the subscription BEFORE the reply's claude call, not after");
864
- });
865
-
866
- test("resolveSelfMemberId reads the member cuid from the res frame's `result` (own-echo keystone)", async () => {
867
- daemon._resetSelfMemberId();
868
- const pk = process.env.COHORT_API_KEY, po = process.env.COHORT_ORG_ID, pa = process.env.COHORT_AGENT_ID;
869
- process.env.COHORT_API_KEY = "nlk_test";
870
- process.env.COHORT_ORG_ID = "org_test";
871
- process.env.COHORT_AGENT_ID = "A016";
872
- try {
873
- let calls = 0;
874
- // fetchSelfProfile returns the authed call() res frame: the member is under
875
- // `result`, and the org key is passed as apiKey. (Both were wrong in 2.11.7.)
876
- const mid = await daemon.resolveSelfMemberId({
877
- fetchSelfProfileImpl: async (o) => {
878
- calls++;
879
- assert.equal(o.agentId, "A016", "must pass the slug so /members/<slug> resolves");
880
- return { ok: true, result: { memberId: "cmSELFcuid", slug: o.agentId } };
881
- },
882
- });
883
- assert.equal(mid, "cmSELFcuid");
884
- // cached: a second call must NOT refetch
885
- const again = await daemon.resolveSelfMemberId({
886
- fetchSelfProfileImpl: async () => { throw new Error("must not refetch a cached self id"); },
887
- });
888
- assert.equal(again, "cmSELFcuid");
889
- assert.equal(calls, 1);
890
- } finally {
891
- daemon._resetSelfMemberId();
892
- if (pk === undefined) delete process.env.COHORT_API_KEY; else process.env.COHORT_API_KEY = pk;
893
- if (po === undefined) delete process.env.COHORT_ORG_ID; else process.env.COHORT_ORG_ID = po;
894
- if (pa === undefined) delete process.env.COHORT_AGENT_ID; else process.env.COHORT_AGENT_ID = pa;
895
- }
896
- });
897
-
898
- test("fastPathClassify: a short directed question skips the classifier; work-requests do not", async () => {
899
- const fp = (content, isDm = true, extra = {}) => daemon.fastPathClassify({ content, ...extra }, isDm);
900
- // Plain short questions → fast-path (quick reply, high priority, sonnet).
901
- const q = fp("what OS is your machine on?");
902
- assert.equal(q && q.fast_path, true);
903
- assert.equal(q.answerable, true);
904
- assert.equal(q.priority, "high");
905
- assert.equal(q.action, "respond");
906
- assert.ok(daemon.fastPathClassify({ content: "hey, you around?" }, true));
907
- assert.ok(daemon.fastPathClassify({ content: "thanks!" }, true));
908
- // Work-requests → NOT fast-pathed (fall through to full classify → session).
909
- assert.equal(fp("can you analyze the Q3 report and send me the findings?"), null);
910
- assert.equal(fp("please review the PR and fix the failing tests"), null);
911
- assert.equal(fp("build a dashboard for the sales pipeline"), null);
912
- // Non-questions, long messages, group, and non-DM → NOT fast-pathed.
913
- assert.equal(fp("here are the notes from the meeting for your records"), null);
914
- assert.equal(fp("x".repeat(300)), null);
915
- assert.equal(fp("what OS?", false), null); // not a DM
916
- assert.equal(fp(""), null);
917
- // Attachments → full path.
918
- assert.equal(daemon.fastPathClassify({ content: "what's this?", has_attachments: true }, true), null);
919
- // Kill switch (deps.disableFastPath) forces the full classify path.
920
- assert.ok(daemon.fastPathClassify({ content: "what OS?" }, true));
921
- assert.equal(daemon.fastPathClassify({ content: "what OS?" }, true, { disableFastPath: true }), null);
922
- });
923
-
924
- // ===========================================================================
925
- // SELF-ECHO — the agent's own outbound, re-ingested by a poll lane, must be
926
- // dropped BEFORE it can be classified, elected on, acked or answered. Recurred
927
- // live 2026-08-25 / 08-27 / 08-30: the old filter normalised the agent's full
928
- // name to "isla-roselli" while real events carry "Isla Roselli", so the agent
929
- // said "Looking now." to itself (and, in shared DMs, to another agent's echo).
930
- // ===========================================================================
931
-
932
- test("SELF-ECHO: an echoed own message is dropped at the processItem choke point — file retired, drop audited", async () => {
933
- resetState();
934
- const selfEcho = await import("./lib/self-echo.mjs");
935
- const { writeFileSync: wf } = await import("node:fs");
936
- mkdirSync(join(AGENT_DIR, "config"), { recursive: true });
937
- wf(join(AGENT_DIR, "config", "agent.json"), JSON.stringify({
938
- firstName: "Isla", fullName: "Isla Roselli", memberId: "cmqh0tcml004ihhl6zev6ocb5",
939
- }));
940
- selfEcho._resetSelfIdentity();
941
- try {
942
- // The REAL echo shape: display name verbatim, spaces and all.
943
- const item = { id: "MSG-SELF", raw_ref: "slack:DSELF:1", channel: "dm/candace", channel_id: "DSELF0001", sender: "Isla Roselli", content: "Both answers — and the second one settles it." };
944
- const name = writeLiveItem(item);
945
-
946
- let pipelineTouched = null;
947
- const res = await daemon.processItem(item, "slack", {
948
- classify: async () => { pipelineTouched = "classify"; return { priority: "high", action: "respond", model: "opus", summary: "x", category: "action_required", directed_at_agent: true }; },
949
- isQuickReply: () => { pipelineTouched = "isQuickReply"; return false; },
950
- sendQuickResponse: async () => { pipelineTouched = "quick_reply"; return { sent: true }; },
951
- sendHoldingMessage: async () => { pipelineTouched = "holding_ack"; return { sent: true, holdingText: "x" }; },
952
- dispatch: () => { pipelineTouched = "dispatch"; },
953
- claudeAvailable: () => true,
954
- });
955
-
956
- assert.equal(res && res.path, "filtered", "the echo is filtered, not answered");
957
- assert.equal(res && res.reason, "self_echo");
958
- assert.equal(pipelineTouched, null, "no classify, no ack, no reply, no session — the echo never enters the pipeline");
959
- assert.deepEqual(inboxFiles(), [`${name}.processed`], "the echo's inbox file is RETIRED — an un-retired echo would be re-scanned and re-dropped every tick forever");
960
- const auditPath = join(AGENT_DIR, "logs", "daemon", "self-echo-drops.jsonl");
961
- assert.ok(existsSync(auditPath), "every drop leaves an audit row");
962
- assert.match(readFileSync(auditPath, "utf-8"), /slack:DSELF:1/, "the audit row names the dropped event");
963
- } finally {
964
- rmSync(join(AGENT_DIR, "config", "agent.json"), { force: true });
965
- selfEcho._resetSelfIdentity();
966
- }
967
- });
968
-
969
- test("SELF-ECHO: a genuine colleague's message is untouched by the guard", async () => {
970
- resetState();
971
- const selfEcho = await import("./lib/self-echo.mjs");
972
- const { writeFileSync: wf } = await import("node:fs");
973
- mkdirSync(join(AGENT_DIR, "config"), { recursive: true });
974
- wf(join(AGENT_DIR, "config", "agent.json"), JSON.stringify({
975
- firstName: "Isla", fullName: "Isla Roselli", memberId: "cmqh0tcml004ihhl6zev6ocb5",
976
- }));
977
- selfEcho._resetSelfIdentity();
978
- try {
979
- const item = { id: "MSG-COLLEAGUE", raw_ref: "slack:DCOLL:1", channel: "dm/jordan", channel_id: "DCOLL0001", is_dm: true, sender: "Jordan Blake", content: "please draft the board memo" };
980
- writeLiveItem(item);
981
- let dispatched = false;
982
- const res = await daemon.answerItem(item, "slack", "slack:DCOLL:1", "trace-coll", {
983
- classify: async () => ({ priority: "critical", action: "respond", model: "opus", summary: "draft board memo", category: "action_required", directed_at_agent: true }),
984
- isQuickReply: () => false,
985
- sendHoldingMessage: async () => ({ sent: true, holdingText: "Starting on the board memo now." }),
986
- dispatch: (_p, _it, _cr, _s, opts) => { dispatched = true; opts.onClose({ ok: true, code: 0 }); },
987
- claudeAvailable: () => true,
988
- });
989
- assert.equal(dispatched, true, "a real colleague's ask still dispatches");
990
- assert.equal(res.path, "session");
991
- } finally {
992
- rmSync(join(AGENT_DIR, "config", "agent.json"), { force: true });
993
- selfEcho._resetSelfIdentity();
994
- }
995
- });